├── tests ├── __init__.py ├── test_loaders │ ├── __init__.py │ ├── test_babel_loader.py │ ├── test_json_loader.py │ ├── conftest.py │ └── test_common.py ├── translations │ └── babel │ │ ├── de_DE │ │ └── LC_MESSAGES │ │ │ ├── messages.mo │ │ │ └── messages.po │ │ ├── en_US │ │ └── LC_MESSAGES │ │ │ ├── messages.mo │ │ │ └── messages.po │ │ └── es_AR │ │ └── LC_MESSAGES │ │ ├── messages.mo │ │ └── messages.po ├── conftest.py ├── test_pydantic_messages.py └── test_main.py ├── docs ├── en │ ├── data │ │ └── .gitignore │ ├── docs │ │ ├── img │ │ │ ├── favicon.png │ │ │ ├── icon-white.png │ │ │ ├── logo-white.png │ │ │ └── logo-white.svg │ │ ├── css │ │ │ ├── custom.css │ │ │ └── termynal.css │ │ ├── help-pydantic-i18n.md │ │ ├── js │ │ │ ├── custom.js │ │ │ └── termynal.js │ │ ├── index.md │ │ ├── contributing.md │ │ └── release-notes.md │ ├── overrides │ │ └── main.html │ └── mkdocs.yml └── missing-translation.md ├── pydantic_i18n ├── py.typed ├── __init__.py ├── loaders.py └── main.py ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── feature-request.md │ └── bug_report.md ├── actions │ └── comment-docs-preview-in-pr │ │ ├── Dockerfile │ │ ├── README.md │ │ ├── action.yml │ │ └── app │ │ └── main.py ├── dependabot.yml └── workflows │ ├── new_contributor_pr.yml │ ├── latest-changes.yml │ ├── publish.yml │ ├── test.yml │ ├── preview-docs.yml │ └── build-docs.yml ├── scripts ├── publish.sh ├── docs-live.sh ├── build-docs.sh ├── test-cov-html.sh ├── clean.sh ├── zip-docs.sh ├── test.sh ├── format-imports.sh ├── lint.sh ├── format.sh └── docs.py ├── docs_src ├── pydantic_v1 │ ├── own-loader │ │ ├── translations │ │ │ ├── en_US.csv │ │ │ └── de_DE.csv │ │ └── tutorial001.py │ ├── json-loader │ │ ├── translations │ │ │ ├── en_US.json │ │ │ └── de_DE.json │ │ └── tutorial001.py │ ├── babel-loader │ │ ├── translations │ │ │ ├── de_DE │ │ │ │ └── LC_MESSAGES │ │ │ │ │ ├── messages.mo │ │ │ │ │ └── messages.po │ │ │ └── en_US │ │ │ │ └── LC_MESSAGES │ │ │ │ ├── messages.mo │ │ │ │ └── messages.po │ │ └── tutorial001.py │ ├── fastapi-usage │ │ ├── main.py │ │ └── tr.py │ ├── pydantic-messages │ │ ├── tutorial001.py │ │ └── tutorial002.py │ ├── dict-loader │ │ └── tutorial001.py │ └── placeholder │ │ └── tutorial001.py └── pydantic_v2 │ ├── own-loader │ ├── translations │ │ ├── en_US.csv │ │ └── de_DE.csv │ └── tutorial001.py │ ├── json-loader │ ├── translations │ │ ├── en_US.json │ │ └── de_DE.json │ └── tutorial001.py │ ├── fastapi-usage │ ├── main.py │ └── tr.py │ ├── pydantic-messages │ ├── tutorial001.py │ └── tutorial002.py │ ├── babel-loader │ ├── translations │ │ ├── de_DE │ │ │ └── LC_MESSAGES │ │ │ │ ├── messages.mo │ │ │ │ └── messages.po │ │ └── en_US │ │ │ └── LC_MESSAGES │ │ │ ├── messages.mo │ │ │ └── messages.po │ └── tutorial001.py │ ├── dict-loader │ └── tutorial001.py │ └── placeholder │ └── tutorial001.py ├── .editorconfig ├── setup.cfg ├── mypy.ini ├── .coveragerc ├── LICENSE ├── pyproject.toml ├── .gitignore └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/en/data/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pydantic_i18n/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_loaders/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false -------------------------------------------------------------------------------- /scripts/publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | flit publish 6 | -------------------------------------------------------------------------------- /docs_src/pydantic_v1/own-loader/translations/en_US.csv: -------------------------------------------------------------------------------- 1 | field required,field required 2 | -------------------------------------------------------------------------------- /docs_src/pydantic_v2/own-loader/translations/en_US.csv: -------------------------------------------------------------------------------- 1 | Field required,field required 2 | -------------------------------------------------------------------------------- /docs_src/pydantic_v1/own-loader/translations/de_DE.csv: -------------------------------------------------------------------------------- 1 | field required,Feld erforderlich 2 | -------------------------------------------------------------------------------- /docs_src/pydantic_v2/own-loader/translations/de_DE.csv: -------------------------------------------------------------------------------- 1 | Field required,Feld erforderlich 2 | -------------------------------------------------------------------------------- /scripts/docs-live.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | mkdocs serve --dev-addr 0.0.0.0:8008 6 | -------------------------------------------------------------------------------- /docs_src/pydantic_v1/json-loader/translations/en_US.json: -------------------------------------------------------------------------------- 1 | { 2 | "field required": "field required" 3 | } 4 | -------------------------------------------------------------------------------- /docs_src/pydantic_v2/json-loader/translations/en_US.json: -------------------------------------------------------------------------------- 1 | { 2 | "Field required": "field required" 3 | } 4 | -------------------------------------------------------------------------------- /docs/en/docs/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boardpack/pydantic-i18n/HEAD/docs/en/docs/img/favicon.png -------------------------------------------------------------------------------- /docs_src/pydantic_v1/json-loader/translations/de_DE.json: -------------------------------------------------------------------------------- 1 | { 2 | "field required": "Feld erforderlich" 3 | } 4 | -------------------------------------------------------------------------------- /docs_src/pydantic_v2/json-loader/translations/de_DE.json: -------------------------------------------------------------------------------- 1 | { 2 | "Field required": "Feld erforderlich" 3 | } 4 | -------------------------------------------------------------------------------- /scripts/build-docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -x 5 | 6 | python ./scripts/docs.py build-all 7 | -------------------------------------------------------------------------------- /docs/en/docs/img/icon-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boardpack/pydantic-i18n/HEAD/docs/en/docs/img/icon-white.png -------------------------------------------------------------------------------- /docs/en/docs/img/logo-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boardpack/pydantic-i18n/HEAD/docs/en/docs/img/logo-white.png -------------------------------------------------------------------------------- /scripts/test-cov-html.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -x 5 | 6 | bash scripts/test.sh --cov-report=html ${@} 7 | -------------------------------------------------------------------------------- /scripts/clean.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | if [ -d 'dist' ] ; then 4 | rm -r dist 5 | fi 6 | if [ -d 'site' ] ; then 7 | rm -r site 8 | fi 9 | -------------------------------------------------------------------------------- /tests/translations/babel/de_DE/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boardpack/pydantic-i18n/HEAD/tests/translations/babel/de_DE/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /tests/translations/babel/en_US/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boardpack/pydantic-i18n/HEAD/tests/translations/babel/en_US/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /tests/translations/babel/es_AR/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boardpack/pydantic-i18n/HEAD/tests/translations/babel/es_AR/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /scripts/zip-docs.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | 3 | set -x 4 | set -e 5 | 6 | if [ -f docs.zip ]; then 7 | rm -rf docs.zip 8 | fi 9 | zip -r docs.zip ./site 10 | -------------------------------------------------------------------------------- /.github/actions/comment-docs-preview-in-pr/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7 2 | 3 | RUN pip install httpx "pydantic==1.5.1" pygithub 4 | 5 | COPY ./app /app 6 | 7 | CMD ["python", "/app/main.py"] 8 | -------------------------------------------------------------------------------- /scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -x 5 | 6 | bash ./scripts/lint.sh 7 | pytest --cov=pydantic_i18n --cov=tests --cov-report=term-missing --cov-report=xml tests ${@} 8 | -------------------------------------------------------------------------------- /docs_src/pydantic_v1/babel-loader/translations/de_DE/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boardpack/pydantic-i18n/HEAD/docs_src/pydantic_v1/babel-loader/translations/de_DE/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /docs_src/pydantic_v1/babel-loader/translations/en_US/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boardpack/pydantic-i18n/HEAD/docs_src/pydantic_v1/babel-loader/translations/en_US/LC_MESSAGES/messages.mo -------------------------------------------------------------------------------- /scripts/format-imports.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | set -x 3 | 4 | # Sort imports one per line, so autoflake can remove unused imports 5 | isort pydantic_i18n tests scripts --force-single-line-imports 6 | sh ./scripts/format.sh 7 | -------------------------------------------------------------------------------- /scripts/lint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -x 5 | 6 | mypy pydantic_i18n 7 | flake8 pydantic_i18n tests 8 | black pydantic_i18n tests --check 9 | isort pydantic_i18n tests scripts --check-only 10 | -------------------------------------------------------------------------------- /docs/missing-translation.md: -------------------------------------------------------------------------------- 1 | !!! warning 2 | The current page still doesn't have a translation for this language. 3 | 4 | But you can help translating it: [Contributing](https://pydantic-i18n.boardpack.org/contributing/){.internal-link target=_blank}. 5 | -------------------------------------------------------------------------------- /tests/test_loaders/test_babel_loader.py: -------------------------------------------------------------------------------- 1 | from pydantic_i18n import BabelLoader 2 | 3 | 4 | def test_init(babel_translations_directory: str): 5 | babel_loader = BabelLoader(babel_translations_directory) 6 | assert len(babel_loader.translations) >= 2 7 | -------------------------------------------------------------------------------- /scripts/format.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | set -x 3 | 4 | autoflake --remove-all-unused-imports --recursive --remove-unused-variables --in-place pydantic_i18n tests scripts --exclude=__init__.py 5 | black pydantic_i18n tests scripts 6 | isort pydantic_i18n tests scripts 7 | -------------------------------------------------------------------------------- /docs/en/overrides/main.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block site_nav %} 4 |
5 |
6 |
7 | {% include "partials/toc.html" %} 8 |
9 |
10 |
11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /docs/en/docs/css/custom.css: -------------------------------------------------------------------------------- 1 | 2 | a.external-link::after { 3 | /* \00A0 is a non-breaking space 4 | to make the mark be on the same line as the link 5 | */ 6 | content: "\00A0[↪]"; 7 | } 8 | 9 | a.internal-link::after { 10 | /* \00A0 is a non-breaking space 11 | to make the mark be on the same line as the link 12 | */ 13 | content: "\00A0↪"; 14 | } 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.bat] 14 | indent_style = tab 15 | end_of_line = crlf 16 | 17 | [LICENSE] 18 | insert_final_newline = false 19 | 20 | [Makefile] 21 | indent_style = tab -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pytest 4 | 5 | from pydantic_i18n import BabelLoader 6 | 7 | 8 | @pytest.fixture 9 | def babel_translations_directory() -> str: 10 | return os.path.abspath("./tests/translations/babel") 11 | 12 | 13 | @pytest.fixture 14 | def babel_loader(babel_translations_directory: str) -> BabelLoader: 15 | return BabelLoader(babel_translations_directory) 16 | -------------------------------------------------------------------------------- /.github/actions/comment-docs-preview-in-pr/README.md: -------------------------------------------------------------------------------- 1 | # Comment docs preview in PR 2 | 3 | This action was used from the [FastAPI](https://github.com/tiangolo/fastapi) project, original one can be found [here](https://github.com/tiangolo/fastapi/tree/master/.github/actions/comment-docs-preview-in-pr). Special thanks to [Sebastián Ramírez](https://github.com/tiangolo). 4 | 5 | ## License 6 | 7 | This project is licensed under the terms of the MIT license. 8 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E203, E266, E501, W503 3 | max-line-length = 88 4 | max-complexity = 18 5 | select = B,C,E,F,W,T4 6 | exclude = docs, __init__.py 7 | 8 | [isort] 9 | multi_line_output=3 10 | include_trailing_comma=True 11 | force_grid_wrap=0 12 | use_parentheses=True 13 | line_length=88 14 | 15 | [mypy] 16 | files=pydantic_i18n,tests 17 | ignore_missing_imports=true 18 | 19 | [aliases] 20 | test = pytest 21 | 22 | [tool:pytest] 23 | testpaths=tests/ 24 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | 3 | # --strict 4 | disallow_any_generics = True 5 | disallow_subclassing_any = True 6 | disallow_untyped_calls = True 7 | disallow_untyped_defs = True 8 | disallow_incomplete_defs = True 9 | check_untyped_defs = True 10 | disallow_untyped_decorators = True 11 | no_implicit_optional = True 12 | warn_redundant_casts = True 13 | warn_unused_ignores = True 14 | warn_return_any = True 15 | implicit_reexport = False 16 | strict_equality = True 17 | # --strict end 18 | -------------------------------------------------------------------------------- /pydantic_i18n/__init__.py: -------------------------------------------------------------------------------- 1 | """pydantic-i18n is an extension to support an i18n for the pydantic error messages.""" 2 | 3 | __author__ = """Roman Sadzhenytsia""" 4 | __email__ = "urchin.dukkee@gmail.com" 5 | __version__ = "0.4.5" 6 | 7 | from .loaders import BabelLoader, BaseLoader, DictLoader, JsonLoader 8 | from .main import PydanticI18n 9 | 10 | __all__ = ( 11 | "PydanticI18n", 12 | "BaseLoader", 13 | "BabelLoader", 14 | "DictLoader", 15 | "JsonLoader", 16 | ) 17 | -------------------------------------------------------------------------------- /.github/actions/comment-docs-preview-in-pr/action.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Comment Docs Preview in PR 3 | description: Comment with the docs URL preview in the PR 4 | author: Sebastián Ramírez 5 | inputs: 6 | token: 7 | description: Token for the repo. Can be passed in using {{ secrets.GITHUB_TOKEN }} 8 | required: true 9 | deploy_url: 10 | description: The deployment URL to comment in the PR 11 | required: true 12 | runs: 13 | using: docker 14 | image: Dockerfile 15 | 16 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "pip" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | day: "monday" 9 | pull-request-branch-name: 10 | separator: "-" 11 | commit-message: 12 | prefix: "⬆" 13 | 14 | - package-ecosystem: "github-actions" 15 | directory: "/" 16 | schedule: 17 | interval: "weekly" 18 | day: "monday" 19 | pull-request-branch-name: 20 | separator: "-" 21 | commit-message: 22 | prefix: "⬆" 23 | -------------------------------------------------------------------------------- /docs_src/pydantic_v1/fastapi-usage/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import Depends, FastAPI, Request 2 | from fastapi.exceptions import RequestValidationError 3 | 4 | from pydantic import BaseModel 5 | 6 | import tr 7 | 8 | app = FastAPI(dependencies=[Depends(tr.get_locale)]) 9 | 10 | app.add_exception_handler(RequestValidationError, tr.validation_exception_handler) 11 | 12 | 13 | class User(BaseModel): 14 | name: str 15 | 16 | 17 | @app.post("/user", response_model=User) 18 | def create_user(request: Request, user: User): 19 | pass 20 | -------------------------------------------------------------------------------- /docs_src/pydantic_v2/fastapi-usage/main.py: -------------------------------------------------------------------------------- 1 | from fastapi import Depends, FastAPI, Request 2 | from fastapi.exceptions import RequestValidationError 3 | 4 | from pydantic import BaseModel 5 | 6 | import tr 7 | 8 | app = FastAPI(dependencies=[Depends(tr.get_locale)]) 9 | 10 | app.add_exception_handler(RequestValidationError, tr.validation_exception_handler) 11 | 12 | 13 | class User(BaseModel): 14 | name: str 15 | 16 | 17 | @app.post("/user", response_model=User) 18 | def create_user(request: Request, user: User): 19 | pass 20 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = pydantic-i18n 3 | omit = pydantic-i18n/__main__.py 4 | 5 | [report] 6 | exclude_lines = 7 | # Have to re-enable the standard pragma 8 | pragma: no cover 9 | 10 | # Don't complain about missing debug-only code: 11 | def __repr__ 12 | if self\.debug 13 | 14 | # Don't complain if tests don't hit defensive assertion code: 15 | raise AssertionError 16 | raise NotImplementedError 17 | 18 | # Don't complain if non-runnable code isn't run: 19 | if 0: 20 | if __name__ == .__main__.: 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | labels: enhancement 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | 9 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 10 | 11 | **Describe the solution you'd like** 12 | 13 | A clear and concise description of what you want to happen. 14 | 15 | **Additional context** 16 | 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /tests/test_loaders/test_json_loader.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pydantic_i18n import JsonLoader 4 | 5 | 6 | def test_init(json_translations_directory: str): 7 | json_loader = JsonLoader(json_translations_directory) 8 | assert json_loader.directory == json_translations_directory 9 | 10 | 11 | def test_not_dir_path(tmp_path): 12 | fp = tmp_path / "hello.txt" 13 | fp.touch() 14 | 15 | with pytest.raises(OSError) as excinfo: 16 | JsonLoader(str(fp)) 17 | 18 | assert str(excinfo.value) == f"'{str(fp)}' is not a directory." 19 | -------------------------------------------------------------------------------- /docs_src/pydantic_v1/pydantic-messages/tutorial001.py: -------------------------------------------------------------------------------- 1 | from pydantic_i18n import PydanticI18n 2 | 3 | print(PydanticI18n.get_pydantic_messages()) 4 | # { 5 | # "field required": "field required", 6 | # "extra fields not permitted": "extra fields not permitted", 7 | # "none is not an allowed value": "none is not an allowed value", 8 | # "value is not none": "value is not none", 9 | # "value could not be parsed to a boolean": "value could not be parsed to a boolean", 10 | # "byte type expected": "byte type expected", 11 | # ..... 12 | # } 13 | -------------------------------------------------------------------------------- /docs_src/pydantic_v2/pydantic-messages/tutorial001.py: -------------------------------------------------------------------------------- 1 | from pydantic_i18n import PydanticI18n 2 | 3 | print(PydanticI18n.get_pydantic_messages()) 4 | # { 5 | # "Object has no attribute '{}'": "Object has no attribute '{}'", 6 | # "Invalid JSON: {}": "Invalid JSON: {}", 7 | # "JSON input should be string, bytes or bytearray": "JSON input should be string, bytes or bytearray", 8 | # "Recursion error - cyclic reference detected": "Recursion error - cyclic reference detected", 9 | # "Field required": "Field required", 10 | # "Field is frozen": "Field is frozen", 11 | # ..... 12 | # } 13 | -------------------------------------------------------------------------------- /docs_src/pydantic_v1/babel-loader/tutorial001.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, ValidationError 2 | from pydantic_i18n import PydanticI18n, BabelLoader 3 | 4 | loader = BabelLoader("./translations") 5 | tr = PydanticI18n(loader) 6 | 7 | 8 | class User(BaseModel): 9 | name: str 10 | 11 | 12 | try: 13 | User() 14 | except ValidationError as e: 15 | translated_errors = tr.translate(e.errors(), locale="de") 16 | 17 | print(translated_errors) 18 | # [ 19 | # { 20 | # 'loc': ('name',), 21 | # 'msg': 'Feld erforderlich', 22 | # 'type': 'value_error.missing' 23 | # } 24 | # ] 25 | -------------------------------------------------------------------------------- /docs_src/pydantic_v1/json-loader/tutorial001.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, ValidationError 2 | from pydantic_i18n import PydanticI18n, JsonLoader 3 | 4 | loader = JsonLoader("./translations") 5 | tr = PydanticI18n(loader) 6 | 7 | 8 | class User(BaseModel): 9 | name: str 10 | 11 | 12 | try: 13 | User() 14 | except ValidationError as e: 15 | translated_errors = tr.translate(e.errors(), locale="de_DE") 16 | 17 | print(translated_errors) 18 | # [ 19 | # { 20 | # 'loc': ('name',), 21 | # 'msg': 'Feld erforderlich', 22 | # 'type': 'value_error.missing' 23 | # } 24 | # ] 25 | -------------------------------------------------------------------------------- /docs_src/pydantic_v2/babel-loader/translations/de_DE/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- 1 | ��,<=�L�Field requiredProject-Id-Version: PROJECT VERSION 2 | Report-Msgid-Bugs-To: EMAIL@ADDRESS 3 | POT-Creation-Date: 2021-07-25 01:32+0300 4 | PO-Revision-Date: 2021-07-25 01:37+0300 5 | Last-Translator: FULL NAME 6 | Language: de_DE 7 | Language-Team: de_DE 8 | Plural-Forms: nplurals=2; plural=(n != 1); 9 | MIME-Version: 1.0 10 | Content-Type: text/plain; charset=utf-8 11 | Content-Transfer-Encoding: 8bit 12 | Generated-By: Babel 2.14.0 13 | Feld erforderlich 14 | -------------------------------------------------------------------------------- /docs_src/pydantic_v2/babel-loader/translations/en_US/LC_MESSAGES/messages.mo: -------------------------------------------------------------------------------- 1 | ��,<=�L�Field requiredProject-Id-Version: PROJECT VERSION 2 | Report-Msgid-Bugs-To: EMAIL@ADDRESS 3 | POT-Creation-Date: 2021-07-25 01:32+0300 4 | PO-Revision-Date: 2021-07-25 01:37+0300 5 | Last-Translator: FULL NAME 6 | Language: en_US 7 | Language-Team: en_US 8 | Plural-Forms: nplurals=2; plural=(n != 1); 9 | MIME-Version: 1.0 10 | Content-Type: text/plain; charset=utf-8 11 | Content-Transfer-Encoding: 8bit 12 | Generated-By: Babel 2.14.0 13 | Field required 14 | -------------------------------------------------------------------------------- /.github/workflows/new_contributor_pr.yml: -------------------------------------------------------------------------------- 1 | name: New contributor message 2 | 3 | on: 4 | pull_request_target: 5 | types: [opened] 6 | 7 | jobs: 8 | build: 9 | name: Hello new contributor 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/first-interaction@v1 13 | with: 14 | repo-token: ${{ secrets.GITHUB_TOKEN }} 15 | pr-message: | 16 | Hello! Thank you for your contribution 💪 17 | 18 | As it's your first contribution be sure to check out the [contribution notes](https://pydantic-i18n.boardpack.org/contributing/). 19 | 20 | Welcome aboard ⛵️! 21 | -------------------------------------------------------------------------------- /docs_src/pydantic_v2/pydantic-messages/tutorial002.py: -------------------------------------------------------------------------------- 1 | from pydantic_i18n import PydanticI18n 2 | 3 | print(PydanticI18n.get_pydantic_messages(output="json")) 4 | # { 5 | # "Field required": "Field required", 6 | # "Field is frozen": "Field is frozen", 7 | # "Error extracting attribute: {}": "Error extracting attribute: {}", 8 | # ..... 9 | # } 10 | 11 | print(PydanticI18n.get_pydantic_messages(output="babel")) 12 | # msgid "Field required" 13 | # msgstr "Field required" 14 | # 15 | # msgid "Field is frozen" 16 | # msgstr "Field is frozen" 17 | # 18 | # msgid "Error extracting attribute: {}" 19 | # msgstr "Error extracting attribute: {}" 20 | # .... 21 | -------------------------------------------------------------------------------- /docs_src/pydantic_v1/pydantic-messages/tutorial002.py: -------------------------------------------------------------------------------- 1 | from pydantic_i18n import PydanticI18n 2 | 3 | print(PydanticI18n.get_pydantic_messages(output="json")) 4 | # { 5 | # "field required": "field required", 6 | # "extra fields not permitted": "extra fields not permitted", 7 | # "none is not an allowed value": "none is not an allowed value", 8 | # ..... 9 | # } 10 | 11 | print(PydanticI18n.get_pydantic_messages(output="babel")) 12 | # msgid "field required" 13 | # msgstr "field required" 14 | # 15 | # msgid "extra fields not permitted" 16 | # msgstr "extra fields not permitted" 17 | # 18 | # msgid "none is not an allowed value" 19 | # msgstr "none is not an allowed value" 20 | # .... 21 | -------------------------------------------------------------------------------- /.github/workflows/latest-changes.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Latest Changes 3 | 4 | on: 5 | pull_request_target: 6 | branches: 7 | - master 8 | types: 9 | - closed 10 | workflow_dispatch: 11 | inputs: 12 | number: 13 | description: PR number 14 | required: true 15 | 16 | jobs: 17 | latest-changes: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: docker://tiangolo/latest-changes:0.0.3 22 | with: 23 | token: ${{ secrets.GITHUB_TOKEN }} 24 | latest_changes_file: docs/en/docs/release-notes.md 25 | latest_changes_header: '## Latest Changes\n\n' 26 | debug_logs: true 27 | 28 | -------------------------------------------------------------------------------- /docs_src/pydantic_v2/babel-loader/tutorial001.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, ValidationError 2 | from pydantic_i18n import PydanticI18n, BabelLoader 3 | 4 | loader = BabelLoader("./translations") 5 | tr = PydanticI18n(loader) 6 | 7 | 8 | class User(BaseModel): 9 | name: str 10 | 11 | 12 | try: 13 | User() 14 | except ValidationError as e: 15 | translated_errors = tr.translate(e.errors(), locale="de_DE") 16 | 17 | print(translated_errors) 18 | # [ 19 | # { 20 | # 'type': 'missing', 21 | # 'loc': ('name',), 22 | # 'msg': 'Feld erforderlich', 23 | # 'input': { 24 | # 25 | # }, 26 | # 'url': 'https://errors.pydantic.dev/2.6/v/missing' 27 | # } 28 | # ] 29 | -------------------------------------------------------------------------------- /docs_src/pydantic_v2/json-loader/tutorial001.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, ValidationError 2 | from pydantic_i18n import PydanticI18n, JsonLoader 3 | 4 | loader = JsonLoader("./translations") 5 | tr = PydanticI18n(loader) 6 | 7 | 8 | class User(BaseModel): 9 | name: str 10 | 11 | 12 | try: 13 | User() 14 | except ValidationError as e: 15 | translated_errors = tr.translate(e.errors(), locale="de_DE") 16 | 17 | print(translated_errors) 18 | # [ 19 | # { 20 | # 'type': 'missing', 21 | # 'loc': ('name', 22 | # ), 23 | # 'msg': 'Feld erforderlich', 24 | # 'input': { 25 | # 26 | # }, 27 | # 'url': 'https://errors.pydantic.dev/2.6/v/missing' 28 | # } 29 | # ] 30 | -------------------------------------------------------------------------------- /docs_src/pydantic_v1/dict-loader/tutorial001.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, ValidationError 2 | from pydantic_i18n import PydanticI18n 3 | 4 | 5 | translations = { 6 | "en_US": { 7 | "field required": "field required", 8 | }, 9 | "de_DE": { 10 | "field required": "Feld erforderlich", 11 | }, 12 | } 13 | 14 | tr = PydanticI18n(translations) 15 | 16 | 17 | class User(BaseModel): 18 | name: str 19 | 20 | 21 | try: 22 | User() 23 | except ValidationError as e: 24 | translated_errors = tr.translate(e.errors(), locale="de_DE") 25 | 26 | print(translated_errors) 27 | # [ 28 | # { 29 | # 'loc': ('name',), 30 | # 'msg': 'Feld erforderlich', 31 | # 'type': 'value_error.missing' 32 | # } 33 | # ] 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | labels: bug 5 | --- 6 | 7 | **Describe the bug** 8 | 9 | A clear and concise description of what the bug is. 10 | 11 | **To Reproduce** 12 | 13 | Steps to reproduce the behavior: 14 | 1. 15 | 16 | ### Environment 17 | 18 | * OS: [e.g. Linux / Windows / macOS]: 19 | * pydantic-i18n version [e.g. 0.1.0]: 20 | 21 | To know the pydantic-i18n version use: 22 | 23 | ```bash 24 | python -c "import pydantic_i18n; print(pydantic_i18n.__version__)" 25 | ``` 26 | 27 | * Python version: 28 | 29 | To know the Python version use: 30 | 31 | ```bash 32 | python --version 33 | ``` 34 | 35 | ### Additional context 36 | 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /tests/translations/babel/de_DE/LC_MESSAGES/messages.po: -------------------------------------------------------------------------------- 1 | # German (Germany) translations for PROJECT. 2 | # Copyright (C) 2021 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2021. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PROJECT VERSION\n" 9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 10 | "POT-Creation-Date: 2021-07-25 01:32+0300\n" 11 | "PO-Revision-Date: 2021-07-25 01:37+0300\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language: de_DE\n" 14 | "Language-Team: de_DE \n" 15 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.9.1\n" 20 | 21 | msgid "field required" 22 | msgstr "Feld erforderlich" 23 | -------------------------------------------------------------------------------- /docs_src/pydantic_v1/babel-loader/translations/de_DE/LC_MESSAGES/messages.po: -------------------------------------------------------------------------------- 1 | # German (Germany) translations for PROJECT. 2 | # Copyright (C) 2021 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2021. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PROJECT VERSION\n" 9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 10 | "POT-Creation-Date: 2021-07-25 01:32+0300\n" 11 | "PO-Revision-Date: 2021-07-25 01:37+0300\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language: de_DE\n" 14 | "Language-Team: de_DE \n" 15 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.9.1\n" 20 | 21 | msgid "field required" 22 | msgstr "Feld erforderlich" 23 | -------------------------------------------------------------------------------- /docs_src/pydantic_v2/babel-loader/translations/de_DE/LC_MESSAGES/messages.po: -------------------------------------------------------------------------------- 1 | # German (Germany) translations for PROJECT. 2 | # Copyright (C) 2021 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2021. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PROJECT VERSION\n" 9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 10 | "POT-Creation-Date: 2021-07-25 01:32+0300\n" 11 | "PO-Revision-Date: 2021-07-25 01:37+0300\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language: de_DE\n" 14 | "Language-Team: de_DE \n" 15 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.9.1\n" 20 | 21 | msgid "Field required" 22 | msgstr "Feld erforderlich" 23 | -------------------------------------------------------------------------------- /docs_src/pydantic_v2/dict-loader/tutorial001.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, ValidationError 2 | from pydantic_i18n import PydanticI18n 3 | 4 | 5 | translations = { 6 | "en_US": { 7 | "Field required": "field required", 8 | }, 9 | "de_DE": { 10 | "Field required": "Feld erforderlich", 11 | }, 12 | } 13 | 14 | tr = PydanticI18n(translations) 15 | 16 | 17 | class User(BaseModel): 18 | name: str 19 | 20 | 21 | try: 22 | User() 23 | except ValidationError as e: 24 | translated_errors = tr.translate(e.errors(), locale="de_DE") 25 | 26 | print(translated_errors) 27 | # [ 28 | # { 29 | # 'type': 'missing', 30 | # 'loc': ('name',), 31 | # 'msg': 'Feld erforderlich', 32 | # 'input': { 33 | # 34 | # }, 35 | # 'url': 'https://errors.pydantic.dev/2.6/v/missing' 36 | # } 37 | # ] 38 | -------------------------------------------------------------------------------- /docs_src/pydantic_v1/babel-loader/translations/en_US/LC_MESSAGES/messages.po: -------------------------------------------------------------------------------- 1 | # English (United States) translations for PROJECT. 2 | # Copyright (C) 2021 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2021. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PROJECT VERSION\n" 9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 10 | "POT-Creation-Date: 2021-07-25 01:32+0300\n" 11 | "PO-Revision-Date: 2021-07-25 01:37+0300\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language: en_US\n" 14 | "Language-Team: en_US \n" 15 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.9.1\n" 20 | 21 | msgid "field required" 22 | msgstr "field required" 23 | -------------------------------------------------------------------------------- /docs_src/pydantic_v2/babel-loader/translations/en_US/LC_MESSAGES/messages.po: -------------------------------------------------------------------------------- 1 | # English (United States) translations for PROJECT. 2 | # Copyright (C) 2021 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2021. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PROJECT VERSION\n" 9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 10 | "POT-Creation-Date: 2021-07-25 01:32+0300\n" 11 | "PO-Revision-Date: 2021-07-25 01:37+0300\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language: en_US\n" 14 | "Language-Team: en_US \n" 15 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.9.1\n" 20 | 21 | msgid "Field required" 22 | msgstr "Field required" 23 | -------------------------------------------------------------------------------- /tests/translations/babel/es_AR/LC_MESSAGES/messages.po: -------------------------------------------------------------------------------- 1 | # English (United States) translations for PROJECT. 2 | # Copyright (C) 2021 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2021. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PROJECT VERSION\n" 9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 10 | "POT-Creation-Date: 2021-07-25 01:32+0300\n" 11 | "PO-Revision-Date: 2021-07-25 01:37+0300\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language: en_US\n" 14 | "Language-Team: en_US \n" 15 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.9.1\n" 20 | 21 | msgid "field required" 22 | msgstr "field required" 23 | 24 | msgid "value is not a valid color: {}" 25 | msgstr "no es un color válido: {}" 26 | -------------------------------------------------------------------------------- /tests/translations/babel/en_US/LC_MESSAGES/messages.po: -------------------------------------------------------------------------------- 1 | # English (United States) translations for PROJECT. 2 | # Copyright (C) 2021 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2021. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PROJECT VERSION\n" 9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 10 | "POT-Creation-Date: 2021-07-25 01:32+0300\n" 11 | "PO-Revision-Date: 2021-07-25 01:37+0300\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language: en_US\n" 14 | "Language-Team: en_US \n" 15 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.9.1\n" 20 | 21 | msgid "field required" 22 | msgstr "field required" 23 | 24 | msgid "value is not a valid color: {}" 25 | msgstr "value is not a valid color: {}" 26 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: 6 | - created 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Dump GitHub context 13 | env: 14 | GITHUB_CONTEXT: ${{ toJson(github) }} 15 | run: echo "$GITHUB_CONTEXT" 16 | - uses: actions/checkout@v4 17 | - name: Set up Python 18 | uses: actions/setup-python@v5 19 | with: 20 | python-version: "3.8" 21 | - name: Install Flit 22 | run: pip install flit 23 | - name: Install Dependencies 24 | run: flit install --symlink 25 | - name: Publish 26 | env: 27 | FLIT_USERNAME: ${{ secrets.FLIT_USERNAME }} 28 | FLIT_PASSWORD: ${{ secrets.FLIT_PASSWORD }} 29 | run: bash scripts/publish.sh 30 | - name: Dump GitHub context 31 | env: 32 | GITHUB_CONTEXT: ${{ toJson(github) }} 33 | run: echo "$GITHUB_CONTEXT" 34 | -------------------------------------------------------------------------------- /docs_src/pydantic_v1/fastapi-usage/tr.py: -------------------------------------------------------------------------------- 1 | from fastapi import Request 2 | from fastapi.exceptions import RequestValidationError 3 | from starlette.responses import JSONResponse 4 | from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY 5 | 6 | from pydantic_i18n import PydanticI18n 7 | 8 | __all__ = ["get_locale", "validation_exception_handler"] 9 | 10 | 11 | DEFAULT_LOCALE = "en_US" 12 | 13 | translations = { 14 | "en_US": { 15 | "field required": "field required", 16 | }, 17 | "de_DE": { 18 | "field required": "Feld erforderlich", 19 | }, 20 | } 21 | 22 | tr = PydanticI18n(translations) 23 | 24 | 25 | def get_locale(locale: str = DEFAULT_LOCALE) -> str: 26 | return locale 27 | 28 | 29 | async def validation_exception_handler( 30 | request: Request, exc: RequestValidationError 31 | ) -> JSONResponse: 32 | current_locale = request.query_params.get("locale", DEFAULT_LOCALE) 33 | return JSONResponse( 34 | status_code=HTTP_422_UNPROCESSABLE_ENTITY, 35 | content={"detail": tr.translate(exc.errors(), current_locale)}, 36 | ) 37 | -------------------------------------------------------------------------------- /docs_src/pydantic_v2/fastapi-usage/tr.py: -------------------------------------------------------------------------------- 1 | from fastapi import Request 2 | from fastapi.exceptions import RequestValidationError 3 | from starlette.responses import JSONResponse 4 | from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY 5 | 6 | from pydantic_i18n import PydanticI18n 7 | 8 | __all__ = ["get_locale", "validation_exception_handler"] 9 | 10 | 11 | DEFAULT_LOCALE = "en_US" 12 | 13 | translations = { 14 | "en_US": { 15 | "Field required": "field required", 16 | }, 17 | "de_DE": { 18 | "Field required": "Feld erforderlich", 19 | }, 20 | } 21 | 22 | tr = PydanticI18n(translations) 23 | 24 | 25 | def get_locale(locale: str = DEFAULT_LOCALE) -> str: 26 | return locale 27 | 28 | 29 | async def validation_exception_handler( 30 | request: Request, exc: RequestValidationError 31 | ) -> JSONResponse: 32 | current_locale = request.query_params.get("locale", DEFAULT_LOCALE) 33 | return JSONResponse( 34 | status_code=HTTP_422_UNPROCESSABLE_ENTITY, 35 | content={"detail": tr.translate(exc.errors(), current_locale)}, 36 | ) 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021, Roman Sadzhenytsia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /docs_src/pydantic_v2/placeholder/tutorial001.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | 3 | from pydantic import BaseModel, ValidationError, Field 4 | from pydantic_i18n import PydanticI18n 5 | 6 | 7 | translations = { 8 | "en_US": { 9 | "Decimal input should have no more than {} in total": 10 | "Decimal input should have no more than {} in total", 11 | }, 12 | "es_AR": { 13 | "Decimal input should have no more than {} in total": 14 | "La entrada decimal no debe tener más de {} en total", 15 | }, 16 | } 17 | 18 | tr = PydanticI18n(translations) 19 | 20 | 21 | class CoolSchema(BaseModel): 22 | my_field: Decimal = Field(max_digits=3) 23 | 24 | 25 | try: 26 | CoolSchema(my_field=1111) 27 | except ValidationError as e: 28 | translated_errors = tr.translate(e.errors(), locale="es_AR") 29 | 30 | print(translated_errors) 31 | # [ 32 | # { 33 | # 'type': 'decimal_max_digits', 34 | # 'loc': ('my_field',), 35 | # 'msg': 'La entrada decimal no debe tener más de 3 digits en total', 36 | # 'input': 1111, 37 | # 'ctx': { 38 | # 'max_digits': 3 39 | # }, 40 | # 'url': 'https://errors.pydantic.dev/2.6/v/decimal_max_digits' 41 | # } 42 | # ] 43 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | types: [opened, synchronize] 9 | 10 | jobs: 11 | test: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | python-version: [3.8, 3.9, '3.10', 3.11, 3.12] 16 | fail-fast: false 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | 26 | - id: cache 27 | uses: actions/cache@v4 28 | with: 29 | path: ${{ env.pythonLocation }} 30 | key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-test 31 | 32 | - name: Install flit 33 | if: steps.cache.outputs.cache-hit != 'true' 34 | run: pip install flit 35 | 36 | - name: Install dependencies 37 | if: steps.cache.outputs.cache-hit != 'true' 38 | run: flit install --symlink --extras test 39 | 40 | - name: Lint 41 | run: bash ./scripts/lint.sh 42 | 43 | - name: Test 44 | run: bash ./scripts/test.sh 45 | 46 | - name: Upload coverage 47 | uses: codecov/codecov-action@v5.4.0 48 | -------------------------------------------------------------------------------- /docs_src/pydantic_v1/own-loader/tutorial001.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import List, Dict 3 | 4 | from pydantic import BaseModel, ValidationError 5 | from pydantic_i18n import PydanticI18n, BaseLoader 6 | 7 | 8 | class CsvLoader(BaseLoader): 9 | def __init__(self, directory: str): 10 | self.directory = directory 11 | 12 | @property 13 | def locales(self) -> List[str]: 14 | return [ 15 | filename[:-4] 16 | for filename in os.listdir(self.directory) 17 | if filename.endswith(".csv") 18 | ] 19 | 20 | def get_translations(self, locale: str) -> Dict[str, str]: 21 | with open(os.path.join(self.directory, f"{locale}.csv")) as fp: 22 | data = dict(line.strip().split(",") for line in fp) 23 | 24 | return data 25 | 26 | 27 | class User(BaseModel): 28 | name: str 29 | 30 | 31 | if __name__ == '__main__': 32 | loader = CsvLoader("./translations") 33 | tr = PydanticI18n(loader) 34 | 35 | try: 36 | User() 37 | except ValidationError as e: 38 | translated_errors = tr.translate(e.errors(), locale="de") 39 | 40 | print(translated_errors) 41 | # [ 42 | # { 43 | # 'loc': ('name',), 44 | # 'msg': 'Feld erforderlich', 45 | # 'type': 'value_error.missing' 46 | # } 47 | # ] 48 | -------------------------------------------------------------------------------- /.github/workflows/preview-docs.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Preview Docs 3 | on: 4 | workflow_run: 5 | workflows: 6 | - Build Docs 7 | types: 8 | - completed 9 | 10 | jobs: 11 | deploy: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Download Artifact Docs 17 | uses: dawidd6/action-download-artifact@v9 18 | with: 19 | github_token: ${{ secrets.GITHUB_TOKEN }} 20 | workflow: build-docs.yml 21 | run_id: ${{ github.event.workflow_run.id }} 22 | name: docs-zip 23 | 24 | - name: Unzip docs 25 | run: | 26 | rm -rf ./site 27 | unzip docs.zip 28 | rm -f docs.zip 29 | 30 | - name: Deploy to Netlify 31 | id: netlify 32 | uses: nwtgck/actions-netlify@v3.0.0 33 | with: 34 | publish-dir: './site' 35 | production-deploy: false 36 | github-token: ${{ secrets.GITHUB_TOKEN }} 37 | enable-commit-comment: false 38 | env: 39 | NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} 40 | NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} 41 | 42 | - name: Comment Deploy 43 | uses: ./.github/actions/comment-docs-preview-in-pr 44 | with: 45 | token: ${{ secrets.GITHUB_TOKEN }} 46 | deploy_url: "${{ steps.netlify.outputs.deploy-url }}" 47 | 48 | -------------------------------------------------------------------------------- /tests/test_loaders/conftest.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | 5 | from pydantic_i18n import DictLoader, JsonLoader 6 | 7 | 8 | @pytest.fixture 9 | def json_translations_directory(tmp_path) -> str: 10 | package_name = "translations" 11 | default_translation_filename = "en_US.json" 12 | de_translation_filename = "de_DE.json" 13 | 14 | package = tmp_path / package_name 15 | package.mkdir() 16 | 17 | fp = package / default_translation_filename 18 | fp.touch() 19 | fp.write_text( 20 | json.dumps( 21 | { 22 | "field required": "field required", 23 | } 24 | ) 25 | ) 26 | 27 | fp = package / de_translation_filename 28 | fp.touch() 29 | fp.write_text( 30 | json.dumps( 31 | { 32 | "field required": "Feld erforderlich", 33 | } 34 | ) 35 | ) 36 | 37 | yield str(package) 38 | 39 | 40 | @pytest.fixture 41 | def json_loader(json_translations_directory: str) -> JsonLoader: 42 | return JsonLoader(json_translations_directory) 43 | 44 | 45 | @pytest.fixture 46 | def dict_loader() -> DictLoader: 47 | translations = { 48 | "en_US": { 49 | "field required": "field required", 50 | }, 51 | "de_DE": { 52 | "field required": "Feld erforderlich", 53 | }, 54 | } 55 | 56 | return DictLoader(translations) 57 | -------------------------------------------------------------------------------- /docs_src/pydantic_v2/own-loader/tutorial001.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import List, Dict 3 | 4 | from pydantic import BaseModel, ValidationError 5 | from pydantic_i18n import PydanticI18n, BaseLoader 6 | 7 | 8 | class CsvLoader(BaseLoader): 9 | def __init__(self, directory: str): 10 | self.directory = directory 11 | 12 | @property 13 | def locales(self) -> List[str]: 14 | return [ 15 | filename[:-4] 16 | for filename in os.listdir(self.directory) 17 | if filename.endswith(".csv") 18 | ] 19 | 20 | def get_translations(self, locale: str) -> Dict[str, str]: 21 | with open(os.path.join(self.directory, f"{locale}.csv")) as fp: 22 | data = dict(line.strip().split(",") for line in fp) 23 | 24 | return data 25 | 26 | 27 | class User(BaseModel): 28 | name: str 29 | 30 | 31 | if __name__ == '__main__': 32 | loader = CsvLoader("./translations") 33 | tr = PydanticI18n(loader) 34 | 35 | try: 36 | User() 37 | except ValidationError as e: 38 | translated_errors = tr.translate(e.errors(), locale="de_DE") 39 | 40 | print(translated_errors) 41 | # [ 42 | # { 43 | # 'type': 'missing', 44 | # 'loc': ('name',), 45 | # 'msg': 'Feld erforderlich', 46 | # 'input': { 47 | # 48 | # }, 49 | # 'url': 'https://errors.pydantic.dev/2.6/v/missing' 50 | # } 51 | # ] 52 | -------------------------------------------------------------------------------- /docs_src/pydantic_v1/placeholder/tutorial001.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from pydantic import BaseModel, ValidationError 4 | from pydantic_i18n import PydanticI18n 5 | 6 | 7 | translations = { 8 | "en_US": { 9 | "value is not a valid enumeration member; permitted: {}": 10 | "value is not a valid enumeration member; permitted: {}", 11 | }, 12 | "es_AR": { 13 | "value is not a valid enumeration member; permitted: {}": 14 | "el valor no es uno de los valores permitidos, que son: {}", 15 | }, 16 | } 17 | 18 | tr = PydanticI18n(translations) 19 | 20 | 21 | class ACoolEnum(Enum): 22 | NINE_TO_TWELVE = "9_to_12" 23 | TWELVE_TO_FIFTEEN = "12_to_15" 24 | FOURTEEN_TO_EIGHTEEN = "14_to_18" 25 | 26 | 27 | class CoolSchema(BaseModel): 28 | enum_field: ACoolEnum 29 | 30 | 31 | try: 32 | CoolSchema(enum_field="invalid value") 33 | except ValidationError as e: 34 | translated_errors = tr.translate(e.errors(), locale="es_AR") 35 | 36 | print(translated_errors) 37 | # [ 38 | # { 39 | # 'ctx': { 40 | # 'enum_values': [ 41 | # , 42 | # , 43 | # 44 | # ] 45 | # }, 46 | # 'loc': ('enum_field',), 47 | # 'msg': "el valor no es uno de los valores permitidos, que son: '9_to_12', '12_to_15', '14_to_18'", 48 | # 'type': 'type_error.enum' 49 | # } 50 | # ] 51 | -------------------------------------------------------------------------------- /.github/workflows/build-docs.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Build Docs 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | types: [opened, synchronize] 9 | jobs: 10 | build: 11 | runs-on: ubuntu-20.04 12 | steps: 13 | - name: Dump GitHub context 14 | env: 15 | GITHUB_CONTEXT: ${{ toJson(github) }} 16 | run: echo "$GITHUB_CONTEXT" 17 | - uses: actions/checkout@v4 18 | 19 | - name: Set up Python 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: "3.8" 23 | - uses: actions/cache@v4 24 | id: cache 25 | with: 26 | path: ${{ env.pythonLocation }} 27 | key: ${{ runner.os }}-python-${{ env.pythonLocation }}-${{ hashFiles('pyproject.toml') }}-docs 28 | 29 | - name: Install Flit 30 | if: steps.cache.outputs.cache-hit != 'true' 31 | run: python3.8 -m pip install flit 32 | 33 | - name: Install docs extras 34 | if: steps.cache.outputs.cache-hit != 'true' 35 | run: python3.8 -m flit install --extras doc 36 | 37 | - name: Build Docs 38 | run: python3.8 ./scripts/docs.py build-all 39 | 40 | - name: Zip docs 41 | run: bash ./scripts/zip-docs.sh 42 | - uses: actions/upload-artifact@v4 43 | with: 44 | name: docs-zip 45 | path: ./docs.zip 46 | 47 | - name: Deploy to Netlify 48 | uses: nwtgck/actions-netlify@v3.0.0 49 | with: 50 | publish-dir: './site' 51 | production-branch: master 52 | github-token: ${{ secrets.GITHUB_TOKEN }} 53 | enable-commit-comment: false 54 | env: 55 | NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} 56 | NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} 57 | 58 | -------------------------------------------------------------------------------- /tests/test_pydantic_messages.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | from typing import Dict 4 | 5 | import pytest 6 | from _pytest.fixtures import FixtureRequest 7 | 8 | from pydantic_i18n import PydanticI18n 9 | 10 | 11 | @pytest.fixture 12 | def json_output() -> Dict[str, str]: 13 | output = PydanticI18n.get_pydantic_messages(output="json") 14 | assert isinstance(output, str) 15 | 16 | return json.loads(output) 17 | 18 | 19 | @pytest.fixture 20 | def dict_output() -> Dict[str, str]: 21 | output = PydanticI18n.get_pydantic_messages(output="dict") 22 | assert isinstance(output, dict) 23 | 24 | return output 25 | 26 | 27 | @pytest.fixture 28 | def babel_output() -> Dict[str, str]: 29 | output = PydanticI18n.get_pydantic_messages(output="babel") 30 | assert isinstance(output, str) 31 | 32 | return dict( 33 | zip( 34 | re.findall('msgid "(.+)"\n', output), 35 | re.findall('msgstr "(.+)"\n', output), 36 | ) 37 | ) 38 | 39 | 40 | @pytest.mark.parametrize( 41 | "output_fixture_name", 42 | [ 43 | "json_output", 44 | "dict_output", 45 | "babel_output", 46 | ], 47 | ) 48 | def test_messages(request: FixtureRequest, output_fixture_name: str) -> None: 49 | output = request.getfixturevalue(output_fixture_name) 50 | 51 | for k, v in output.items(): 52 | assert isinstance(k, str) 53 | assert k == v 54 | 55 | 56 | def test_dict_by_default(): 57 | output = PydanticI18n.get_pydantic_messages() 58 | assert isinstance(output, dict) 59 | 60 | 61 | @pytest.mark.parametrize( 62 | "output_fixture_name", 63 | [ 64 | "json_output", 65 | "dict_output", 66 | "babel_output", 67 | ], 68 | ) 69 | def test_placeholders_dict(request: FixtureRequest, output_fixture_name: str) -> None: 70 | output = request.getfixturevalue(output_fixture_name) 71 | 72 | for k in output: 73 | if "{" in k: 74 | assert "{}" in k 75 | -------------------------------------------------------------------------------- /docs/en/mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: pydantic-i18n 2 | site_description: pydantic-i18n is an extension to support an i18n for the pydantic error messages. 3 | site_url: https://pydantic-i18n.boardpack.org 4 | theme: 5 | name: material 6 | custom_dir: overrides 7 | palette: 8 | - scheme: default 9 | primary: deep purple 10 | accent: amber 11 | toggle: 12 | icon: material/lightbulb-outline 13 | name: Switch to dark mode 14 | - scheme: slate 15 | primary: deep purple 16 | accent: amber 17 | toggle: 18 | icon: material/lightbulb 19 | name: Switch to light mode 20 | features: 21 | - search.suggest 22 | - search.highlight 23 | - navigation.tabs 24 | - content.code.copy 25 | icon: 26 | repo: fontawesome/brands/github-alt 27 | logo: img/icon-white.png 28 | favicon: img/favicon.png 29 | language: en 30 | repo_name: boardpack/pydantic-i18n 31 | repo_url: https://github.com/boardpack/pydantic-i18n 32 | edit_uri: '' 33 | copyright: Copyright © 2021 34 | plugins: 35 | - search 36 | - markdownextradata: 37 | data: data 38 | nav: 39 | - pydantic-i18n: index.md 40 | - contributing.md 41 | - release-notes.md 42 | markdown_extensions: 43 | - toc: 44 | permalink: true 45 | - markdown.extensions.codehilite: 46 | guess_lang: false 47 | - markdown_include.include: 48 | base_path: docs 49 | - admonition 50 | - codehilite 51 | - extra 52 | - meta 53 | - pymdownx.superfences: 54 | custom_fences: 55 | - name: mermaid 56 | class: mermaid 57 | format: !!python/name:pymdownx.superfences.fence_div_format '' 58 | - pymdownx.tabbed 59 | extra: 60 | social: 61 | - icon: fontawesome/brands/github-alt 62 | link: https://github.com/boardpack/pydantic-i18n 63 | - icon: fontawesome/brands/telegram 64 | link: https://t.me/dukkee 65 | - icon: fontawesome/solid/globe 66 | link: https://boardpack.org 67 | alternate: 68 | - link: / 69 | name: English 70 | extra_css: 71 | - css/termynal.css 72 | - css/custom.css 73 | extra_javascript: 74 | - https://unpkg.com/mermaid@8.4.6/dist/mermaid.min.js 75 | - js/termynal.js 76 | - js/custom.js 77 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit"] 3 | build-backend = "flit.buildapi" 4 | 5 | [project] 6 | name = "pydantic-i18n" 7 | authors = [ 8 | {name = "Roman Sadzhenytsia", email = "urchin.dukkee@gmail.com"}, 9 | ] 10 | classifiers = [ 11 | "Intended Audience :: Information Technology", 12 | "Intended Audience :: System Administrators", 13 | "Operating System :: OS Independent", 14 | "Programming Language :: Python :: 3", 15 | "Programming Language :: Python", 16 | "Topic :: Internet", 17 | "Topic :: Software Development :: Libraries :: Python Modules", 18 | "Topic :: Software Development :: Libraries", 19 | "Topic :: Software Development", 20 | "Typing :: Typed", 21 | "Environment :: Web Environment", 22 | "Intended Audience :: Developers", 23 | "License :: OSI Approved :: MIT License", 24 | "Programming Language :: Python :: 3 :: Only", 25 | "Programming Language :: Python :: 3.8", 26 | "Programming Language :: Python :: 3.9", 27 | "Programming Language :: Python :: 3.10", 28 | "Programming Language :: Python :: 3.11", 29 | "Programming Language :: Python :: 3.12", 30 | ] 31 | readme = "README.md" 32 | requires-python = ">=3.8.1" 33 | dependencies = [ 34 | "pydantic >=1.9.0", 35 | ] 36 | dynamic = ["version", "description"] 37 | 38 | [tool.flit.module] 39 | name = "pydantic_i18n" 40 | 41 | [project.urls] 42 | Documentation = "https://pydantic-i18n.boardpack.org" 43 | Source = "https://github.com/boardpack/pydantic-i18n" 44 | 45 | [project.optional-dependencies] 46 | test = [ 47 | "pydantic >=2", 48 | "pytest ==8.3.4", 49 | "pytest-cov ==5.0.0; python_version<'3.9'", 50 | "pytest-cov ==6.0.0; python_version>='3.9'", 51 | "mypy ==1.13.0", 52 | "flake8 ==7.1.1", 53 | "black ==24.8.0; python_version<'3.9'", 54 | "black ==25.1.0; python_version>='3.9'", 55 | "isort ==5.13.2", 56 | "babel ==2.17.0", 57 | ] 58 | dev = [ 59 | "autoflake ==2.3.1", 60 | "flake8 ==7.1.1", 61 | "pre-commit", 62 | ] 63 | doc = [ 64 | "mkdocs ==1.5.3", 65 | "mkdocs-material ==9.5.18", 66 | "markdown ==3.7", 67 | "markdown-include ==0.8.1", 68 | "mkdocs-markdownextradata-plugin ==0.2.6", 69 | "typer ==0.15.1", 70 | "pyyaml ==6.0.2", 71 | ] 72 | 73 | [tool.isort] 74 | profile = "black" 75 | known_first_party = ["pydantic_i18n", "pydantic", "babel"] 76 | -------------------------------------------------------------------------------- /tests/test_loaders/test_common.py: -------------------------------------------------------------------------------- 1 | from typing import Callable 2 | 3 | import pytest 4 | from _pytest.fixtures import FixtureRequest 5 | 6 | from pydantic_i18n import BabelLoader, JsonLoader 7 | 8 | 9 | @pytest.mark.parametrize( 10 | "loader_fixture_name", 11 | [ 12 | "json_loader", 13 | "dict_loader", 14 | "babel_loader", 15 | ], 16 | ) 17 | def test_get_locales(request: FixtureRequest, loader_fixture_name: str): 18 | loader = request.getfixturevalue(loader_fixture_name) 19 | 20 | locales = loader.locales 21 | assert len(locales) >= 2 22 | for locale in ("en_US", "de_DE"): 23 | assert locale in locales 24 | 25 | 26 | @pytest.mark.parametrize( 27 | "loader_fixture_name", 28 | [ 29 | "json_loader", 30 | "dict_loader", 31 | "babel_loader", 32 | ], 33 | ) 34 | def test_translation(request: FixtureRequest, loader_fixture_name: str): 35 | loader = request.getfixturevalue(loader_fixture_name) 36 | 37 | assert loader.gettext("field required", "en_US") == "field required" 38 | assert loader.gettext("field required", "de_DE") == "Feld erforderlich" 39 | 40 | 41 | @pytest.mark.parametrize( 42 | "loader_fixture_name", 43 | [ 44 | "json_loader", 45 | "dict_loader", 46 | "babel_loader", 47 | ], 48 | ) 49 | def test_unsupported_locale(request: FixtureRequest, loader_fixture_name: str): 50 | loader = request.getfixturevalue(loader_fixture_name) 51 | 52 | locale = "fr_FR" 53 | 54 | with pytest.raises(ValueError) as e: 55 | loader.gettext("field required", locale) 56 | 57 | assert str(e.value) == f"Locale '{locale}' wasn't found." 58 | 59 | 60 | @pytest.mark.parametrize( 61 | "loader_fixture_name", 62 | [ 63 | "json_loader", 64 | "dict_loader", 65 | "babel_loader", 66 | ], 67 | ) 68 | def test_unknown_key(request: FixtureRequest, loader_fixture_name: str): 69 | loader = request.getfixturevalue(loader_fixture_name) 70 | 71 | assert loader.gettext("unknown key", "en_US") == "unknown key" 72 | 73 | 74 | @pytest.mark.parametrize( 75 | "loader_class", 76 | [ 77 | JsonLoader, 78 | BabelLoader, 79 | ], 80 | ) 81 | def test_unexisted_dir(loader_class: Callable[[str], None]): 82 | unexisted_directory = "/unexisted_dir" 83 | 84 | with pytest.raises(OSError): 85 | loader_class(unexisted_directory) 86 | -------------------------------------------------------------------------------- /.github/actions/comment-docs-preview-in-pr/app/main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | from pathlib import Path 4 | from typing import Optional 5 | 6 | import httpx 7 | from github import Github 8 | from github.PullRequest import PullRequest 9 | from pydantic import BaseModel, BaseSettings, SecretStr, ValidationError 10 | 11 | github_api = "https://api.github.com" 12 | 13 | 14 | class Settings(BaseSettings): 15 | github_repository: str 16 | github_event_path: Path 17 | github_event_name: Optional[str] = None 18 | input_token: SecretStr 19 | input_deploy_url: str 20 | 21 | 22 | class PartialGithubEventHeadCommit(BaseModel): 23 | id: str 24 | 25 | 26 | class PartialGithubEventWorkflowRun(BaseModel): 27 | head_commit: PartialGithubEventHeadCommit 28 | 29 | 30 | class PartialGithubEvent(BaseModel): 31 | workflow_run: PartialGithubEventWorkflowRun 32 | 33 | 34 | if __name__ == "__main__": 35 | logging.basicConfig(level=logging.INFO) 36 | settings = Settings() 37 | logging.info(f"Using config: {settings.json()}") 38 | g = Github(settings.input_token.get_secret_value()) 39 | repo = g.get_repo(settings.github_repository) 40 | try: 41 | event = PartialGithubEvent.parse_file(settings.github_event_path) 42 | except ValidationError as e: 43 | logging.error(f"Error parsing event file: {e.errors()}") 44 | sys.exit(0) 45 | use_pr: Optional[PullRequest] = None 46 | for pr in repo.get_pulls(): 47 | if pr.head.sha == event.workflow_run.head_commit.id: 48 | use_pr = pr 49 | break 50 | if not use_pr: 51 | logging.error( 52 | f"No PR found for hash: {event.workflow_run.head_commit.id}" 53 | ) 54 | sys.exit(0) 55 | github_headers = { 56 | "Authorization": f"token {settings.input_token.get_secret_value()}" 57 | } 58 | url = f"{github_api}/repos/{settings.github_repository}/issues/{use_pr.number}/comments" 59 | logging.info(f"Using comments URL: {url}") 60 | response = httpx.post( 61 | url, 62 | headers=github_headers, 63 | json={ 64 | "body": f"📝 Docs preview for commit {use_pr.head.sha} at: {settings.input_deploy_url}" 65 | }, 66 | ) 67 | if not (200 <= response.status_code <= 300): 68 | logging.error(f"Error posting comment: {response.text}") 69 | sys.exit(1) 70 | logging.info("Finished") 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | # *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # IDE settings 132 | .vscode/ 133 | .idea/ 134 | -------------------------------------------------------------------------------- /docs/en/docs/css/termynal.css: -------------------------------------------------------------------------------- 1 | /** 2 | * termynal.js 3 | * 4 | * @author Ines Montani 5 | * @version 0.0.1 6 | * @license MIT 7 | */ 8 | 9 | :root { 10 | --color-bg: #252a33; 11 | --color-text: #eee; 12 | --color-text-subtle: #a2a2a2; 13 | } 14 | 15 | [data-termynal] { 16 | width: 750px; 17 | max-width: 100%; 18 | background: var(--color-bg); 19 | color: var(--color-text); 20 | font-size: 18px; 21 | /* font-family: 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; */ 22 | font-family: 'Roboto Mono', 'Fira Mono', Consolas, Menlo, Monaco, 'Courier New', Courier, monospace; 23 | border-radius: 4px; 24 | padding: 75px 45px 35px; 25 | position: relative; 26 | -webkit-box-sizing: border-box; 27 | box-sizing: border-box; 28 | } 29 | 30 | [data-termynal]:before { 31 | content: ''; 32 | position: absolute; 33 | top: 15px; 34 | left: 15px; 35 | display: inline-block; 36 | width: 15px; 37 | height: 15px; 38 | border-radius: 50%; 39 | /* A little hack to display the window buttons in one pseudo element. */ 40 | background: #d9515d; 41 | -webkit-box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930; 42 | box-shadow: 25px 0 0 #f4c025, 50px 0 0 #3ec930; 43 | } 44 | 45 | [data-termynal]:after { 46 | content: 'bash'; 47 | position: absolute; 48 | color: var(--color-text-subtle); 49 | top: 5px; 50 | left: 0; 51 | width: 100%; 52 | text-align: center; 53 | } 54 | 55 | a[data-terminal-control] { 56 | text-align: right; 57 | display: block; 58 | color: #aebbff; 59 | } 60 | 61 | [data-ty] { 62 | display: block; 63 | line-height: 2; 64 | } 65 | 66 | [data-ty]:before { 67 | /* Set up defaults and ensure empty lines are displayed. */ 68 | content: ''; 69 | display: inline-block; 70 | vertical-align: middle; 71 | } 72 | 73 | [data-ty="input"]:before, 74 | [data-ty-prompt]:before { 75 | margin-right: 0.75em; 76 | color: var(--color-text-subtle); 77 | } 78 | 79 | [data-ty="input"]:before { 80 | content: '$'; 81 | } 82 | 83 | [data-ty][data-ty-prompt]:before { 84 | content: attr(data-ty-prompt); 85 | } 86 | 87 | [data-ty-cursor]:after { 88 | content: attr(data-ty-cursor); 89 | font-family: monospace; 90 | margin-left: 0.5em; 91 | -webkit-animation: blink 1s infinite; 92 | animation: blink 1s infinite; 93 | } 94 | 95 | 96 | /* Cursor animation */ 97 | 98 | @-webkit-keyframes blink { 99 | 50% { 100 | opacity: 0; 101 | } 102 | } 103 | 104 | @keyframes blink { 105 | 50% { 106 | opacity: 0; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /docs/en/docs/help-pydantic-i18n.md: -------------------------------------------------------------------------------- 1 | # Help pydantic-i18n - Get Help 2 | 3 | Do you like **pydantic-i18n**? 4 | 5 | Would you like to help pydantic-i18n, other users, and the author? 6 | 7 | Or would you like to get help with **pydantic-i18n**? 8 | 9 | There are very simple ways to help (several involve just one or two clicks). 10 | 11 | And there are several ways to get help too. 12 | 13 | ## Star **pydantic-i18n** in GitHub 14 | 15 | You can "star" pydantic-i18n in GitHub (clicking the star button at the top right): https://github.com/boardpack/pydantic-i18n. ⭐️ 16 | 17 | By adding a star, other users will be able to find it more easily and see that it has been already useful for others. 18 | 19 | ## Watch the GitHub repository for releases 20 | 21 | You can "watch" pydantic-i18n in GitHub (clicking the "watch" button at the top right): https://github.com/boardpack/pydantic-i18n. 👀 22 | 23 | There you can select "Releases only". 24 | 25 | Doing it, you will receive notifications (in your email) whenever there's a new release (a new version) of **pydantic-i18n** with bug fixes and new features. 26 | 27 | ## Help others with issues in GitHub 28 | 29 | You can see existing issues and try and help others, most of the times they are questions that you might already know the answer for. 🤓 30 | 31 | ## Watch the GitHub repository 32 | 33 | You can "watch" pydantic-i18n in GitHub (clicking the "watch" button at the top right): https://github.com/boardpack/pydantic-i18n. 👀 34 | 35 | If you select "Watching" instead of "Releases only", you will receive notifications when someone creates a new issue. 36 | 37 | Then you can try and help them solving those issues. 38 | 39 | ## Create issues 40 | 41 | You can create a new issue in the GitHub repository, for example to: 42 | 43 | * Ask a **question** or ask about a **problem**. 44 | * Suggest a new **feature**. 45 | 46 | **Note**: if you create an issue then I'm going to ask you to also help others. 😉 47 | 48 | ## Create a Pull Request 49 | 50 | You can [contribute](contributing.md){.internal-link target=_blank} to the source code with Pull Requests, for example: 51 | 52 | * To fix a typo you found on the documentation. 53 | * To help [translate the documentation](contributing.md#translations){.internal-link target=_blank} to your language. 54 | * You can also help reviewing the translations created by others. 55 | * To propose new documentation sections. 56 | * To fix an existing issue/bug. 57 | * To add a new feature. 58 | 59 | --- 60 | 61 | Thanks! 🚀 62 | -------------------------------------------------------------------------------- /pydantic_i18n/loaders.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from typing import Dict, Mapping, Sequence 4 | 5 | __all__ = ( 6 | "BaseLoader", 7 | "BabelLoader", 8 | "DictLoader", 9 | "JsonLoader", 10 | ) 11 | 12 | 13 | class BaseLoader: 14 | @property 15 | def locales(self) -> Sequence[str]: 16 | raise NotImplementedError 17 | 18 | def get_translations(self, locale: str) -> Mapping[str, str]: 19 | raise NotImplementedError 20 | 21 | def gettext(self, key: str, locale: str) -> str: 22 | if locale not in self.locales: 23 | raise ValueError(f"Locale '{locale}' wasn't found.") 24 | 25 | data = self.get_translations(locale) 26 | return data.get(key, key) 27 | 28 | 29 | class DictLoader(BaseLoader): 30 | def __init__(self, translations: Dict[str, Dict[str, str]]): 31 | self.data = translations 32 | 33 | @property 34 | def locales(self) -> Sequence[str]: 35 | locales: Sequence[str] = tuple(self.data.keys()) 36 | return locales 37 | 38 | def get_translations(self, locale: str) -> Mapping[str, str]: 39 | return self.data[locale] 40 | 41 | 42 | class JsonLoader(BaseLoader): 43 | def __init__(self, directory: str, encoding: str = "utf-8"): 44 | if not os.path.exists(directory): 45 | raise OSError(f"Directory '{directory}' doesn't exist.") 46 | if not os.path.isdir(directory): 47 | raise OSError(f"'{directory}' is not a directory.") 48 | 49 | self.directory = directory 50 | self.encoding = encoding 51 | 52 | @property 53 | def locales(self) -> Sequence[str]: 54 | locales: Sequence[str] = [ 55 | filename[:-5] 56 | for filename in os.listdir(self.directory) 57 | if filename.endswith(".json") 58 | ] 59 | return locales 60 | 61 | def get_translations(self, locale: str) -> Mapping[str, str]: 62 | with open( 63 | os.path.join(self.directory, f"{locale}.json"), encoding=self.encoding 64 | ) as fp: 65 | data: Dict[str, str] = json.load(fp) 66 | 67 | return data 68 | 69 | 70 | class BabelLoader(BaseLoader): 71 | def __init__(self, translations_directory: str, domain: str = "messages"): 72 | try: 73 | from babel import Locale 74 | from babel.support import Translations 75 | except ImportError as e: # pragma: no cover 76 | raise ImportError( 77 | "babel not installed, you cannot use this loader.\n" 78 | "To install, run: pip install babel" 79 | ) from e 80 | 81 | self.translations = {} 82 | 83 | for dir_name in os.listdir(translations_directory): 84 | locale = Locale.parse(dir_name) 85 | self.translations[str(locale)] = Translations.load( 86 | translations_directory, 87 | [locale], 88 | domain=domain, 89 | ) 90 | 91 | @property 92 | def locales(self) -> Sequence[str]: 93 | return tuple(self.translations) 94 | 95 | def get_translations(self, locale: str) -> Dict[str, str]: 96 | return {k: v for k, v in self.translations[locale]._catalog.items() if k} # type: ignore 97 | -------------------------------------------------------------------------------- /docs/en/docs/js/custom.js: -------------------------------------------------------------------------------- 1 | 2 | function setupTermynal() { 3 | document.querySelectorAll(".use-termynal").forEach(node => { 4 | node.style.display = "block"; 5 | new Termynal(node, { 6 | lineDelay: 500 7 | }); 8 | }); 9 | const progressLiteralStart = "---> 100%"; 10 | const promptLiteralStart = "$ "; 11 | const customPromptLiteralStart = "# "; 12 | const termynalActivateClass = "termy"; 13 | let termynals = []; 14 | 15 | function createTermynals() { 16 | document 17 | .querySelectorAll(`.${termynalActivateClass} .highlight`) 18 | .forEach(node => { 19 | const text = node.textContent; 20 | const lines = text.split("\n"); 21 | const useLines = []; 22 | let buffer = []; 23 | function saveBuffer() { 24 | if (buffer.length) { 25 | let isBlankSpace = true; 26 | buffer.forEach(line => { 27 | if (line) { 28 | isBlankSpace = false; 29 | } 30 | }); 31 | dataValue = {}; 32 | if (isBlankSpace) { 33 | dataValue["delay"] = 0; 34 | } 35 | if (buffer[buffer.length - 1] === "") { 36 | // A last single
won't have effect 37 | // so put an additional one 38 | buffer.push(""); 39 | } 40 | const bufferValue = buffer.join("
"); 41 | dataValue["value"] = bufferValue; 42 | useLines.push(dataValue); 43 | buffer = []; 44 | } 45 | } 46 | for (let line of lines) { 47 | if (line === progressLiteralStart) { 48 | saveBuffer(); 49 | useLines.push({ 50 | type: "progress" 51 | }); 52 | } else if (line.startsWith(promptLiteralStart)) { 53 | saveBuffer(); 54 | const value = line.replace(promptLiteralStart, "").trimEnd(); 55 | useLines.push({ 56 | type: "input", 57 | value: value 58 | }); 59 | } else if (line.startsWith("// ")) { 60 | saveBuffer(); 61 | const value = "💬 " + line.replace("// ", "").trimEnd(); 62 | useLines.push({ 63 | value: value, 64 | class: "termynal-comment", 65 | delay: 0 66 | }); 67 | } else if (line.startsWith(customPromptLiteralStart)) { 68 | saveBuffer(); 69 | const promptStart = line.indexOf(promptLiteralStart); 70 | if (promptStart === -1) { 71 | console.error("Custom prompt found but no end delimiter", line) 72 | } 73 | const prompt = line.slice(0, promptStart).replace(customPromptLiteralStart, "") 74 | let value = line.slice(promptStart + promptLiteralStart.length); 75 | useLines.push({ 76 | type: "input", 77 | value: value, 78 | prompt: prompt 79 | }); 80 | } else { 81 | buffer.push(line); 82 | } 83 | } 84 | saveBuffer(); 85 | const div = document.createElement("div"); 86 | node.replaceWith(div); 87 | const termynal = new Termynal(div, { 88 | lineData: useLines, 89 | noInit: true, 90 | lineDelay: 500 91 | }); 92 | termynals.push(termynal); 93 | }); 94 | } 95 | 96 | function loadVisibleTermynals() { 97 | termynals = termynals.filter(termynal => { 98 | if (termynal.container.getBoundingClientRect().top - innerHeight <= 0) { 99 | termynal.init(); 100 | return false; 101 | } 102 | return true; 103 | }); 104 | } 105 | window.addEventListener("scroll", loadVisibleTermynals); 106 | createTermynals(); 107 | loadVisibleTermynals(); 108 | } 109 | 110 | setupTermynal(); 111 | -------------------------------------------------------------------------------- /pydantic_i18n/main.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | from typing import TYPE_CHECKING, Callable, Dict, List, Pattern, Sequence, Union, cast 4 | 5 | from .loaders import BaseLoader, DictLoader 6 | 7 | if TYPE_CHECKING: # pragma: no cover 8 | from pydantic.error_wrappers import ErrorDict 9 | 10 | __all__ = ("PydanticI18n",) 11 | 12 | 13 | class PydanticI18n: 14 | def __init__( 15 | self, 16 | source: Union[Dict[str, Dict[str, str]], BaseLoader], 17 | default_locale: str = "en_US", 18 | ): 19 | if isinstance(source, dict): 20 | source = DictLoader(source) 21 | 22 | self.source = source 23 | self.default_locale = default_locale 24 | self._pattern = self._init_pattern() 25 | 26 | def _init_pattern(self) -> Pattern[str]: 27 | keys = list(self.source.get_translations(self.default_locale)) 28 | return re.compile( 29 | "|".join("({})".format(re.escape(i).replace(r"\{\}", "(.+)")) for i in keys) 30 | ) 31 | 32 | def _translate(self, message: str, locale: str) -> str: 33 | placeholder_values = [] 34 | placeholder_indexes = [] 35 | searched = self._pattern.search(message) 36 | 37 | if searched: 38 | for group_index in range(len(searched.groups())): 39 | group_index += 1 40 | start = searched.start(group_index) 41 | end = searched.end(group_index) 42 | 43 | if start != end and not (start == 0 and end == len(message)): 44 | placeholder_indexes.append((start, end)) 45 | placeholder_values.append(searched.group(group_index)) 46 | 47 | key = "" 48 | prev = 0 49 | for start, end in placeholder_indexes: 50 | key += message[prev:start] + "{}" 51 | prev = end 52 | key += message[prev:] 53 | 54 | # NOTE: If we have placeholder values in the input text, we assume the 55 | # translated texts have the correct number of placeholders in the target 56 | # language. If we have too many placeholder values, Python will ignore 57 | # them. If we have too few, we have to handle the `IndexError` (and return 58 | # the un-translated text to be on the safe side). 59 | message_translated = self.source.gettext(key, locale) 60 | # NOTE: If there are no placeholder_values, we not only can but must skip 61 | # formatting in the off-chance that the message contains format characters { 62 | # or } (especially if the lookup above failed and we use the un-translated 63 | # message) 64 | if placeholder_values: 65 | message_translated = message_translated.format(*placeholder_values) 66 | return message_translated 67 | 68 | @property 69 | def locales(self) -> Sequence[str]: 70 | return self.source.locales 71 | 72 | def translate( 73 | self, 74 | errors: List["ErrorDict"], 75 | locale: str, 76 | type_search: bool = False, 77 | ) -> List["ErrorDict"]: 78 | result = [] 79 | 80 | for error in errors: 81 | message = error["msg"] 82 | error_type = error.get("type") 83 | 84 | translated_message = self._translate(message, locale) 85 | if type_search and error_type and translated_message == message: 86 | translated_message = self._translate(error_type, locale) 87 | if translated_message == error_type: 88 | translated_message = message 89 | 90 | result.append( 91 | cast( 92 | "ErrorDict", 93 | { 94 | **error, 95 | "msg": translated_message, 96 | }, 97 | ) 98 | ) 99 | 100 | return result 101 | 102 | @classmethod 103 | def get_pydantic_messages(cls, output: str = "dict") -> Union[Dict[str, str], str]: 104 | output_mapping: Dict[str, Callable[[], Union[Dict[str, str], str]]] = { 105 | "json": cls._get_pydantic_messages_json, 106 | "dict": cls._get_pydantic_messages_dict, 107 | "babel": cls._get_pydantic_messages_babel, 108 | } 109 | 110 | return output_mapping[output]() 111 | 112 | @classmethod 113 | def _get_pydantic_messages_dict(cls) -> Dict[str, str]: 114 | try: 115 | from pydantic_core._pydantic_core import list_all_errors 116 | except ImportError: # pragma: no cover 117 | from pydantic import errors 118 | 119 | list_all_errors = None # type: ignore 120 | 121 | if list_all_errors is not None: 122 | messages = [item["message_template_python"] for item in list_all_errors()] 123 | else: 124 | messages = ( 125 | getattr(errors, name).msg_template 126 | for name in errors.__all__ 127 | if hasattr(getattr(errors, name), "msg_template") 128 | ) 129 | 130 | return { 131 | value: value 132 | for value in (re.sub(r"\{.+\}", "{}", item) for item in messages) 133 | if value != "{}" 134 | } 135 | 136 | @classmethod 137 | def _get_pydantic_messages_json(cls) -> str: 138 | return json.dumps(cls._get_pydantic_messages_dict(), indent=4) 139 | 140 | @classmethod 141 | def _get_pydantic_messages_babel(cls) -> str: 142 | return "\n\n".join( 143 | f'msgid "{item}"\nmsgstr "{item}"' 144 | for item in cls._get_pydantic_messages_dict() 145 | ) 146 | -------------------------------------------------------------------------------- /tests/test_main.py: -------------------------------------------------------------------------------- 1 | from decimal import Decimal 2 | from typing import Dict, Tuple 3 | 4 | import pytest 5 | from _pytest.fixtures import FixtureRequest 6 | 7 | from pydantic import BaseModel, Field, ValidationError 8 | from pydantic.color import Color 9 | from pydantic_i18n import BaseLoader, DictLoader, PydanticI18n 10 | 11 | translations = { 12 | "en_US": { 13 | "Field required": "field required", 14 | "value is not a valid color: {}": "value is not a valid color: {}", 15 | "Decimal input should have no more than {} digits in total": "Decimal input should have no more than {} digits in total", 16 | }, 17 | "de_DE": { 18 | "Field required": "Feld erforderlich", 19 | }, 20 | "es_AR": { 21 | "value is not a valid color: {}": "no es un color válido: {}", 22 | }, 23 | } 24 | 25 | 26 | @pytest.fixture 27 | def dict_loader() -> DictLoader: 28 | return DictLoader(translations) 29 | 30 | 31 | @pytest.fixture 32 | def tr() -> PydanticI18n: 33 | return PydanticI18n(translations) 34 | 35 | 36 | def test_required_message(): 37 | class User(BaseModel): 38 | name: str 39 | 40 | with pytest.raises(ValidationError) as e: 41 | User() 42 | 43 | assert e.value.errors()[0]["msg"] == "Field required" 44 | 45 | 46 | def test_locales(tr: PydanticI18n): 47 | assert set(tr.locales) == set(translations) 48 | 49 | 50 | @pytest.mark.parametrize( 51 | "translation_data", 52 | [ 53 | translations, 54 | { 55 | "en_US": { 56 | "Field required": "Field required", 57 | }, 58 | "de_DE": { 59 | "Field required": "Feld erforderlich", 60 | }, 61 | }, 62 | { 63 | "en_US": { 64 | "missing": "Field required", 65 | }, 66 | "de_DE": { 67 | "missing": "Feld erforderlich", 68 | }, 69 | }, 70 | ], 71 | ids=[ 72 | "multiple_keys", 73 | "single_keys", 74 | "type_search", 75 | ], 76 | ) 77 | def test_required_message_translation(translation_data: Dict[str, Dict[str, str]]): 78 | tr = PydanticI18n(translation_data) 79 | 80 | class User(BaseModel): 81 | name: str 82 | 83 | with pytest.raises(ValidationError) as e: 84 | User() 85 | 86 | translated_errors = tr.translate(e.value.errors(), locale="de_DE", type_search=True) 87 | assert ( 88 | translated_errors[0]["msg"] == translations["de_DE"][e.value.errors()[0]["msg"]] 89 | ) 90 | 91 | 92 | def test_multiple_fields_errors(tr: PydanticI18n): 93 | class User(BaseModel): 94 | first_name: str 95 | last_name: str 96 | 97 | with pytest.raises(ValidationError) as e: 98 | User() 99 | 100 | errors = e.value.errors() 101 | translated_errors = tr.translate(errors, locale="de_DE") 102 | 103 | for original, translated in zip(errors, translated_errors): 104 | assert translated["msg"] == translations["de_DE"][original["msg"]] 105 | 106 | 107 | def test_unsupported_locale(tr: PydanticI18n): 108 | class User(BaseModel): 109 | name: str 110 | 111 | with pytest.raises(ValidationError) as validation_error: 112 | User() 113 | 114 | locale = "fr_FR" 115 | with pytest.raises(ValueError) as e: 116 | tr.translate(validation_error.value.errors(), locale=locale) 117 | 118 | assert str(e.value) == f"Locale '{locale}' wasn't found." 119 | 120 | 121 | def test_curly_bracket_in_message(): 122 | # This test covers the a case where the message does not map to any translation and 123 | # is thus used "as is". Thus, we don't need any inputs to translate 124 | locale = "en_US" 125 | _translations = {locale: {}} 126 | 127 | tr = PydanticI18n(_translations) 128 | 129 | test_errors = [{"msg": "test {"}] 130 | translated_errors = tr.translate(test_errors, locale=locale) 131 | 132 | assert translated_errors == test_errors 133 | 134 | 135 | def test_dict_source(): 136 | tr = PydanticI18n(translations) 137 | assert isinstance(tr.source, BaseLoader) 138 | 139 | 140 | @pytest.mark.parametrize( 141 | "loader_fixture_name", 142 | [ 143 | # "dict_loader", 144 | "babel_loader", 145 | ], 146 | ) 147 | def test_key_with_placeholder_at_the_end( 148 | request: FixtureRequest, loader_fixture_name: str 149 | ): 150 | loader = request.getfixturevalue(loader_fixture_name) 151 | 152 | class CoolSchema(BaseModel): 153 | color_field: Color 154 | 155 | tr = PydanticI18n(loader, default_locale="en_US") 156 | 157 | locale = "es_AR" 158 | with pytest.raises(ValidationError) as e: 159 | CoolSchema(color_field=(300, 300, 300, 1)) 160 | 161 | translated_errors = tr.translate(e.value.errors(), locale=locale) 162 | assert ( 163 | translated_errors[0]["msg"] 164 | == "no es un color válido: color values must be in the range 0 to 255" 165 | ) 166 | 167 | 168 | @pytest.mark.parametrize( 169 | "loader_fixture_name", 170 | [ 171 | "dict_loader", 172 | "babel_loader", 173 | ], 174 | ) 175 | def test_key_with_placeholder_in_the_middle( 176 | request: FixtureRequest, loader_fixture_name: str 177 | ): 178 | loader = request.getfixturevalue(loader_fixture_name) 179 | 180 | class T(BaseModel): 181 | decimal_field: Decimal = Field(max_digits=3) 182 | 183 | tr = PydanticI18n(loader, default_locale="en_US") 184 | 185 | locale = "en_US" 186 | with pytest.raises(ValidationError) as e: 187 | T(decimal_field=1111) 188 | 189 | translated_errors = tr.translate(e.value.errors(), locale=locale) 190 | assert ( 191 | translated_errors[0]["msg"] 192 | == "Decimal input should have no more than 3 digits in total" 193 | ) 194 | 195 | 196 | def test_last_key_without_placeholder(): 197 | _translations = { 198 | "en_US": { 199 | "field required": "field required", 200 | "value is not a valid integer": "Input should be a valid integer, unable to parse string as an integer", 201 | }, 202 | } 203 | 204 | class User(BaseModel): 205 | user_id: int 206 | 207 | message = "value is not a valid integer" 208 | data = {"user_id": "abc"} 209 | 210 | tr = PydanticI18n(_translations) 211 | 212 | with pytest.raises(ValidationError) as e: 213 | User(**data) 214 | 215 | locale = "en_US" 216 | translated_errors = tr.translate(e.value.errors(), locale=locale) 217 | 218 | assert _translations[locale][message] == translated_errors[0]["msg"] 219 | 220 | 221 | def test_multiple_placeholders(): 222 | _translations = { 223 | "en_US": { 224 | "Tuple should have at most {} items after validation, not {}": "Tuple should have at most {} items after validation, not {}", 225 | "field required": "field required", 226 | }, 227 | "de_DE": { 228 | "Tuple should have at most {} items after validation, not {}": "Tupel sollte nach der Validierung höchstens {} Elemente haben, nicht {}", 229 | "field required": "Feld erforderlich", 230 | }, 231 | } 232 | 233 | class MyModel(BaseModel): 234 | value: Tuple[str, str] 235 | 236 | tr = PydanticI18n(_translations) 237 | 238 | with pytest.raises(ValidationError) as e: 239 | MyModel(value=("1", "2", "3")) 240 | 241 | locale = "de_DE" 242 | translated_errors = tr.translate(e.value.errors(), locale=locale) 243 | 244 | assert ( 245 | translated_errors[0]["msg"] 246 | == "Tupel sollte nach der Validierung höchstens 2 Elemente haben, nicht 3" 247 | ) 248 | 249 | 250 | def test_invalid_regexp(): 251 | _translations = { 252 | "en_US": { 253 | "This contains a partial [ regexp": "This contains a partial [ regexp", 254 | }, 255 | "de_DE": { 256 | "This contains a partial [ regexp": "Hier ist eine partielle [ regexp", 257 | }, 258 | } 259 | # all we test here is that loading doesn't crash 260 | PydanticI18n(_translations) 261 | 262 | 263 | def test_valid_regexp(): 264 | _translations = { 265 | "en_US": { 266 | "This contains [a] regexp": "This contains [a] regexp", 267 | }, 268 | "de_DE": { 269 | "This contains [a] regexp": "Hier ist [eine] regexp", 270 | }, 271 | } 272 | 273 | tr = PydanticI18n(_translations) 274 | 275 | locale = "de_DE" 276 | translated_errors = tr.translate( 277 | [{"msg": "This contains [a] regexp"}], locale=locale 278 | ) 279 | assert translated_errors[0]["msg"] == "Hier ist [eine] regexp" 280 | 281 | 282 | def test_without_search_by_error_type(): 283 | _translations = { 284 | "en_US": { 285 | "missing": "Field required", 286 | }, 287 | "de_DE": { 288 | "missing": "Feld erforderlich", 289 | }, 290 | } 291 | tr = PydanticI18n(_translations) 292 | 293 | class User(BaseModel): 294 | name: str 295 | 296 | with pytest.raises(ValidationError) as e: 297 | User() 298 | 299 | translated_errors = tr.translate(e.value.errors(), locale="de_DE") 300 | assert ( 301 | translated_errors[0]["msg"] != translations["de_DE"][e.value.errors()[0]["msg"]] 302 | ) 303 | -------------------------------------------------------------------------------- /docs/en/docs/js/termynal.js: -------------------------------------------------------------------------------- 1 | /** 2 | * termynal.js 3 | * A lightweight, modern and extensible animated terminal window, using 4 | * async/await. 5 | * 6 | * @author Ines Montani 7 | * @version 0.0.1 8 | * @license MIT 9 | */ 10 | 11 | 'use strict'; 12 | 13 | /** Generate a terminal widget. */ 14 | class Termynal { 15 | /** 16 | * Construct the widget's settings. 17 | * @param {(string|Node)=} container - Query selector or container element. 18 | * @param {Object=} options - Custom settings. 19 | * @param {string} options.prefix - Prefix to use for data attributes. 20 | * @param {number} options.startDelay - Delay before animation, in ms. 21 | * @param {number} options.typeDelay - Delay between each typed character, in ms. 22 | * @param {number} options.lineDelay - Delay between each line, in ms. 23 | * @param {number} options.progressLength - Number of characters displayed as progress bar. 24 | * @param {string} options.progressChar – Character to use for progress bar, defaults to █. 25 | * @param {number} options.progressPercent - Max percent of progress. 26 | * @param {string} options.cursor – Character to use for cursor, defaults to ▋. 27 | * @param {Object[]} lineData - Dynamically loaded line data objects. 28 | * @param {boolean} options.noInit - Don't initialise the animation. 29 | */ 30 | constructor(container = '#termynal', options = {}) { 31 | this.container = (typeof container === 'string') ? document.querySelector(container) : container; 32 | this.pfx = `data-${options.prefix || 'ty'}`; 33 | this.originalStartDelay = this.startDelay = options.startDelay 34 | || parseFloat(this.container.getAttribute(`${this.pfx}-startDelay`)) || 600; 35 | this.originalTypeDelay = this.typeDelay = options.typeDelay 36 | || parseFloat(this.container.getAttribute(`${this.pfx}-typeDelay`)) || 90; 37 | this.originalLineDelay = this.lineDelay = options.lineDelay 38 | || parseFloat(this.container.getAttribute(`${this.pfx}-lineDelay`)) || 1500; 39 | this.progressLength = options.progressLength 40 | || parseFloat(this.container.getAttribute(`${this.pfx}-progressLength`)) || 40; 41 | this.progressChar = options.progressChar 42 | || this.container.getAttribute(`${this.pfx}-progressChar`) || '█'; 43 | this.progressPercent = options.progressPercent 44 | || parseFloat(this.container.getAttribute(`${this.pfx}-progressPercent`)) || 100; 45 | this.cursor = options.cursor 46 | || this.container.getAttribute(`${this.pfx}-cursor`) || '▋'; 47 | this.lineData = this.lineDataToElements(options.lineData || []); 48 | this.loadLines() 49 | if (!options.noInit) this.init() 50 | } 51 | 52 | loadLines() { 53 | // Load all the lines and create the container so that the size is fixed 54 | // Otherwise it would be changing and the user viewport would be constantly 55 | // moving as she/he scrolls 56 | const finish = this.generateFinish() 57 | finish.style.visibility = 'hidden' 58 | this.container.appendChild(finish) 59 | // Appends dynamically loaded lines to existing line elements. 60 | this.lines = [...this.container.querySelectorAll(`[${this.pfx}]`)].concat(this.lineData); 61 | for (let line of this.lines) { 62 | line.style.visibility = 'hidden' 63 | this.container.appendChild(line) 64 | } 65 | const restart = this.generateRestart() 66 | restart.style.visibility = 'hidden' 67 | this.container.appendChild(restart) 68 | this.container.setAttribute('data-termynal', ''); 69 | } 70 | 71 | /** 72 | * Initialise the widget, get lines, clear container and start animation. 73 | */ 74 | init() { 75 | /** 76 | * Calculates width and height of Termynal container. 77 | * If container is empty and lines are dynamically loaded, defaults to browser `auto` or CSS. 78 | */ 79 | const containerStyle = getComputedStyle(this.container); 80 | this.container.style.width = containerStyle.width !== '0px' ? 81 | containerStyle.width : undefined; 82 | this.container.style.minHeight = containerStyle.height !== '0px' ? 83 | containerStyle.height : undefined; 84 | 85 | this.container.setAttribute('data-termynal', ''); 86 | this.container.innerHTML = ''; 87 | for (let line of this.lines) { 88 | line.style.visibility = 'visible' 89 | } 90 | this.start(); 91 | } 92 | 93 | /** 94 | * Start the animation and rener the lines depending on their data attributes. 95 | */ 96 | async start() { 97 | this.addFinish() 98 | await this._wait(this.startDelay); 99 | 100 | for (let line of this.lines) { 101 | const type = line.getAttribute(this.pfx); 102 | const delay = line.getAttribute(`${this.pfx}-delay`) || this.lineDelay; 103 | 104 | if (type == 'input') { 105 | line.setAttribute(`${this.pfx}-cursor`, this.cursor); 106 | await this.type(line); 107 | await this._wait(delay); 108 | } 109 | 110 | else if (type == 'progress') { 111 | await this.progress(line); 112 | await this._wait(delay); 113 | } 114 | 115 | else { 116 | this.container.appendChild(line); 117 | await this._wait(delay); 118 | } 119 | 120 | line.removeAttribute(`${this.pfx}-cursor`); 121 | } 122 | this.addRestart() 123 | this.finishElement.style.visibility = 'hidden' 124 | this.lineDelay = this.originalLineDelay 125 | this.typeDelay = this.originalTypeDelay 126 | this.startDelay = this.originalStartDelay 127 | } 128 | 129 | generateRestart() { 130 | const restart = document.createElement('a') 131 | restart.onclick = (e) => { 132 | e.preventDefault() 133 | this.container.innerHTML = '' 134 | this.init() 135 | } 136 | restart.href = '#' 137 | restart.setAttribute('data-terminal-control', '') 138 | restart.innerHTML = "restart ↻" 139 | return restart 140 | } 141 | 142 | generateFinish() { 143 | const finish = document.createElement('a') 144 | finish.onclick = (e) => { 145 | e.preventDefault() 146 | this.lineDelay = 0 147 | this.typeDelay = 0 148 | this.startDelay = 0 149 | } 150 | finish.href = '#' 151 | finish.setAttribute('data-terminal-control', '') 152 | finish.innerHTML = "fast →" 153 | this.finishElement = finish 154 | return finish 155 | } 156 | 157 | addRestart() { 158 | const restart = this.generateRestart() 159 | this.container.appendChild(restart) 160 | } 161 | 162 | addFinish() { 163 | const finish = this.generateFinish() 164 | this.container.appendChild(finish) 165 | } 166 | 167 | /** 168 | * Animate a typed line. 169 | * @param {Node} line - The line element to render. 170 | */ 171 | async type(line) { 172 | const chars = [...line.textContent]; 173 | line.textContent = ''; 174 | this.container.appendChild(line); 175 | 176 | for (let char of chars) { 177 | const delay = line.getAttribute(`${this.pfx}-typeDelay`) || this.typeDelay; 178 | await this._wait(delay); 179 | line.textContent += char; 180 | } 181 | } 182 | 183 | /** 184 | * Animate a progress bar. 185 | * @param {Node} line - The line element to render. 186 | */ 187 | async progress(line) { 188 | const progressLength = line.getAttribute(`${this.pfx}-progressLength`) 189 | || this.progressLength; 190 | const progressChar = line.getAttribute(`${this.pfx}-progressChar`) 191 | || this.progressChar; 192 | const chars = progressChar.repeat(progressLength); 193 | const progressPercent = line.getAttribute(`${this.pfx}-progressPercent`) 194 | || this.progressPercent; 195 | line.textContent = ''; 196 | this.container.appendChild(line); 197 | 198 | for (let i = 1; i < chars.length + 1; i++) { 199 | await this._wait(this.typeDelay); 200 | const percent = Math.round(i / chars.length * 100); 201 | line.textContent = `${chars.slice(0, i)} ${percent}%`; 202 | if (percent>progressPercent) { 203 | break; 204 | } 205 | } 206 | } 207 | 208 | /** 209 | * Helper function for animation delays, called with `await`. 210 | * @param {number} time - Timeout, in ms. 211 | */ 212 | _wait(time) { 213 | return new Promise(resolve => setTimeout(resolve, time)); 214 | } 215 | 216 | /** 217 | * Converts line data objects into line elements. 218 | * 219 | * @param {Object[]} lineData - Dynamically loaded lines. 220 | * @param {Object} line - Line data object. 221 | * @returns {Element[]} - Array of line elements. 222 | */ 223 | lineDataToElements(lineData) { 224 | return lineData.map(line => { 225 | let div = document.createElement('div'); 226 | div.innerHTML = `${line.value || ''}`; 227 | 228 | return div.firstElementChild; 229 | }); 230 | } 231 | 232 | /** 233 | * Helper function for generating attributes string. 234 | * 235 | * @param {Object} line - Line data object. 236 | * @returns {string} - String of attributes. 237 | */ 238 | _attributes(line) { 239 | let attrs = ''; 240 | for (let prop in line) { 241 | // Custom add class 242 | if (prop === 'class') { 243 | attrs += ` class=${line[prop]} ` 244 | continue 245 | } 246 | if (prop === 'type') { 247 | attrs += `${this.pfx}="${line[prop]}" ` 248 | } else if (prop !== 'value') { 249 | attrs += `${this.pfx}-${prop}="${line[prop]}" ` 250 | } 251 | } 252 | 253 | return attrs; 254 | } 255 | } 256 | 257 | /** 258 | * HTML API: If current script has container(s) specified, initialise Termynal. 259 | */ 260 | if (document.currentScript.hasAttribute('data-termynal-container')) { 261 | const containers = document.currentScript.getAttribute('data-termynal-container'); 262 | containers.split('|') 263 | .forEach(container => new Termynal(container)) 264 | } 265 | -------------------------------------------------------------------------------- /docs/en/docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | hide: 3 | - navigation 4 | --- 5 | 6 |

7 | pydantic-i18n 8 |

9 |

10 | pydantic-i18n is an extension to support an i18n for the pydantic error messages. 11 |

12 |

13 | 14 | Test 15 | 16 | 17 | Coverage 18 | 19 | 20 | Package version 21 | 22 | Code style: black 23 | Imports: isort 24 |

25 | 26 | --- 27 | 28 | **Documentation**: https://pydantic-i18n.boardpack.org 29 | 30 | **Source Code**: https://github.com/boardpack/pydantic-i18n 31 | 32 | --- 33 | 34 | ## Requirements 35 | 36 | Python 3.8+ 37 | 38 | pydantic-i18n has the next dependencies: 39 | 40 | * Pydantic 41 | * Babel 42 | 43 | 44 | ## Installation 45 | 46 |
47 | 48 | ```console 49 | $ pip install pydantic-i18n 50 | 51 | ---> 100% 52 | ``` 53 | 54 |
55 | 56 | ## First steps 57 | 58 | To start to work with pydantic-i18n, you can just create a dictionary (or 59 | create any needed translations storage and then convert it into dictionary) 60 | and pass to the main `PydanticI18n` class. 61 | 62 | To translate messages, you need to pass result of `exception.errors()` call to 63 | the `translate` method: 64 | 65 | ```Python hl_lines="14 24" 66 | {!../../../docs_src/pydantic_v2/dict-loader/tutorial001.py!} 67 | ``` 68 | _(This script is complete, it should run "as is" for Pydantic 2+, an example for Pydantic 1 is 69 | [here](https://github.com/boardpack/pydantic-i18n/blob/master/docs_src/pydantic_v1/dict-loader/tutorial001.py))_ 70 | 71 | In the next chapters, you will see current available loaders and how to 72 | implement your own loader. 73 | 74 | ## Usage with FastAPI 75 | 76 | Here is a simple example usage with FastAPI. 77 | 78 | ### Create it 79 | 80 | Let's create a `tr.py` file: 81 | 82 | ```Python linenums="1" hl_lines="13-22 25-26 32 35" 83 | {!../../../docs_src/pydantic_v2/fastapi-usage/tr.py!} 84 | ``` 85 | 86 | `13-22`: As you see, we selected the simplest variant to store translations, 87 | you can use any that you need. 88 | 89 | `25-26`: To not include `locale` query parameter into every handler, we 90 | created a simple function `get_locale`, which we will include as a global 91 | dependency with `Depends`. 92 | 93 | `29-36`: An example of overridden function to return translated messages of the 94 | validation exception. 95 | 96 | Now we are ready to create a FastAPI application: 97 | 98 | ```Python linenums="1" hl_lines="8 10" 99 | {!../../../docs_src/pydantic_v2/fastapi-usage/main.py!} 100 | ``` 101 | 102 | `8`: Add `get_locale` function as a global dependency. 103 | 104 | !!! note 105 | If you need to use i18n only for specific part of your 106 | application, you can add this `get_locale` function to the specific 107 | `APIRouter`. More information about `APIRouter` you can find 108 | [here](https://fastapi.tiangolo.com/tutorial/bigger-applications/#apirouter). 109 | 110 | `10`: Override default request validation error handler with 111 | `validation_exception_handler`. 112 | 113 | ### Run it 114 | 115 | Run the server with: 116 | 117 |
118 | 119 | ```console 120 | $ uvicorn main:app --reload 121 | 122 | INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) 123 | INFO: Started reloader process [28720] 124 | INFO: Started server process [28722] 125 | INFO: Waiting for application startup. 126 | INFO: Application startup complete. 127 | ``` 128 | 129 |
130 | 131 |
132 | About the command uvicorn main:app --reload... 133 | 134 | The command `uvicorn main:app` refers to: 135 | 136 | * `main`: the file `main.py` (the Python "module"). 137 | * `app`: the object created inside of `main.py` with the line `app = FastAPI()`. 138 | * `--reload`: make the server restart after code changes. Only do this for development. 139 | 140 |
141 | 142 | ### Send it 143 | 144 | Open your browser at http://127.0.0.1:8000/docs#/default/create_user_user_post. 145 | 146 | Send POST-request with empty body and `de_DE` locale query param via swagger UI 147 | or `curl`: 148 | 149 | ```bash 150 | $ curl -X 'POST' \ 151 | 'http://127.0.0.1:8000/user?locale=de_DE' \ 152 | -H 'accept: application/json' \ 153 | -H 'Content-Type: application/json' \ 154 | -d '{ 155 | }' 156 | ``` 157 | 158 | ### Check it 159 | 160 | As a result, you will get the next response body: 161 | 162 | ```json hl_lines="8" 163 | { 164 | "detail": [ 165 | { 166 | "loc": [ 167 | "body", 168 | "name" 169 | ], 170 | "msg": "Feld erforderlich", 171 | "type": "value_error.missing" 172 | } 173 | ] 174 | } 175 | ``` 176 | 177 | If you don't mention the `locale` param, English locale will be used by 178 | default. 179 | 180 | ## Use placeholder in error strings 181 | 182 | You can use placeholders in error strings, but you **must mark** every placeholder with `{}`. 183 | 184 | ```Python 185 | {!../../../docs_src/pydantic_v2/placeholder/tutorial001.py!} 186 | ``` 187 | _(This script is complete, it should run "as is" for Pydantic 2+, an example for Pydantic 1 is 188 | [here](https://github.com/boardpack/pydantic-i18n/blob/master/docs_src/pydantic_v1/placeholder/tutorial001.py))_ 189 | 190 | ## Get current error strings from Pydantic 191 | 192 | pydantic-i18n doesn't provide prepared translations of all current error 193 | messages from pydantic, but you can use a special class method 194 | `PydanticI18n.get_pydantic_messages` to load original messages in English. By 195 | default, it returns a `dict` object: 196 | 197 | ```Python 198 | {!../../../docs_src/pydantic_v2/pydantic-messages/tutorial001.py!} 199 | ``` 200 | _(This script is complete, it should run "as is" for Pydantic 2+, an example for Pydantic 1 is 201 | [here](https://github.com/boardpack/pydantic-i18n/blob/master/docs_src/pydantic_v1/pydantic-messages/tutorial001.py))_ 202 | 203 | You can also choose JSON string or Babel format with `output` parameter values 204 | `"json"` and `"babel"`: 205 | 206 | ```Python 207 | {!../../../docs_src/pydantic_v2/pydantic-messages/tutorial002.py!} 208 | ``` 209 | _(This script is complete, it should run "as is" for Pydantic 2+, an example for Pydantic 1 is 210 | [here](https://github.com/boardpack/pydantic-i18n/blob/master/docs_src/pydantic_v1/pydantic-messages/tutorial002.py))_ 211 | 212 | 213 | ## Loaders 214 | 215 | pydantic-i18n provides a list of loaders to use translations. 216 | 217 | ### DictLoader 218 | 219 | DictLoader is the simplest loader and default in PydanticI18n. So you can 220 | just pass your translations dictionary without any other preparation steps. 221 | 222 | ```Python 223 | {!../../../docs_src/pydantic_v2/dict-loader/tutorial001.py!} 224 | ``` 225 | _(This script is complete, it should run "as is" for Pydantic 2+, an example for Pydantic 1 is 226 | [here](https://github.com/boardpack/pydantic-i18n/blob/master/docs_src/pydantic_v1/dict-loader/tutorial001.py))_ 227 | 228 | ### JsonLoader 229 | 230 | JsonLoader needs to get the path to some directory with the next structure: 231 | 232 | ```text 233 | 234 | |-- translations 235 | |-- en_US.json 236 | |-- de_DE.json 237 | |-- ... 238 | ``` 239 | 240 | where e.g. `en_US.json` looks like: 241 | 242 | ```json 243 | {!../../../docs_src/pydantic_v2/json-loader/translations/en_US.json!} 244 | ``` 245 | 246 | and `de_DE.json`: 247 | 248 | ```json 249 | {!../../../docs_src/pydantic_v2/json-loader/translations/de_DE.json!} 250 | ``` 251 | 252 | Then we can use `JsonLoader` to load our translations: 253 | 254 | ```Python 255 | {!../../../docs_src/pydantic_v2/json-loader/tutorial001.py!} 256 | ``` 257 | _(This script is complete, it should run "as is" for Pydantic 2+, an example for Pydantic 1 is 258 | [here](https://github.com/boardpack/pydantic-i18n/blob/master/docs_src/pydantic_v1/json-loader/tutorial001.py))_ 259 | 260 | ### BabelLoader 261 | 262 | To use this loader, you need to install `babel` first: 263 | 264 | ```bash 265 | (venv) $ pip install babel 266 | ``` 267 | 268 | BabelLoader works in the similar way as JsonLoader. It also needs a 269 | translations directory with the next structure: 270 | 271 | ```text 272 | 273 | |-- translations 274 | |-- en_US 275 | |-- LC_MESSAGES 276 | |-- messages.mo 277 | |-- messages.po 278 | |-- de_DE 279 | |-- LC_MESSAGES 280 | |-- messages.mo 281 | |-- messages.po 282 | |-- ... 283 | ``` 284 | 285 | Information about translations preparation you can find on the 286 | [Babel docs pages](http://babel.pocoo.org/en/latest/cmdline.html){:target="_blank"} and e.g. 287 | from [this article](https://phrase.com/blog/posts/i18n-advantages-babel-python/#Message_Extraction){:target="_blank"}. 288 | If you need to use another domain, you can pass the `domain` argument to the loader constructor. 289 | 290 | Here is an example of the `BabelLoader` usage: 291 | 292 | ```Python 293 | {!../../../docs_src/pydantic_v2/babel-loader/tutorial001.py!} 294 | ``` 295 | _(This script is complete, it should run "as is" for Pydantic 2+, an example for Pydantic 1 is 296 | [here](https://github.com/boardpack/pydantic-i18n/blob/master/docs_src/pydantic_v1/babel-loader/tutorial001.py))_ 297 | 298 | ### Write your own loader 299 | 300 | If current loaders aren't suitable for you, it's possible to write your own 301 | loader and use it with pydantic-i18n. To do it, you need to import 302 | `BaseLoader` and implement the next items: 303 | 304 | - property `locales` to get a list of locales; 305 | - method `get_translations` to get content for the specific locale. 306 | 307 | In some cases you will also need to change implementation of the `gettext` 308 | method. 309 | 310 | Here is an example of the loader to get translations from CSV files: 311 | 312 | ```text 313 | |-- translations 314 | |-- en_US.csv 315 | |-- de_DE.csv 316 | |-- ... 317 | ``` 318 | 319 | `en_US.csv` content: 320 | 321 | ```csv 322 | 323 | {!../../../docs_src/pydantic_v2/own-loader/translations/en_US.csv!} 324 | ``` 325 | 326 | `de_DE.csv` content: 327 | 328 | ```csv 329 | {!../../../docs_src/pydantic_v2/own-loader/translations/de_DE.csv!} 330 | ``` 331 | 332 | ```Python 333 | {!../../../docs_src/pydantic_v2/own-loader/tutorial001.py!} 334 | ``` 335 | _(This script is complete, it should run "as is" for Pydantic 2+, an example for Pydantic 1 is 336 | [here](https://github.com/boardpack/pydantic-i18n/blob/master/docs_src/pydantic_v1/own-loader/tutorial001.py))_ 337 | 338 | ## Acknowledgments 339 | 340 | Thanks to [Samuel Colvin](https://github.com/samuelcolvin) and his 341 | [pydantic](https://github.com/samuelcolvin/pydantic) library. 342 | 343 | Also, thanks to [Sebastián Ramírez](https://github.com/tiangolo) and his 344 | [FastAPI](https://github.com/tiangolo/fastapi) project, some scripts and 345 | documentation structure and parts were used from there. 346 | 347 | ## License 348 | 349 | This project is licensed under the terms of the MIT license. 350 | 351 | -------------------------------------------------------------------------------- /docs/en/docs/img/logo-white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /scripts/docs.py: -------------------------------------------------------------------------------- 1 | """Lightweight version of docs.py script for pydantic-i18n package 2 | 3 | You can find original here: https://github.com/tiangolo/fastapi/blob/master/scripts/docs.py 4 | 5 | """ 6 | 7 | import os 8 | import re 9 | import shutil 10 | from http.server import HTTPServer, SimpleHTTPRequestHandler 11 | from multiprocessing import Pool 12 | from pathlib import Path 13 | from typing import Dict, List, Optional, Tuple 14 | 15 | import mkdocs.commands.build 16 | import mkdocs.commands.serve 17 | import mkdocs.config 18 | import mkdocs.utils 19 | import typer 20 | import yaml 21 | 22 | app = typer.Typer() 23 | 24 | mkdocs_name = "mkdocs.yml" 25 | 26 | missing_translation_snippet = """ 27 | {!../../../docs/missing-translation.md!} 28 | """ 29 | 30 | docs_path = Path("docs") 31 | en_docs_path = Path("docs/en") 32 | en_config_path: Path = en_docs_path / mkdocs_name 33 | 34 | 35 | def get_en_config() -> dict: 36 | return mkdocs.utils.yaml_load(en_config_path.read_text(encoding="utf-8")) 37 | 38 | 39 | def get_lang_paths(): 40 | return sorted(docs_path.iterdir()) 41 | 42 | 43 | def lang_callback(lang: Optional[str]): 44 | if lang is None: 45 | return 46 | if not lang.isalpha() or len(lang) != 2: 47 | typer.echo("Use a 2 letter language code, like: es") 48 | raise typer.Abort() 49 | lang = lang.lower() 50 | return lang 51 | 52 | 53 | def complete_existing_lang(incomplete: str): 54 | lang_path: Path 55 | for lang_path in get_lang_paths(): 56 | if lang_path.is_dir() and lang_path.name.startswith(incomplete): 57 | yield lang_path.name 58 | 59 | 60 | def get_base_lang_config(lang: str): 61 | en_config = get_en_config() 62 | new_config = en_config.copy() 63 | new_config["site_url"] = en_config["site_url"] + f"{lang}/" 64 | new_config["theme"]["logo"] = en_config["site_url"] + en_config["theme"]["logo"] 65 | new_config["theme"]["favicon"] = ( 66 | en_config["site_url"] + en_config["theme"]["favicon"] 67 | ) 68 | new_config["theme"]["language"] = lang 69 | new_config["nav"] = en_config["nav"][:2] 70 | extra_css = [] 71 | css: str 72 | for css in en_config["extra_css"]: 73 | if css.startswith("http"): 74 | extra_css.append(css) 75 | else: 76 | extra_css.append(en_config["site_url"] + css) 77 | new_config["extra_css"] = extra_css 78 | 79 | extra_js = [] 80 | js: str 81 | for js in en_config["extra_javascript"]: 82 | if js.startswith("http"): 83 | extra_js.append(js) 84 | else: 85 | extra_js.append(en_config["site_url"] + js) 86 | new_config["extra_javascript"] = extra_js 87 | return new_config 88 | 89 | 90 | @app.command() 91 | def new_lang(lang: str = typer.Argument(..., callback=lang_callback)): 92 | """ 93 | Generate a new docs translation directory for the language LANG. 94 | 95 | LANG should be a 2-letter language code, like: en, es, de, pt, etc. 96 | """ 97 | new_path: Path = Path("docs") / lang 98 | if new_path.exists(): 99 | typer.echo(f"The language was already created: {lang}") 100 | raise typer.Abort() 101 | new_path.mkdir() 102 | new_config = get_base_lang_config(lang) 103 | new_config_path: Path = Path(new_path) / mkdocs_name 104 | new_config_path.write_text( 105 | yaml.dump(new_config, sort_keys=False, width=200, allow_unicode=True), 106 | encoding="utf-8", 107 | ) 108 | new_config_docs_path: Path = new_path / "docs" 109 | new_config_docs_path.mkdir() 110 | en_index_path: Path = en_docs_path / "docs" / "index.md" 111 | new_index_path: Path = new_config_docs_path / "index.md" 112 | en_index_content = en_index_path.read_text(encoding="utf-8") 113 | new_index_content = f"{missing_translation_snippet}\n\n{en_index_content}" 114 | new_index_path.write_text(new_index_content, encoding="utf-8") 115 | typer.secho(f"Successfully initialized: {new_path}", color=typer.colors.GREEN) 116 | update_languages(lang=None) 117 | 118 | 119 | @app.command() 120 | def build_lang( 121 | lang: str = typer.Argument( 122 | ..., callback=lang_callback, autocompletion=complete_existing_lang 123 | ) 124 | ): 125 | """ 126 | Build the docs for a language, filling missing pages with translation notifications. 127 | """ 128 | lang_path: Path = Path("docs") / lang 129 | if not lang_path.is_dir(): 130 | typer.echo(f"The language translation doesn't seem to exist yet: {lang}") 131 | raise typer.Abort() 132 | typer.echo(f"Building docs for: {lang}") 133 | build_dir_path = Path("docs_build") 134 | build_dir_path.mkdir(exist_ok=True) 135 | build_lang_path = build_dir_path / lang 136 | en_lang_path = Path("docs/en") 137 | site_path = Path("site").absolute() 138 | if lang == "en": 139 | dist_path = site_path 140 | else: 141 | dist_path: Path = site_path / lang 142 | shutil.rmtree(build_lang_path, ignore_errors=True) 143 | shutil.copytree(lang_path, build_lang_path) 144 | shutil.copytree(en_docs_path / "data", build_lang_path / "data") 145 | overrides_src = en_docs_path / "overrides" 146 | overrides_dest = build_lang_path / "overrides" 147 | for path in overrides_src.iterdir(): 148 | dest_path = overrides_dest / path.name 149 | if not dest_path.exists(): 150 | shutil.copy(path, dest_path) 151 | en_config_path: Path = en_lang_path / mkdocs_name 152 | en_config: dict = mkdocs.utils.yaml_load(en_config_path.read_text(encoding="utf-8")) 153 | nav = en_config["nav"] 154 | lang_config_path: Path = lang_path / mkdocs_name 155 | lang_config: dict = mkdocs.utils.yaml_load( 156 | lang_config_path.read_text(encoding="utf-8") 157 | ) 158 | lang_nav = lang_config["nav"] 159 | # Exclude first 2 entries pydantic-i18n and Languages, for custom handling 160 | use_nav = nav[2:] 161 | lang_use_nav = lang_nav[2:] 162 | file_to_nav = get_file_to_nav_map(use_nav) 163 | sections = get_sections(use_nav) 164 | lang_file_to_nav = get_file_to_nav_map(lang_use_nav) 165 | use_lang_file_to_nav = get_file_to_nav_map(lang_use_nav) 166 | for file in file_to_nav: 167 | file_path = Path(file) 168 | lang_file_path: Path = build_lang_path / "docs" / file_path 169 | en_file_path: Path = en_lang_path / "docs" / file_path 170 | lang_file_path.parent.mkdir(parents=True, exist_ok=True) 171 | if not lang_file_path.is_file(): 172 | en_text = en_file_path.read_text(encoding="utf-8") 173 | lang_text = get_text_with_translate_missing(en_text) 174 | lang_file_path.write_text(lang_text, encoding="utf-8") 175 | file_key = file_to_nav[file] 176 | use_lang_file_to_nav[file] = file_key 177 | if file_key: 178 | composite_key = () 179 | new_key = () 180 | for key_part in file_key: 181 | composite_key += (key_part,) 182 | key_first_file = sections[composite_key] 183 | if key_first_file in lang_file_to_nav: 184 | new_key = lang_file_to_nav[key_first_file] 185 | else: 186 | new_key += (key_part,) 187 | use_lang_file_to_nav[file] = new_key 188 | key_to_section = {(): []} 189 | for file, orig_file_key in file_to_nav.items(): 190 | if file in use_lang_file_to_nav: 191 | file_key = use_lang_file_to_nav[file] 192 | else: 193 | file_key = orig_file_key 194 | section = get_key_section(key_to_section=key_to_section, key=file_key) 195 | section.append(file) 196 | new_nav = key_to_section[()] 197 | export_lang_nav = [lang_nav[0], nav[1]] + new_nav 198 | lang_config["nav"] = export_lang_nav 199 | build_lang_config_path: Path = build_lang_path / mkdocs_name 200 | build_lang_config_path.write_text( 201 | yaml.dump(lang_config, sort_keys=False, width=200, allow_unicode=True), 202 | encoding="utf-8", 203 | ) 204 | current_dir = os.getcwd() 205 | os.chdir(build_lang_path) 206 | mkdocs.commands.build.build(mkdocs.config.load_config(site_dir=str(dist_path))) 207 | os.chdir(current_dir) 208 | typer.secho(f"Successfully built docs for: {lang}", color=typer.colors.GREEN) 209 | 210 | 211 | @app.command() 212 | def build_all(): 213 | """ 214 | Build mkdocs site for en, and then build each language inside, end result is located 215 | at directory ./site/ with each language inside. 216 | """ 217 | site_path = Path("site").absolute() 218 | update_languages(lang=None) 219 | current_dir = os.getcwd() 220 | os.chdir(en_docs_path) 221 | typer.echo("Building docs for: en") 222 | mkdocs.commands.build.build(mkdocs.config.load_config(site_dir=str(site_path))) 223 | os.chdir(current_dir) 224 | langs = [] 225 | for lang in get_lang_paths(): 226 | if lang == en_docs_path or not lang.is_dir(): 227 | continue 228 | langs.append(lang.name) 229 | cpu_count = os.cpu_count() or 1 230 | with Pool(cpu_count * 2) as p: 231 | p.map(build_lang, langs) 232 | 233 | 234 | def update_single_lang(lang: str): 235 | lang_path = docs_path / lang 236 | typer.echo(f"Updating {lang_path.name}") 237 | update_config(lang_path.name) 238 | 239 | 240 | @app.command() 241 | def update_languages( 242 | lang: str = typer.Argument( 243 | None, callback=lang_callback, autocompletion=complete_existing_lang 244 | ) 245 | ): 246 | """ 247 | Update the mkdocs.yml file Languages section including all the available languages. 248 | 249 | The LANG argument is a 2-letter language code. If it's not provided, update all the 250 | mkdocs.yml files (for all the languages). 251 | """ 252 | if lang is None: 253 | for lang_path in get_lang_paths(): 254 | if lang_path.is_dir(): 255 | update_single_lang(lang_path.name) 256 | else: 257 | update_single_lang(lang) 258 | 259 | 260 | @app.command() 261 | def serve(): 262 | """ 263 | A quick server to preview a built site with translations. 264 | 265 | For development, prefer the command live (or just mkdocs serve). 266 | 267 | This is here only to preview a site with translations already built. 268 | 269 | Make sure you run the build-all command first. 270 | """ 271 | typer.echo("Warning: this is a very simple server.") 272 | typer.echo("For development, use the command live instead.") 273 | typer.echo("This is here only to preview a site with translations already built.") 274 | typer.echo("Make sure you run the build-all command first.") 275 | os.chdir("site") 276 | server_address = ("", 8008) 277 | server = HTTPServer(server_address, SimpleHTTPRequestHandler) 278 | typer.echo(f"Serving at: http://127.0.0.1:8008") 279 | server.serve_forever() 280 | 281 | 282 | @app.command() 283 | def live( 284 | lang: str = typer.Argument( 285 | None, callback=lang_callback, autocompletion=complete_existing_lang 286 | ) 287 | ): 288 | """ 289 | Serve with livereload a docs site for a specific language. 290 | 291 | This only shows the actual translated files, not the placeholders created with 292 | build-all. 293 | 294 | Takes an optional LANG argument with the name of the language to serve, by default 295 | en. 296 | """ 297 | if lang is None: 298 | lang = "en" 299 | lang_path: Path = docs_path / lang 300 | os.chdir(lang_path) 301 | mkdocs.commands.serve.serve(dev_addr="127.0.0.1:8008") 302 | 303 | 304 | def update_config(lang: str): 305 | lang_path: Path = docs_path / lang 306 | config_path = lang_path / mkdocs_name 307 | current_config: dict = mkdocs.utils.yaml_load( 308 | config_path.read_text(encoding="utf-8") 309 | ) 310 | if lang == "en": 311 | config = get_en_config() 312 | else: 313 | config = get_base_lang_config(lang) 314 | config["nav"] = current_config["nav"] 315 | config["theme"]["language"] = current_config["theme"]["language"] 316 | config["theme"]["palette"] = current_config["theme"]["palette"] 317 | 318 | original_languages: Dict[str, str] = { 319 | re.sub("[^a-z]", "", lang["link"]) or "en": lang["name"] 320 | for lang in config["extra"]["alternate"] 321 | } 322 | languages: List[Dict[str, str]] = [] 323 | 324 | alternate: List[Dict[str, str]] = config["extra"].get("alternate", []) 325 | alternate_dict = {alt["link"]: alt["name"] for alt in alternate} 326 | new_alternate: List[Dict[str, str]] = [] 327 | for lang_path in get_lang_paths(): 328 | if not lang_path.is_dir(): 329 | continue 330 | 331 | name = path_part = lang_path.name 332 | if name in original_languages: 333 | name = original_languages[name] 334 | 335 | languages.append({name: "/" if path_part == "en" else f"/{path_part}/"}) 336 | for lang_dict in languages: 337 | name = list(lang_dict.keys())[0] 338 | url = lang_dict[name] 339 | if url not in alternate_dict: 340 | new_alternate.append({"link": url, "name": name}) 341 | else: 342 | use_name = alternate_dict[url] 343 | new_alternate.append({"link": url, "name": use_name}) 344 | # config["nav"][1] = {"Languages": languages} 345 | config["extra"]["alternate"] = new_alternate 346 | config_path.write_text( 347 | yaml.dump(config, sort_keys=False, width=200, allow_unicode=True), 348 | encoding="utf-8", 349 | ) 350 | 351 | 352 | def get_key_section( 353 | *, key_to_section: Dict[Tuple[str, ...], list], key: Tuple[str, ...] 354 | ) -> list: 355 | if key in key_to_section: 356 | return key_to_section[key] 357 | super_key = key[:-1] 358 | title = key[-1] 359 | super_section = get_key_section(key_to_section=key_to_section, key=super_key) 360 | new_section = [] 361 | super_section.append({title: new_section}) 362 | key_to_section[key] = new_section 363 | return new_section 364 | 365 | 366 | def get_text_with_translate_missing(text: str) -> str: 367 | lines = text.splitlines() 368 | lines.insert(1, missing_translation_snippet) 369 | new_text = "\n".join(lines) 370 | return new_text 371 | 372 | 373 | def get_file_to_nav_map(nav: list) -> Dict[str, Tuple[str, ...]]: 374 | file_to_nav = {} 375 | for item in nav: 376 | if type(item) is str: 377 | file_to_nav[item] = tuple() 378 | elif type(item) is dict: 379 | item_key = list(item.keys())[0] 380 | sub_nav = item[item_key] 381 | sub_file_to_nav = get_file_to_nav_map(sub_nav) 382 | for k, v in sub_file_to_nav.items(): 383 | file_to_nav[k] = (item_key,) + v 384 | return file_to_nav 385 | 386 | 387 | def get_sections(nav: list) -> Dict[Tuple[str, ...], str]: 388 | sections = {} 389 | for item in nav: 390 | if type(item) is str: 391 | continue 392 | elif type(item) is dict: 393 | item_key = list(item.keys())[0] 394 | sub_nav = item[item_key] 395 | sections[(item_key,)] = sub_nav[0] 396 | sub_sections = get_sections(sub_nav) 397 | for k, v in sub_sections.items(): 398 | new_key = (item_key,) + k 399 | sections[new_key] = v 400 | return sections 401 | 402 | 403 | if __name__ == "__main__": 404 | app() 405 | -------------------------------------------------------------------------------- /docs/en/docs/contributing.md: -------------------------------------------------------------------------------- 1 | # Development - Contributing 2 | 3 | First, you might want to see the basic ways to [help pydantic-i18n and get help](help-pydantic-i18n.md){.internal-link target=_blank}. 4 | 5 | ## Developing 6 | 7 | If you already cloned the repository and you know that you need to deep dive in the code, here are some guidelines to set up your environment. 8 | 9 | ### Virtual environment with `venv` 10 | 11 | You can create a virtual environment in a directory using Python's `venv` module: 12 | 13 |
14 | 15 | ```console 16 | $ python -m venv env 17 | ``` 18 | 19 |
20 | 21 | That will create a directory `./env/` with the Python binaries and then you will be able to install packages for that isolated environment. 22 | 23 | ### Activate the environment 24 | 25 | Activate the new environment with: 26 | 27 | === "Linux, macOS" 28 | 29 |
30 | 31 | ```console 32 | $ source ./env/bin/activate 33 | ``` 34 | 35 |
36 | 37 | === "Windows PowerShell" 38 | 39 |
40 | 41 | ```console 42 | $ .\env\Scripts\Activate.ps1 43 | ``` 44 | 45 |
46 | 47 | === "Windows Bash" 48 | 49 | Or if you use Bash for Windows (e.g. Git Bash): 50 | 51 |
52 | 53 | ```console 54 | $ source ./env/Scripts/activate 55 | ``` 56 | 57 |
58 | 59 | To check it worked, use: 60 | 61 | === "Linux, macOS, Windows Bash" 62 | 63 |
64 | 65 | ```console 66 | $ which pip 67 | 68 | some/directory/pydantic-i18n/env/bin/pip 69 | ``` 70 | 71 |
72 | 73 | === "Windows PowerShell" 74 | 75 |
76 | 77 | ```console 78 | $ Get-Command pip 79 | 80 | some/directory/pydantic-i18n/env/bin/pip 81 | ``` 82 | 83 |
84 | 85 | If it shows the `pip` binary at `env/bin/pip` then it worked. 🎉 86 | 87 | 88 | 89 | !!! tip 90 | Every time you install a new package with `pip` under that environment, activate the environment again. 91 | 92 | This makes sure that if you use a terminal program installed by that package (like `flit`), you use the one from your local environment and not any other that could be installed globally. 93 | 94 | ### Flit 95 | 96 | **pydantic-i18n** uses Flit to build, package and publish the project. 97 | 98 | After activating the environment as described above, install `flit`: 99 | 100 |
101 | 102 | ```console 103 | $ pip install flit 104 | 105 | ---> 100% 106 | ``` 107 | 108 |
109 | 110 | Now re-activate the environment to make sure you are using the `flit` you just installed (and not a global one). 111 | 112 | And now use `flit` to install the development dependencies: 113 | 114 | === "Linux, macOS" 115 | 116 |
117 | 118 | ```console 119 | $ flit install --deps develop --symlink 120 | 121 | ---> 100% 122 | ``` 123 | 124 |
125 | 126 | === "Windows" 127 | 128 | If you are on Windows, use `--pth-file` instead of `--symlink`: 129 | 130 |
131 | 132 | ```console 133 | $ flit install --deps develop --pth-file 134 | 135 | ---> 100% 136 | ``` 137 | 138 |
139 | 140 | It will install all the dependencies and your local pydantic-i18n in your local environment. 141 | 142 | #### Using your local pydantic-i18n 143 | 144 | If you create a Python file that imports and uses pydantic-i18n, and run it with the Python from your local environment, it will use your local pydantic-i18n source code. 145 | 146 | And if you update that local pydantic-i18n source code, as it is installed with `--symlink` (or `--pth-file` on Windows), when you run that Python file again, it will use the fresh version of pydantic-i18n you just edited. 147 | 148 | That way, you don't have to "install" your local version to be able to test every change. 149 | 150 | ### Format 151 | 152 | There is a script that you can run that will format and clean all your code: 153 | 154 |
155 | 156 | ```console 157 | $ bash scripts/format.sh 158 | ``` 159 | 160 |
161 | 162 | It will also auto-sort all your imports. 163 | 164 | For it to sort them correctly, you need to have pydantic-i18n installed locally in your environment, with the command in the section above using `--symlink` (or `--pth-file` on Windows). 165 | 166 | ### Format imports 167 | 168 | There is another script that formats all the imports and makes sure you don't have unused imports: 169 | 170 |
171 | 172 | ```console 173 | $ bash scripts/format-imports.sh 174 | ``` 175 | 176 |
177 | 178 | As it runs one command after the other and modifies and reverts many files, it takes a bit longer to run, so it might be easier to use `scripts/format.sh` frequently and `scripts/format-imports.sh` only before committing. 179 | 180 | ## Docs 181 | 182 | First, make sure you set up your environment as described above, that will install all the requirements. 183 | 184 | The documentation uses MkDocs. 185 | 186 | And there are extra tools/scripts in place to handle translations in `./scripts/docs.py`. 187 | 188 | !!! tip 189 | You don't need to see the code in `./scripts/docs.py`, you just use it in the command line. 190 | 191 | All the documentation is in Markdown format in the directory `./docs/en/`. 192 | 193 | Many of the tutorials have blocks of code. 194 | 195 | In most of the cases, these blocks of code are actual complete applications that can be run as is. 196 | 197 | In fact, those blocks of code are not written inside the Markdown, they are Python files in the `./docs_src/` directory. 198 | 199 | And those Python files are included/injected in the documentation when generating the site. 200 | 201 | ### Docs for tests 202 | 203 | Most of the tests actually run against the example source files in the documentation. 204 | 205 | This helps making sure that: 206 | 207 | * The documentation is up to date. 208 | * The documentation examples can be run as is. 209 | * Most of the features are covered by the documentation, ensured by test coverage. 210 | 211 | During local development, there is a script that builds the site and checks for any changes, live-reloading: 212 | 213 |
214 | 215 | ```console 216 | $ python ./scripts/docs.py live 217 | 218 | [INFO] Serving on http://127.0.0.1:8008 219 | [INFO] Start watching changes 220 | [INFO] Start detecting changes 221 | ``` 222 | 223 |
224 | 225 | It will serve the documentation on `http://127.0.0.1:8008`. 226 | 227 | That way, you can edit the documentation/source files and see the changes live. 228 | 229 | #### Typer CLI (optional) 230 | 231 | The instructions here show you how to use the script at `./scripts/docs.py` with the `python` program directly. 232 | 233 | But you can also use Typer CLI, and you will get autocompletion in your terminal for the commands after installing completion. 234 | 235 | If you install Typer CLI, you can install completion with: 236 | 237 |
238 | 239 | ```console 240 | $ typer --install-completion 241 | 242 | zsh completion installed in /home/user/.bashrc. 243 | Completion will take effect once you restart the terminal. 244 | ``` 245 | 246 |
247 | 248 | ### Apps and docs at the same time 249 | 250 | If you run the examples with, e.g.: 251 | 252 |
253 | 254 | ```console 255 | $ uvicorn tutorial001:app --reload 256 | 257 | INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) 258 | ``` 259 | 260 |
261 | 262 | as Uvicorn by default will use the port `8000`, the documentation on port `8008` won't clash. 263 | 264 | ### Translations 265 | 266 | Help with translations is VERY MUCH appreciated! And it can't be done without the help from the community. 🌎 🚀 267 | 268 | Here are the steps to help with translations. 269 | 270 | #### Tips and guidelines 271 | 272 | * Check the currently existing pull requests for your language and add reviews requesting changes or approving them. 273 | 274 | !!! tip 275 | You can add comments with change suggestions to existing pull requests. 276 | 277 | Check the docs about adding a pull request review to approve it or request changes. 278 | 279 | * Check in the issues to see if there's one coordinating translations for your language. 280 | 281 | * Add a single pull request per page translated. That will make it much easier for others to review it. 282 | 283 | For the languages I don't speak, I'll wait for several others to review the translation before merging. 284 | 285 | * You can also check if there are translations for your language and add a review to them, that will help me know that the translation is correct and I can merge it. 286 | 287 | * Use the same Python examples and only translate the text in the docs. You don't have to change anything for this to work. 288 | 289 | * Use the same images, file names, and links. You don't have to change anything for it to work. 290 | 291 | * To check the 2-letter code for the language you want to translate you can use the table List of ISO 639-1 codes. 292 | 293 | #### Existing language 294 | 295 | Let's say you want to translate a page for a language that already has translations for some pages, like Russian. 296 | 297 | In the case of Russian, the 2-letter code is `ru`. So, the directory for Russian translations is located at `docs/ru/`. 298 | 299 | !!! tip 300 | The main ("official") language is English, located at `docs/en/`. 301 | 302 | Now run the live server for the docs in Russian: 303 | 304 |
305 | 306 | ```console 307 | // Use the command "live" and pass the language code as a CLI argument 308 | $ python ./scripts/docs.py live ru 309 | 310 | [INFO] Serving on http://127.0.0.1:8008 311 | [INFO] Start watching changes 312 | [INFO] Start detecting changes 313 | ``` 314 | 315 |
316 | 317 | Now you can go to http://127.0.0.1:8008 and see your changes live. 318 | 319 | If you look at the pydantic-i18n docs website, you will see that every language has all the pages. But some pages are not translated and have a notification about the missing translation. 320 | 321 | But when you run it locally like this, you will only see the pages that are already translated. 322 | 323 | Now let's say that you want to add a translation for the section [Home](index.md){.internal-link target=_blank}. 324 | 325 | * Copy the file at: 326 | 327 | ``` 328 | docs/en/docs/index.md 329 | ``` 330 | 331 | * Paste it in exactly the same location but for the language you want to translate, e.g.: 332 | 333 | ``` 334 | docs/ru/docs/index.md 335 | ``` 336 | 337 | !!! tip 338 | Notice that the only change in the path and file name is the language code, from `en` to `ru`. 339 | 340 | * Now open the MkDocs config file for English at: 341 | 342 | ``` 343 | docs/en/docs/mkdocs.yml 344 | ``` 345 | 346 | * Find the place where that `docs/index.md` is located in the config file. Somewhere like: 347 | 348 | ```YAML hl_lines="8" 349 | site_name: pydantic-i18n 350 | # More stuff 351 | nav: 352 | - index.md 353 | - Languages: 354 | - en: / 355 | - ru: /ru/ 356 | ``` 357 | 358 | * Open the MkDocs config file for the language you are editing, e.g.: 359 | 360 | ``` 361 | docs/ru/docs/mkdocs.yml 362 | ``` 363 | 364 | * Add it there at the exact same location it was for English, e.g.: 365 | 366 | ```YAML hl_lines="8" 367 | site_name: pydantic-i18n 368 | # More stuff 369 | nav: 370 | - index.md 371 | ``` 372 | 373 | Make sure that if there are other entries, the new entry with your translation is exactly in the same order as in the English version. 374 | 375 | If you go to your browser you will see that now the docs show your new section. 🎉 376 | 377 | Now you can translate it all and see how it looks as you save the file. 378 | 379 | #### New Language 380 | 381 | Let's say that you want to add translations for a language that is not yet translated, not even some pages. 382 | 383 | Let's say you want to add translations for Creole, and it's not yet there in the docs. 384 | 385 | Checking the link from above, the code for "Creole" is `ht`. 386 | 387 | The next step is to run the script to generate a new translation directory: 388 | 389 |
390 | 391 | ```console 392 | // Use the command new-lang, pass the language code as a CLI argument 393 | $ python ./scripts/docs.py new-lang ht 394 | 395 | Successfully initialized: docs/ht 396 | Updating ht 397 | Updating en 398 | ``` 399 | 400 |
401 | 402 | Now you can check in your code editor the newly created directory `docs/ht/`. 403 | 404 | !!! tip 405 | Create a first pull request with just this, to set up the configuration for the new language, before adding translations. 406 | 407 | That way others can help with other pages while you work on the first one. 🚀 408 | 409 | Start by translating the main page, `docs/ht/index.md`. 410 | 411 | Then you can continue with the previous instructions, for an "Existing Language". 412 | 413 | ##### New Language not supported 414 | 415 | If when running the live server script you get an error about the language not being supported, something like: 416 | 417 | ``` 418 | raise TemplateNotFound(template) 419 | jinja2.exceptions.TemplateNotFound: partials/language/xx.html 420 | ``` 421 | 422 | That means that the theme doesn't support that language (in this case, with a fake 2-letter code of `xx`). 423 | 424 | But don't worry, you can set the theme language to English and then translate the content of the docs. 425 | 426 | If you need to do that, edit the `mkdocs.yml` for your new language, it will have something like: 427 | 428 | ```YAML hl_lines="5" 429 | site_name: pydantic-i18n 430 | # More stuff 431 | theme: 432 | # More stuff 433 | language: xx 434 | ``` 435 | 436 | Change that language from `xx` (from your language code) to `en`. 437 | 438 | Then you can start the live server again. 439 | 440 | #### Preview the result 441 | 442 | When you use the script at `./scripts/docs.py` with the `live` command it only shows the files and translations available for the current language. 443 | 444 | But once you are done, you can test it all as it would look online. 445 | 446 | To do that, first build all the docs: 447 | 448 |
449 | 450 | ```console 451 | // Use the command "build-all", this will take a bit 452 | $ python ./scripts/docs.py build-all 453 | 454 | Updating ru 455 | Updating en 456 | Building docs for: en 457 | Building docs for: ru 458 | Successfully built docs for: ru 459 | Copying en index.md to README.md 460 | ``` 461 | 462 |
463 | 464 | That generates all the docs at `./docs_build/` for each language. This includes adding any files with missing translations, with a note saying that "this file doesn't have a translation yet". But you don't have to do anything with that directory. 465 | 466 | Then it builds all those independent MkDocs sites for each language, combines them, and generates the final output at `./site/`. 467 | 468 | Then you can serve that with the command `serve`: 469 | 470 |
471 | 472 | ```console 473 | // Use the command "serve" after running "build-all" 474 | $ python ./scripts/docs.py serve 475 | 476 | Warning: this is a very simple server. For development, use mkdocs serve instead. 477 | This is here only to preview a site with translations already built. 478 | Make sure you run the build-all command first. 479 | Serving at: http://127.0.0.1:8008 480 | ``` 481 | 482 |
483 | 484 | ## Tests 485 | 486 | There is a script that you can run locally to test all the code and generate coverage reports in HTML: 487 | 488 |
489 | 490 | ```console 491 | $ bash scripts/test-cov-html.sh 492 | ``` 493 | 494 |
495 | 496 | This command generates a directory `./htmlcov/`, if you open the file `./htmlcov/index.html` in your browser, you can explore interactively the regions of code that are covered by the tests, and notice if there is any region missing. 497 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | pydantic-i18n 3 |

4 |

5 | pydantic-i18n is an extension to support an i18n for the pydantic error messages. 6 |

7 |

8 | 9 | Test 10 | 11 | 12 | Coverage 13 | 14 | 15 | Package version 16 | 17 | Code style: black 18 | Imports: isort 19 |

20 | 21 | --- 22 | 23 | **Documentation**: https://pydantic-i18n.boardpack.org 24 | 25 | **Source Code**: https://github.com/boardpack/pydantic-i18n 26 | 27 | --- 28 | 29 | ## Requirements 30 | 31 | Python 3.8+ 32 | 33 | pydantic-i18n has the next dependencies: 34 | 35 | * Pydantic 36 | * Babel 37 | 38 | 39 | ## Installation 40 | 41 |
42 | 43 | ```console 44 | $ pip install pydantic-i18n 45 | 46 | ---> 100% 47 | ``` 48 | 49 |
50 | 51 | ## First steps 52 | 53 | To start to work with pydantic-i18n, you can just create a dictionary (or 54 | create any needed translations storage and then convert it into dictionary) 55 | and pass to the main `PydanticI18n` class. 56 | 57 | To translate messages, you need to pass result of `exception.errors()` call to 58 | the `translate` method: 59 | 60 | ```Python hl_lines="14 24" 61 | from pydantic import BaseModel, ValidationError 62 | from pydantic_i18n import PydanticI18n 63 | 64 | 65 | translations = { 66 | "en_US": { 67 | "Field required": "field required", 68 | }, 69 | "de_DE": { 70 | "Field required": "Feld erforderlich", 71 | }, 72 | } 73 | 74 | tr = PydanticI18n(translations) 75 | 76 | 77 | class User(BaseModel): 78 | name: str 79 | 80 | 81 | try: 82 | User() 83 | except ValidationError as e: 84 | translated_errors = tr.translate(e.errors(), locale="de_DE") 85 | 86 | print(translated_errors) 87 | # [ 88 | # { 89 | # 'type': 'missing', 90 | # 'loc': ('name',), 91 | # 'msg': 'Feld erforderlich', 92 | # 'input': { 93 | # 94 | # }, 95 | # 'url': 'https://errors.pydantic.dev/2.6/v/missing' 96 | # } 97 | # ] 98 | ``` 99 | _(This script is complete, it should run "as is" for Pydantic 2+, an example for Pydantic 1 is 100 | [here](https://github.com/boardpack/pydantic-i18n/blob/master/docs_src/pydantic_v1/dict-loader/tutorial001.py))_ 101 | 102 | In the next chapters, you will see current available loaders and how to 103 | implement your own loader. 104 | 105 | ## Usage with FastAPI 106 | 107 | Here is a simple example usage with FastAPI. 108 | 109 | ### Create it 110 | 111 | Let's create a `tr.py` file: 112 | 113 | ```Python linenums="1" hl_lines="13-22 25-26 32 35" 114 | from fastapi import Request 115 | from fastapi.exceptions import RequestValidationError 116 | from starlette.responses import JSONResponse 117 | from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY 118 | 119 | from pydantic_i18n import PydanticI18n 120 | 121 | __all__ = ["get_locale", "validation_exception_handler"] 122 | 123 | 124 | DEFAULT_LOCALE = "en_US" 125 | 126 | translations = { 127 | "en_US": { 128 | "Field required": "field required", 129 | }, 130 | "de_DE": { 131 | "Field required": "Feld erforderlich", 132 | }, 133 | } 134 | 135 | tr = PydanticI18n(translations) 136 | 137 | 138 | def get_locale(locale: str = DEFAULT_LOCALE) -> str: 139 | return locale 140 | 141 | 142 | async def validation_exception_handler( 143 | request: Request, exc: RequestValidationError 144 | ) -> JSONResponse: 145 | current_locale = request.query_params.get("locale", DEFAULT_LOCALE) 146 | return JSONResponse( 147 | status_code=HTTP_422_UNPROCESSABLE_ENTITY, 148 | content={"detail": tr.translate(exc.errors(), current_locale)}, 149 | ) 150 | ``` 151 | 152 | `11-20`: As you see, we selected the simplest variant to store translations, 153 | you can use any that you need. 154 | 155 | `23-24`: To not include `locale` query parameter into every handler, we 156 | created a simple function `get_locale`, which we will include as a global 157 | dependency with `Depends`. 158 | 159 | `29-36`: An example of overridden function to return translated messages of the 160 | validation exception. 161 | 162 | Now we are ready to create a FastAPI application: 163 | 164 | ```Python linenums="1" hl_lines="8 10" 165 | from fastapi import Depends, FastAPI, Request 166 | from fastapi.exceptions import RequestValidationError 167 | 168 | from pydantic import BaseModel 169 | 170 | import tr 171 | 172 | app = FastAPI(dependencies=[Depends(tr.get_locale)]) 173 | 174 | app.add_exception_handler(RequestValidationError, tr.validation_exception_handler) 175 | 176 | 177 | class User(BaseModel): 178 | name: str 179 | 180 | 181 | @app.post("/user", response_model=User) 182 | def create_user(request: Request, user: User): 183 | pass 184 | ``` 185 | 186 | `8`: Add `get_locale` function as a global dependency. 187 | 188 | !!! note 189 | If you need to use i18n only for specific part of your 190 | application, you can add this `get_locale` function to the specific 191 | `APIRouter`. More information about `APIRouter` you can find 192 | [here](https://fastapi.tiangolo.com/tutorial/bigger-applications/#apirouter). 193 | 194 | `10`: Override default request validation error handler with 195 | `validation_exception_handler`. 196 | 197 | ### Run it 198 | 199 | Run the server with: 200 | 201 |
202 | 203 | ```console 204 | $ uvicorn main:app --reload 205 | 206 | INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit) 207 | INFO: Started reloader process [28720] 208 | INFO: Started server process [28722] 209 | INFO: Waiting for application startup. 210 | INFO: Application startup complete. 211 | ``` 212 | 213 |
214 | 215 |
216 | About the command uvicorn main:app --reload... 217 | 218 | The command `uvicorn main:app` refers to: 219 | 220 | * `main`: the file `main.py` (the Python "module"). 221 | * `app`: the object created inside of `main.py` with the line `app = FastAPI()`. 222 | * `--reload`: make the server restart after code changes. Only do this for development. 223 | 224 |
225 | 226 | ### Send it 227 | 228 | Open your browser at http://127.0.0.1:8000/docs#/default/create_user_user_post. 229 | 230 | Send POST-request with empty body and `de_DE` locale query param via swagger UI 231 | or `curl`: 232 | 233 | ```bash 234 | $ curl -X 'POST' \ 235 | 'http://127.0.0.1:8000/user?locale=de_DE' \ 236 | -H 'accept: application/json' \ 237 | -H 'Content-Type: application/json' \ 238 | -d '{ 239 | }' 240 | ``` 241 | 242 | ### Check it 243 | 244 | As a result, you will get the next response body: 245 | 246 | ```json hl_lines="8" 247 | { 248 | "detail": [ 249 | { 250 | "loc": [ 251 | "body", 252 | "name" 253 | ], 254 | "msg": "Feld erforderlich", 255 | "type": "value_error.missing" 256 | } 257 | ] 258 | } 259 | ``` 260 | 261 | If you don't mention the `locale` param, English locale will be used by 262 | default. 263 | 264 | ## Use placeholder in error strings 265 | 266 | You can use placeholders in error strings, but you **must mark** every placeholder with `{}`. 267 | 268 | ```Python 269 | from decimal import Decimal 270 | 271 | from pydantic import BaseModel, ValidationError, Field 272 | from pydantic_i18n import PydanticI18n 273 | 274 | 275 | translations = { 276 | "en_US": { 277 | "Decimal input should have no more than {} in total": 278 | "Decimal input should have no more than {} in total", 279 | }, 280 | "es_AR": { 281 | "Decimal input should have no more than {} in total": 282 | "La entrada decimal no debe tener más de {} en total", 283 | }, 284 | } 285 | 286 | tr = PydanticI18n(translations) 287 | 288 | 289 | class CoolSchema(BaseModel): 290 | my_field: Decimal = Field(max_digits=3) 291 | 292 | 293 | try: 294 | CoolSchema(my_field=1111) 295 | except ValidationError as e: 296 | translated_errors = tr.translate(e.errors(), locale="es_AR") 297 | 298 | print(translated_errors) 299 | # [ 300 | # { 301 | # 'type': 'decimal_max_digits', 302 | # 'loc': ('my_field',), 303 | # 'msg': 'La entrada decimal no debe tener más de 3 digits en total', 304 | # 'input': 1111, 305 | # 'ctx': { 306 | # 'max_digits': 3 307 | # }, 308 | # 'url': 'https://errors.pydantic.dev/2.6/v/decimal_max_digits' 309 | # } 310 | # ] 311 | ``` 312 | _(This script is complete, it should run "as is" for Pydantic 2+, an example for Pydantic 1 is 313 | [here](https://github.com/boardpack/pydantic-i18n/blob/master/docs_src/pydantic_v1/placeholder/tutorial001.py))_ 314 | 315 | ## Get current error strings from Pydantic 316 | 317 | pydantic-i18n doesn't provide prepared translations of all current error 318 | messages from pydantic, but you can use a special class method 319 | `PydanticI18n.get_pydantic_messages` to load original messages in English. By 320 | default, it returns a `dict` object: 321 | 322 | ```Python 323 | from pydantic_i18n import PydanticI18n 324 | 325 | print(PydanticI18n.get_pydantic_messages()) 326 | # { 327 | # "Object has no attribute '{}'": "Object has no attribute '{}'", 328 | # "Invalid JSON: {}": "Invalid JSON: {}", 329 | # "JSON input should be string, bytes or bytearray": "JSON input should be string, bytes or bytearray", 330 | # "Recursion error - cyclic reference detected": "Recursion error - cyclic reference detected", 331 | # "Field required": "Field required", 332 | # "Field is frozen": "Field is frozen", 333 | # ..... 334 | # } 335 | ``` 336 | _(This script is complete, it should run "as is" for Pydantic 2+, an example for Pydantic 1 is 337 | [here](https://github.com/boardpack/pydantic-i18n/blob/master/docs_src/pydantic_v1/pydantic-messages/tutorial001.py))_ 338 | 339 | You can also choose JSON string or Babel format with `output` parameter values 340 | `"json"` and `"babel"`: 341 | 342 | ```Python 343 | from pydantic_i18n import PydanticI18n 344 | 345 | print(PydanticI18n.get_pydantic_messages(output="json")) 346 | # { 347 | # "Field required": "Field required", 348 | # "Field is frozen": "Field is frozen", 349 | # "Error extracting attribute: {}": "Error extracting attribute: {}", 350 | # ..... 351 | # } 352 | 353 | print(PydanticI18n.get_pydantic_messages(output="babel")) 354 | # msgid "Field required" 355 | # msgstr "Field required" 356 | # 357 | # msgid "Field is frozen" 358 | # msgstr "Field is frozen" 359 | # 360 | # msgid "Error extracting attribute: {}" 361 | # msgstr "Error extracting attribute: {}" 362 | # .... 363 | 364 | ``` 365 | _(This script is complete, it should run "as is" for Pydantic 2+, an example for Pydantic 1 is 366 | [here](https://github.com/boardpack/pydantic-i18n/blob/master/docs_src/pydantic_v1/pydantic-messages/tutorial002.py))_ 367 | 368 | 369 | ## Loaders 370 | 371 | pydantic-i18n provides a list of loaders to use translations. 372 | 373 | ### DictLoader 374 | 375 | DictLoader is the simplest loader and default in PydanticI18n. So you can 376 | just pass your translations dictionary without any other preparation steps. 377 | 378 | ```Python 379 | from pydantic import BaseModel, ValidationError 380 | from pydantic_i18n import PydanticI18n 381 | 382 | 383 | translations = { 384 | "en_US": { 385 | "Field required": "field required", 386 | }, 387 | "de_DE": { 388 | "Field required": "Feld erforderlich", 389 | }, 390 | } 391 | 392 | tr = PydanticI18n(translations) 393 | 394 | 395 | class User(BaseModel): 396 | name: str 397 | 398 | 399 | try: 400 | User() 401 | except ValidationError as e: 402 | translated_errors = tr.translate(e.errors(), locale="de_DE") 403 | 404 | print(translated_errors) 405 | # [ 406 | # { 407 | # 'type': 'missing', 408 | # 'loc': ('name',), 409 | # 'msg': 'Feld erforderlich', 410 | # 'input': { 411 | # 412 | # }, 413 | # 'url': 'https://errors.pydantic.dev/2.6/v/missing' 414 | # } 415 | # ] 416 | ``` 417 | _(This script is complete, it should run "as is" for Pydantic 2+, an example for Pydantic 1 is 418 | [here](https://github.com/boardpack/pydantic-i18n/blob/master/docs_src/pydantic_v1/dict-loader/tutorial001.py))_ 419 | 420 | ### JsonLoader 421 | 422 | JsonLoader needs to get the path to some directory with the next structure: 423 | 424 | ```text 425 | 426 | |-- translations 427 | |-- en_US.json 428 | |-- de_DE.json 429 | |-- ... 430 | ``` 431 | 432 | where e.g. `en_US.json` looks like: 433 | 434 | ```json 435 | { 436 | "Field required": "Field required" 437 | } 438 | ``` 439 | 440 | and `de_DE.json`: 441 | 442 | ```json 443 | { 444 | "Field required": "Feld erforderlich" 445 | } 446 | ``` 447 | 448 | Then we can use `JsonLoader` to load our translations: 449 | 450 | ```Python 451 | from pydantic import BaseModel, ValidationError 452 | from pydantic_i18n import PydanticI18n, JsonLoader 453 | 454 | loader = JsonLoader("./translations") 455 | tr = PydanticI18n(loader) 456 | 457 | 458 | class User(BaseModel): 459 | name: str 460 | 461 | 462 | try: 463 | User() 464 | except ValidationError as e: 465 | translated_errors = tr.translate(e.errors(), locale="de_DE") 466 | 467 | print(translated_errors) 468 | # [ 469 | # { 470 | # 'type': 'missing', 471 | # 'loc': ('name', 472 | # ), 473 | # 'msg': 'Feld erforderlich', 474 | # 'input': { 475 | # 476 | # }, 477 | # 'url': 'https://errors.pydantic.dev/2.6/v/missing' 478 | # } 479 | # ] 480 | 481 | ``` 482 | _(This script is complete, it should run "as is" for Pydantic 2+, an example for Pydantic 1 is 483 | [here](https://github.com/boardpack/pydantic-i18n/blob/master/docs_src/pydantic_v1/json-loader/tutorial001.py))_ 484 | 485 | ### BabelLoader 486 | 487 | BabelLoader works in the similar way as JsonLoader. It also needs a 488 | translations directory with the next structure: 489 | 490 | ```text 491 | 492 | |-- translations 493 | |-- en_US 494 | |-- LC_MESSAGES 495 | |-- messages.mo 496 | |-- messages.po 497 | |-- de_DE 498 | |-- LC_MESSAGES 499 | |-- messages.mo 500 | |-- messages.po 501 | |-- ... 502 | ``` 503 | 504 | Information about translations preparation you can find on the 505 | [Babel docs pages](http://babel.pocoo.org/en/latest/cmdline.html){:target="_blank"} and e.g. 506 | from [this article](https://phrase.com/blog/posts/i18n-advantages-babel-python/#Message_Extraction){:target="_blank"}. 507 | 508 | Here is an example of the `BabelLoader` usage: 509 | 510 | ```Python 511 | from pydantic import BaseModel, ValidationError 512 | from pydantic_i18n import PydanticI18n, BabelLoader 513 | 514 | loader = BabelLoader("./translations") 515 | tr = PydanticI18n(loader) 516 | 517 | 518 | class User(BaseModel): 519 | name: str 520 | 521 | 522 | try: 523 | User() 524 | except ValidationError as e: 525 | translated_errors = tr.translate(e.errors(), locale="de_DE") 526 | 527 | print(translated_errors) 528 | # [ 529 | # { 530 | # 'type': 'missing', 531 | # 'loc': ('name',), 532 | # 'msg': 'Feld erforderlich', 533 | # 'input': { 534 | # 535 | # }, 536 | # 'url': 'https://errors.pydantic.dev/2.6/v/missing' 537 | # } 538 | # ] 539 | 540 | ``` 541 | _(This script is complete, it should run "as is" for Pydantic 2+, an example for Pydantic 1 is 542 | [here](https://github.com/boardpack/pydantic-i18n/blob/master/docs_src/pydantic_v1/babel-loader/tutorial001.py))_ 543 | 544 | ### Write your own loader 545 | 546 | If current loaders aren't suitable for you, it's possible to write your own 547 | loader and use it with pydantic-i18n. To do it, you need to import 548 | `BaseLoader` and implement the next items: 549 | 550 | - property `locales` to get a list of locales; 551 | - method `get_translations` to get content for the specific locale. 552 | 553 | In some cases you will also need to change implementation of the `gettext` 554 | method. 555 | 556 | Here is an example of the loader to get translations from CSV files: 557 | 558 | ```text 559 | |-- translations 560 | |-- en_US.csv 561 | |-- de_DE.csv 562 | |-- ... 563 | ``` 564 | 565 | `en_US.csv` content: 566 | 567 | ```csv 568 | Field required,Field required 569 | ``` 570 | 571 | `de_DE.csv` content: 572 | 573 | ```csv 574 | Field required,Feld erforderlich 575 | ``` 576 | 577 | ```Python 578 | import os 579 | from typing import List, Dict 580 | 581 | from pydantic import BaseModel, ValidationError 582 | from pydantic_i18n import PydanticI18n, BaseLoader 583 | 584 | 585 | class CsvLoader(BaseLoader): 586 | def __init__(self, directory: str): 587 | self.directory = directory 588 | 589 | @property 590 | def locales(self) -> List[str]: 591 | return [ 592 | filename[:-4] 593 | for filename in os.listdir(self.directory) 594 | if filename.endswith(".csv") 595 | ] 596 | 597 | def get_translations(self, locale: str) -> Dict[str, str]: 598 | with open(os.path.join(self.directory, f"{locale}.csv")) as fp: 599 | data = dict(line.strip().split(",") for line in fp) 600 | 601 | return data 602 | 603 | 604 | class User(BaseModel): 605 | name: str 606 | 607 | 608 | if __name__ == '__main__': 609 | loader = CsvLoader("./translations") 610 | tr = PydanticI18n(loader) 611 | 612 | try: 613 | User() 614 | except ValidationError as e: 615 | translated_errors = tr.translate(e.errors(), locale="de_DE") 616 | 617 | print(translated_errors) 618 | # [ 619 | # { 620 | # 'type': 'missing', 621 | # 'loc': ('name',), 622 | # 'msg': 'Feld erforderlich', 623 | # 'input': { 624 | # 625 | # }, 626 | # 'url': 'https://errors.pydantic.dev/2.6/v/missing' 627 | # } 628 | # ] 629 | 630 | ``` 631 | _(This script is complete, it should run "as is" for Pydantic 2+, an example for Pydantic 1 is 632 | [here](https://github.com/boardpack/pydantic-i18n/blob/master/docs_src/pydantic_v1/own-loader/tutorial001.py))_ 633 | 634 | ## Acknowledgments 635 | 636 | Thanks to [Samuel Colvin](https://github.com/samuelcolvin) and his 637 | [pydantic](https://github.com/samuelcolvin/pydantic) library. 638 | 639 | Also, thanks to [Sebastián Ramírez](https://github.com/tiangolo) and his 640 | [FastAPI](https://github.com/tiangolo/fastapi) project, some scripts and 641 | documentation structure and parts were used from there. 642 | 643 | ## License 644 | 645 | This project is licensed under the terms of the MIT license. 646 | -------------------------------------------------------------------------------- /docs/en/docs/release-notes.md: -------------------------------------------------------------------------------- 1 | # Release Notes 2 | 3 | ## Latest Changes 4 | 5 | * ⬆ Bump codecov/codecov-action from 5.1.2 to 5.4.0. PR [#338](https://github.com/boardpack/pydantic-i18n/pull/338) by [@dependabot[bot]](https://github.com/apps/dependabot). 6 | * ⬆ Bump dawidd6/action-download-artifact from 7 to 9. PR [#337](https://github.com/boardpack/pydantic-i18n/pull/337) by [@dependabot[bot]](https://github.com/apps/dependabot). 7 | * ⬆ Bump black from 24.10.0 to 25.1.0. PR [#332](https://github.com/boardpack/pydantic-i18n/pull/332) by [@dependabot[bot]](https://github.com/apps/dependabot). 8 | * ⬆ Bump babel from 2.16.0 to 2.17.0. PR [#331](https://github.com/boardpack/pydantic-i18n/pull/331) by [@dependabot[bot]](https://github.com/apps/dependabot). 9 | * ⬆ Bump pytest-cov from 5.0.0 to 6.0.0. PR [#308](https://github.com/boardpack/pydantic-i18n/pull/308) by [@dependabot[bot]](https://github.com/apps/dependabot). 10 | * ⬆ Bump typer from 0.12.5 to 0.15.1. PR [#320](https://github.com/boardpack/pydantic-i18n/pull/320) by [@dependabot[bot]](https://github.com/apps/dependabot). 11 | * ⬆ Bump black from 24.8.0 to 24.10.0. PR [#301](https://github.com/boardpack/pydantic-i18n/pull/301) by [@dependabot[bot]](https://github.com/apps/dependabot). 12 | * ⬆ Bump codecov/codecov-action from 4.6.0 to 5.1.2. PR [#324](https://github.com/boardpack/pydantic-i18n/pull/324) by [@dependabot[bot]](https://github.com/apps/dependabot). 13 | * ⬆ Bump dawidd6/action-download-artifact from 6 to 7. PR [#316](https://github.com/boardpack/pydantic-i18n/pull/316) by [@dependabot[bot]](https://github.com/apps/dependabot). 14 | * ⬆ Bump pytest from 8.3.3 to 8.3.4. PR [#318](https://github.com/boardpack/pydantic-i18n/pull/318) by [@dependabot[bot]](https://github.com/apps/dependabot). 15 | * ⬆ Bump mypy from 1.12.0 to 1.13.0. PR [#307](https://github.com/boardpack/pydantic-i18n/pull/307) by [@dependabot[bot]](https://github.com/apps/dependabot). 16 | * ⬆ Bump mypy from 1.11.2 to 1.12.0. PR [#303](https://github.com/boardpack/pydantic-i18n/pull/303) by [@dependabot[bot]](https://github.com/apps/dependabot). 17 | * ⬆ Bump codecov/codecov-action from 4.5.0 to 4.6.0. PR [#302](https://github.com/boardpack/pydantic-i18n/pull/302) by [@dependabot[bot]](https://github.com/apps/dependabot). 18 | 19 | ## 0.4.5 20 | 21 | ### Features 22 | 23 | * ✨ Add domain parameter to the Babel loader. PR [#290](https://github.com/boardpack/pydantic-i18n/pull/290) by [@dukkee](https://github.com/dukkee). 24 | 25 | ### Internal 26 | 27 | * 👷 Remove pytest-lazy-fixture package usage. PR [#298](https://github.com/boardpack/pydantic-i18n/pull/298) by [@dukkee](https://github.com/dukkee). 28 | * ⬆ Bump babel from 2.15.0 to 2.16.0. PR [#297](https://github.com/boardpack/pydantic-i18n/pull/297) by [@dependabot[bot]](https://github.com/apps/dependabot). 29 | * ⬆ Bump mkdocs-markdownextradata-plugin from 0.2.5 to 0.2.6. PR [#292](https://github.com/boardpack/pydantic-i18n/pull/292) by [@dependabot[bot]](https://github.com/apps/dependabot). 30 | * ⬆ Bump pyyaml from 6.0.1 to 6.0.2. PR [#291](https://github.com/boardpack/pydantic-i18n/pull/291) by [@dependabot[bot]](https://github.com/apps/dependabot). 31 | * ⬆ Bump typer from 0.12.3 to 0.12.5. PR [#293](https://github.com/boardpack/pydantic-i18n/pull/293) by [@dependabot[bot]](https://github.com/apps/dependabot). 32 | * ⬆ Bump markdown from 3.6 to 3.7. PR [#294](https://github.com/boardpack/pydantic-i18n/pull/294) by [@dependabot[bot]](https://github.com/apps/dependabot). 33 | * ⬆ Bump mypy from 1.10.0 to 1.11.2. PR [#286](https://github.com/boardpack/pydantic-i18n/pull/286) by [@dependabot[bot]](https://github.com/apps/dependabot). 34 | * ⬆ Bump black from 24.4.2 to 24.8.0. PR [#281](https://github.com/boardpack/pydantic-i18n/pull/281) by [@dependabot[bot]](https://github.com/apps/dependabot). 35 | * ⬆ Bump dawidd6/action-download-artifact from 4 to 6. PR [#269](https://github.com/boardpack/pydantic-i18n/pull/269) by [@dependabot[bot]](https://github.com/apps/dependabot). 36 | * ⬆ Bump codecov/codecov-action from 4.4.1 to 4.5.0. PR [#270](https://github.com/boardpack/pydantic-i18n/pull/270) by [@dependabot[bot]](https://github.com/apps/dependabot). 37 | * ⬆ Bump flake8 from 7.0.0 to 7.1.1. PR [#280](https://github.com/boardpack/pydantic-i18n/pull/280) by [@dependabot[bot]](https://github.com/apps/dependabot). 38 | 39 | ## 0.4.4 40 | 41 | ### Features 42 | 43 | * 🐛 Add encoding option to JsonLoader. PR [#252](https://github.com/boardpack/pydantic-i18n/pull/252) by [@dukkee](https://github.com/dukkee). 44 | 45 | ### Internal 46 | 47 | * ⬆ Bump mkdocs-material from 9.5.17 to 9.5.18. PR [#250](https://github.com/boardpack/pydantic-i18n/pull/250) by [@dependabot[bot]](https://github.com/apps/dependabot). 48 | * ⬆ Bump codecov/codecov-action from 4.2.0 to 4.3.0. PR [#248](https://github.com/boardpack/pydantic-i18n/pull/248) by [@dependabot[bot]](https://github.com/apps/dependabot). 49 | * ⬆ Bump black from 24.3.0 to 24.4.0. PR [#247](https://github.com/boardpack/pydantic-i18n/pull/247) by [@dependabot[bot]](https://github.com/apps/dependabot). 50 | * ⬆ Bump typer from 0.12.2 to 0.12.3. PR [#246](https://github.com/boardpack/pydantic-i18n/pull/246) by [@dependabot[bot]](https://github.com/apps/dependabot). 51 | * ⬆ Bump pytest-cov from 4.1.0 to 5.0.0. PR [#239](https://github.com/boardpack/pydantic-i18n/pull/239) by [@dependabot[bot]](https://github.com/apps/dependabot). 52 | * ⬆ Bump autoflake from 2.3.0 to 2.3.1. PR [#234](https://github.com/boardpack/pydantic-i18n/pull/234) by [@dependabot[bot]](https://github.com/apps/dependabot). 53 | * ⬆ Bump markdown from 3.5.2 to 3.6. PR [#232](https://github.com/boardpack/pydantic-i18n/pull/232) by [@dependabot[bot]](https://github.com/apps/dependabot). 54 | * ⬆ Bump dawidd6/action-download-artifact from 3.1.2 to 3.1.4. PR [#236](https://github.com/boardpack/pydantic-i18n/pull/236) by [@dependabot[bot]](https://github.com/apps/dependabot). 55 | 56 | ## 0.4.3 57 | 58 | ### Fixes 59 | 60 | * ⬆ Update docs examples for the usage with Pydantic 2+. PR [#231](https://github.com/boardpack/pydantic-i18n/pull/231) by [@dukkee](https://github.com/dukkee). 61 | * 🐛 Fix the error messages source for the Pydantic 2. PR [#229](https://github.com/boardpack/pydantic-i18n/pull/229) by [@dukkee](https://github.com/dukkee). 62 | * 🐛 Fix handling { in translation message. PR [#219](https://github.com/boardpack/pydantic-i18n/pull/219) by [@clemenskol](https://github.com/clemenskol). 63 | 64 | ### Internal 65 | 66 | * ⬆ Bump pyyaml from 5.3.1 to 6.0.1. PR [#230](https://github.com/boardpack/pydantic-i18n/pull/230) by [@dukkee](https://github.com/dukkee). 67 | * ⬆ Bump nwtgck/actions-netlify from 2.1.0 to 3.0.0. PR [#224](https://github.com/boardpack/pydantic-i18n/pull/224) by [@dependabot[bot]](https://github.com/apps/dependabot). 68 | * ⬆ Bump mkdocs-material from 9.5.12 to 9.5.13. PR [#225](https://github.com/boardpack/pydantic-i18n/pull/225) by [@dependabot[bot]](https://github.com/apps/dependabot). 69 | * ⬆ Bump mypy from 1.8.0 to 1.9.0. PR [#226](https://github.com/boardpack/pydantic-i18n/pull/226) by [@dependabot[bot]](https://github.com/apps/dependabot). 70 | * 👷 Add translations search by the error type. PR [#223](https://github.com/boardpack/pydantic-i18n/pull/223) by [@dukkee](https://github.com/dukkee). 71 | 72 | ## 0.4.1 73 | 74 | ### Features 75 | 76 | * ✨ Add Pydantic 2 support for the message extraction. PR [#213](https://github.com/boardpack/pydantic-i18n/pull/213) by [@dukkee](https://github.com/dukkee). 77 | * ✨ Add Python 3.12 to the support list. PR [#221](https://github.com/boardpack/pydantic-i18n/pull/221) by [@dukkee](https://github.com/dukkee). 78 | 79 | ### Fixes 80 | 81 | * 🐛 Fix handling regexp-chars in translation keys (#216). PR [#217](https://github.com/boardpack/pydantic-i18n/pull/217) by [@clemenskol](https://github.com/clemenskol). 82 | * 🐛 fix: key's ending after placeholders is not matched. PR [#161](https://github.com/boardpack/pydantic-i18n/pull/161) by [@niqzart](https://github.com/niqzart). 83 | 84 | ### Internal 85 | 86 | * 🐛 Fix code coverage. PR [#220](https://github.com/boardpack/pydantic-i18n/pull/220) by [@dukkee](https://github.com/dukkee). 87 | * ⬆ Bump mkdocs-material from 9.5.8 to 9.5.11. PR [#214](https://github.com/boardpack/pydantic-i18n/pull/214) by [@dependabot[bot]](https://github.com/apps/dependabot). 88 | * ⬆ Bump codecov/codecov-action from 4.0.2 to 4.1.0. PR [#215](https://github.com/boardpack/pydantic-i18n/pull/215) by [@dependabot[bot]](https://github.com/apps/dependabot). 89 | * ⬆ Bump flake8 from 6.1.0 to 7.0.0. PR [#208](https://github.com/boardpack/pydantic-i18n/pull/208) by [@dependabot[bot]](https://github.com/apps/dependabot). 90 | * ⬆ Bump dawidd6/action-download-artifact from 3.0.0 to 3.1.2. PR [#206](https://github.com/boardpack/pydantic-i18n/pull/206) by [@dependabot[bot]](https://github.com/apps/dependabot). 91 | * ⬆ Bump markdown from 3.2.1 to 3.5.2. PR [#209](https://github.com/boardpack/pydantic-i18n/pull/209) by [@dependabot[bot]](https://github.com/apps/dependabot). 92 | * ⬆ Bump black from 23.11.0 to 24.2.0. PR [#211](https://github.com/boardpack/pydantic-i18n/pull/211) by [@dependabot[bot]](https://github.com/apps/dependabot). 93 | * ⬆ Bump autoflake from 2.2.1 to 2.3.0. PR [#212](https://github.com/boardpack/pydantic-i18n/pull/212) by [@dependabot[bot]](https://github.com/apps/dependabot). 94 | * 🔧 Change dependabot schedule interval to weekly. PR [#205](https://github.com/boardpack/pydantic-i18n/pull/205) by [@dukkee](https://github.com/dukkee). 95 | * ⬆ Bump babel from 2.13.1 to 2.14.0. PR [#188](https://github.com/boardpack/pydantic-i18n/pull/188) by [@dependabot[bot]](https://github.com/apps/dependabot). 96 | * ⬆ Bump actions/setup-python from 4 to 5. PR [#182](https://github.com/boardpack/pydantic-i18n/pull/182) by [@dependabot[bot]](https://github.com/apps/dependabot). 97 | * ⬆ Bump isort from 5.12.0 to 5.13.2. PR [#189](https://github.com/boardpack/pydantic-i18n/pull/189) by [@dependabot[bot]](https://github.com/apps/dependabot). 98 | * ⬆ Bump actions/upload-artifact from 3 to 4. PR [#191](https://github.com/boardpack/pydantic-i18n/pull/191) by [@dependabot[bot]](https://github.com/apps/dependabot). 99 | * ⬆ Bump actions/cache from 3 to 4. PR [#195](https://github.com/boardpack/pydantic-i18n/pull/195) by [@dependabot[bot]](https://github.com/apps/dependabot). 100 | * ⬆ Bump mypy from 1.4.1 to 1.7.0. PR [#174](https://github.com/boardpack/pydantic-i18n/pull/174) by [@dependabot[bot]](https://github.com/apps/dependabot). 101 | * 👷 Use pydantic 1.X for the tests. PR [#170](https://github.com/boardpack/pydantic-i18n/pull/170) by [@dukkee](https://github.com/dukkee). 102 | * ⬆ Bump pytest from 7.4.0 to 7.4.3. PR [#168](https://github.com/boardpack/pydantic-i18n/pull/168) by [@dependabot[bot]](https://github.com/apps/dependabot). 103 | * ⬆ Bump nwtgck/actions-netlify from 2.0.0 to 2.1.0. PR [#145](https://github.com/boardpack/pydantic-i18n/pull/145) by [@dependabot[bot]](https://github.com/apps/dependabot). 104 | * ⬆ Bump actions/checkout from 3 to 4. PR [#150](https://github.com/boardpack/pydantic-i18n/pull/150) by [@dependabot[bot]](https://github.com/apps/dependabot). 105 | * ⬆ Bump mkdocs from 1.4.3 to 1.5.3. PR [#155](https://github.com/boardpack/pydantic-i18n/pull/155) by [@dependabot[bot]](https://github.com/apps/dependabot). 106 | 107 | ## 0.4.0 108 | 109 | ### Features 110 | 111 | * 🐛 Add multiple placeholders support. PR [#127](https://github.com/boardpack/pydantic-i18n/pull/127) by [@dukkee](https://github.com/dukkee). 112 | 113 | ### Fixes 114 | 115 | * 👷 Add cache step to the test workflow. PR [#95](https://github.com/boardpack/pydantic-i18n/pull/95) by [@dukkee](https://github.com/dukkee). 116 | * 🐛 Add lint step to the test workflow. PR [#93](https://github.com/boardpack/pydantic-i18n/pull/93) by [@dukkee](https://github.com/dukkee). 117 | 118 | ### Internal 119 | 120 | * ⬆ Bump pytest from 7.3.1 to 7.4.0. PR [#133](https://github.com/boardpack/pydantic-i18n/pull/133) by [@dependabot[bot]](https://github.com/apps/dependabot). 121 | * ⬆ Bump autoflake from 2.1.1 to 2.2.0. PR [#134](https://github.com/boardpack/pydantic-i18n/pull/134) by [@dependabot[bot]](https://github.com/apps/dependabot). 122 | * ⬆ Bump mypy from 1.3.0 to 1.4.1. PR [#135](https://github.com/boardpack/pydantic-i18n/pull/135) by [@dependabot[bot]](https://github.com/apps/dependabot). 123 | * ⬆ Bump pytest-cov from 4.0.0 to 4.1.0. PR [#128](https://github.com/boardpack/pydantic-i18n/pull/128) by [@dependabot[bot]](https://github.com/apps/dependabot). 124 | * ⬆ Bump codecov/codecov-action from 3.1.3 to 3.1.4. PR [#122](https://github.com/boardpack/pydantic-i18n/pull/122) by [@dependabot[bot]](https://github.com/apps/dependabot). 125 | * ⬆ Bump mkdocs from 1.4.2 to 1.4.3. PR [#117](https://github.com/boardpack/pydantic-i18n/pull/117) by [@dependabot[bot]](https://github.com/apps/dependabot). 126 | * ⬆ Bump typer from 0.7.0 to 0.9.0. PR [#118](https://github.com/boardpack/pydantic-i18n/pull/118) by [@dependabot[bot]](https://github.com/apps/dependabot). 127 | * ⬆ Bump dawidd6/action-download-artifact from 2.26.1 to 2.27.0. PR [#109](https://github.com/boardpack/pydantic-i18n/pull/109) by [@dependabot[bot]](https://github.com/apps/dependabot). 128 | * ⬆ Bump black from 23.1.0 to 23.3.0. PR [#102](https://github.com/boardpack/pydantic-i18n/pull/102) by [@dependabot[bot]](https://github.com/apps/dependabot). 129 | * 🔥 Remove tox. PR [#96](https://github.com/boardpack/pydantic-i18n/pull/96) by [@dukkee](https://github.com/dukkee). 130 | 131 | ## 0.3.1 132 | 133 | ### Fixes 134 | 135 | * 🎨 Improve translate method output type. PR [#88](https://github.com/boardpack/pydantic-i18n/pull/88) by [@dukkee](https://github.com/dukkee). 136 | 137 | ### Internal 138 | * ⬆ Bump pytest from 7.2.1 to 7.2.2. PR [#87](https://github.com/boardpack/pydantic-i18n/pull/87) by [@dependabot[bot]](https://github.com/apps/dependabot). 139 | * ⬆ Bump babel from 2.11.0 to 2.12.1. PR [#85](https://github.com/boardpack/pydantic-i18n/pull/85) by [@dependabot[bot]](https://github.com/apps/dependabot). 140 | * ⬆ Bump dawidd6/action-download-artifact from 2.25.0 to 2.26.0. PR [#82](https://github.com/boardpack/pydantic-i18n/pull/82) by [@dependabot[bot]](https://github.com/apps/dependabot). 141 | * ⬆ Bump mypy from 1.0.0 to 1.0.1. PR [#80](https://github.com/boardpack/pydantic-i18n/pull/80) by [@dependabot[bot]](https://github.com/apps/dependabot). 142 | * ⬆ Bump markdown-include from 0.8.0 to 0.8.1. PR [#78](https://github.com/boardpack/pydantic-i18n/pull/78) by [@dependabot[bot]](https://github.com/apps/dependabot). 143 | * ⬆ Bump isort from 5.11.4 to 5.12.0. PR [#68](https://github.com/boardpack/pydantic-i18n/pull/68) by [@dependabot[bot]](https://github.com/apps/dependabot). 144 | * ⬆ Bump black from 22.12.0 to 23.1.0. PR [#70](https://github.com/boardpack/pydantic-i18n/pull/70) by [@dependabot[bot]](https://github.com/apps/dependabot). 145 | * ⬆ Bump autoflake from 2.0.0 to 2.0.1. PR [#69](https://github.com/boardpack/pydantic-i18n/pull/69) by [@dependabot[bot]](https://github.com/apps/dependabot). 146 | * 🔧 Add bash to the allowlist_externals in the tox.ini. PR [#76](https://github.com/boardpack/pydantic-i18n/pull/76) by [@dukkee](https://github.com/dukkee). 147 | * ⬆ Bump nwtgck/actions-netlify from 1.2.4 to 2.0.0. PR [#52](https://github.com/boardpack/pydantic-i18n/pull/52) by [@dependabot[bot]](https://github.com/apps/dependabot). 148 | * ⬆ Bump mkdocs-material from 8.5.10 to 8.5.11. PR [#51](https://github.com/boardpack/pydantic-i18n/pull/51) by [@dependabot[bot]](https://github.com/apps/dependabot). 149 | * 🔧 Change dependabot schedule interval to daily. PR [#48](https://github.com/boardpack/pydantic-i18n/pull/48) by [@dukkee](https://github.com/dukkee). 150 | 151 | ## 0.3.0 152 | 153 | ### Features 154 | 155 | * ✨ Add Python 3.10 and 3.11 to the support list. PR [#45](https://github.com/boardpack/pydantic-i18n/pull/45) by [@dukkee](https://github.com/dukkee). 156 | 157 | ### Breaking Changes 158 | 159 | * 🔥 Drop Python 3.6 and 3.7 support. PR [#43](https://github.com/boardpack/pydantic-i18n/pull/45) by [@dukkee](https://github.com/dukkee). 160 | 161 | ### Internal 162 | 163 | * 🔧 Migrate to the new flit style metadata. PR [#46](https://github.com/boardpack/pydantic-i18n/pull/46) by [@dukkee](https://github.com/dukkee). 164 | * 📝 Update project classifiers. PR [#47](https://github.com/boardpack/pydantic-i18n/pull/47) by [@dukkee](https://github.com/dukkee). 165 | * ⬆ Bump codecov/codecov-action from 2.1.0 to 3.1.1. PR [#40](https://github.com/boardpack/pydantic-i18n/pull/40) by [@dependabot[bot]](https://github.com/apps/dependabot). 166 | * ⬆ Bump dawidd6/action-download-artifact from 2.17.0 to 2.24.2. PR [#44](https://github.com/boardpack/pydantic-i18n/pull/44) by [@dependabot[bot]](https://github.com/apps/dependabot). 167 | * ⬆ Bump actions/setup-python from 3 to 4. PR [#38](https://github.com/boardpack/pydantic-i18n/pull/38) by [@dependabot[bot]](https://github.com/apps/dependabot). 168 | * ⬆ Bump actions/cache from 2 to 3. PR [#34](https://github.com/boardpack/pydantic-i18n/pull/34) by [@dependabot[bot]](https://github.com/apps/dependabot). 169 | * ⬆ Bump actions/upload-artifact from 2 to 3. PR [#33](https://github.com/boardpack/pydantic-i18n/pull/33) by [@dependabot[bot]](https://github.com/apps/dependabot). 170 | * ⬆ Bump dependencies. PR [#43](https://github.com/boardpack/pydantic-i18n/pull/43) by [@dukkee](https://github.com/dukkee). 171 | * ⬆ Bump actions/checkout from 2 to 3. PR [#30](https://github.com/boardpack/pydantic-i18n/pull/30) by [@dependabot[bot]](https://github.com/apps/dependabot). 172 | * ⬆ Bump actions/setup-python from 2 to 3. PR [#29](https://github.com/boardpack/pydantic-i18n/pull/29) by [@dependabot[bot]](https://github.com/apps/dependabot). 173 | 174 | ## 0.2.3 175 | 176 | ### Fixes 177 | 178 | * 🐛 Fix unmoved import. PR [#28](https://github.com/boardpack/pydantic-i18n/pull/28) by [@dukkee](https://github.com/dukkee). 179 | 180 | ## 0.2.2 181 | 182 | ### Fixes 183 | 184 | * 🔨 Make babel optional dependency. PR [#26](https://github.com/boardpack/pydantic-i18n/pull/26) by [@dukkee](https://github.com/dukkee). 185 | * 🐛 Fix last key usage without placeholder. PR [#25](https://github.com/boardpack/pydantic-i18n/pull/25) by [@dukkee](https://github.com/dukkee). 186 | 187 | ### Internal 188 | 189 | * ⬆ Bump nwtgck/actions-netlify from 1.2.2 to 1.2.3. PR [#22](https://github.com/boardpack/pydantic-i18n/pull/22) by [@dependabot[bot]](https://github.com/apps/dependabot). 190 | * ⬆ Bump dawidd6/action-download-artifact from 2.15.0 to 2.17.0. PR [#24](https://github.com/boardpack/pydantic-i18n/pull/24) by [@dependabot[bot]](https://github.com/apps/dependabot). 191 | * ⬆ Bump black from 20.8b1 to 22.1.0. PR [#23](https://github.com/boardpack/pydantic-i18n/pull/23) by [@dukkee](https://github.com/dukkee). 192 | * ⬆ Bump dawidd6/action-download-artifact from 2.14.1 to 2.15.0. PR [#18](https://github.com/boardpack/pydantic-i18n/pull/18) by [@dependabot[bot]](https://github.com/apps/dependabot). 193 | 194 | ## 0.2.1 195 | 196 | ### Fixes 197 | 198 | * 🔧 Fix single key translations case. PR [#17](https://github.com/boardpack/pydantic-i18n/pull/17) by [@dukkee](https://github.com/dukkee). 199 | 200 | ## 0.2.0 201 | 202 | ### Features 203 | 204 | * 👷 Add placeholder support. PR [#15](https://github.com/boardpack/pydantic-i18n/pull/15) by [@dukkee](https://github.com/dukkee). 205 | * 👷 Fix unknown key usage. PR [#10](https://github.com/boardpack/pydantic-i18n/pull/10) by [@dukkee](https://github.com/dukkee). 206 | 207 | ### Docs 208 | 209 | * 📝 Add FastAPI example to the docs. PR [#9](https://github.com/boardpack/pydantic-i18n/pull/9) by [@dukkee](https://github.com/dukkee). 210 | 211 | ### Internal 212 | 213 | * ⬆ Bump codecov/codecov-action from 2.0.3 to 2.1.0. PR [#14](https://github.com/boardpack/pydantic-i18n/pull/14) by [@dependabot[bot]](https://github.com/apps/dependabot). 214 | * ⬆ Bump dawidd6/action-download-artifact from 2.14.0 to 2.14.1. PR [#12](https://github.com/boardpack/pydantic-i18n/pull/12) by [@dependabot[bot]](https://github.com/apps/dependabot). 215 | * ⬆ Bump codecov/codecov-action from 2.0.2 to 2.0.3. PR [#11](https://github.com/boardpack/pydantic-i18n/pull/11) by [@dependabot[bot]](https://github.com/apps/dependabot). 216 | 217 | ## 0.1.1 218 | 219 | * 📝 Fix top package description. PR [#5](https://github.com/boardpack/pydantic-i18n/pull/5) by [@dukkee](https://github.com/dukkee). 220 | 221 | ## 0.1.0 222 | 223 | ### Features 224 | 225 | * ✨ Add support for translations loading from the dictionary, JSON files, and Babel files. 226 | * ✨ Add loading errors messages from pydantic. 227 | 228 | ### Docs 229 | 230 | * 📝 Add main docs content 231 | 232 | ### Internal 233 | 234 | * ⬆ Bump dawidd6/action-download-artifact from 2.9.0 to 2.14.0. PR [#3](https://github.com/boardpack/pydantic-i18n/pull/3) by [@dependabot[bot]](https://github.com/apps/dependabot). 235 | * ⬆ Bump nwtgck/actions-netlify from 1.1.5 to 1.2.2. PR [#2](https://github.com/boardpack/pydantic-i18n/pull/2) by [@dependabot[bot]](https://github.com/apps/dependabot). 236 | * ⬆ Bump codecov/codecov-action from 1 to 2.0.2. PR [#1](https://github.com/boardpack/pydantic-i18n/pull/1) by [@dependabot[bot]](https://github.com/apps/dependabot). 237 | 238 | --------------------------------------------------------------------------------