pydantic-i18n 0.4.4

Last updated:

0 purchases

pydantic-i18n 0.4.4 Image
pydantic-i18n 0.4.4 Images
Add to Cart

Description:

pydantici18n 0.4.4

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















Documentation: https://pydantic-i18n.boardpack.org
Source Code: https://github.com/boardpack/pydantic-i18n

Requirements
Python 3.8+
pydantic-i18n has the next dependencies:

Pydantic
Babel

Installation

$ pip install pydantic-i18n

---> 100%


First steps
To start to work with pydantic-i18n, you can just create a dictionary (or
create any needed translations storage and then convert it into dictionary)
and pass to the main PydanticI18n class.
To translate messages, you need to pass result of exception.errors() call to
the translate method:
from pydantic import BaseModel, ValidationError
from pydantic_i18n import PydanticI18n


translations = {
"en_US": {
"Field required": "field required",
},
"de_DE": {
"Field required": "Feld erforderlich",
},
}

tr = PydanticI18n(translations)


class User(BaseModel):
name: str


try:
User()
except ValidationError as e:
translated_errors = tr.translate(e.errors(), locale="de_DE")

print(translated_errors)
# [
# {
# 'type': 'missing',
# 'loc': ('name',),
# 'msg': 'Feld erforderlich',
# 'input': {
#
# },
# 'url': 'https://errors.pydantic.dev/2.6/v/missing'
# }
# ]

(This script is complete, it should run "as is" for Pydantic 2+, an example for Pydantic 1 is
here)
In the next chapters, you will see current available loaders and how to
implement your own loader.
Usage with FastAPI
Here is a simple example usage with FastAPI.
Create it
Let's create a tr.py file:
from fastapi import Request
from fastapi.exceptions import RequestValidationError
from starlette.responses import JSONResponse
from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY

from pydantic_i18n import PydanticI18n

__all__ = ["get_locale", "validation_exception_handler"]


DEFAULT_LOCALE = "en_US"

translations = {
"en_US": {
"Field required": "field required",
},
"de_DE": {
"Field required": "Feld erforderlich",
},
}

tr = PydanticI18n(translations)


def get_locale(locale: str = DEFAULT_LOCALE) -> str:
return locale


async def validation_exception_handler(
request: Request, exc: RequestValidationError
) -> JSONResponse:
current_locale = request.query_params.get("locale", DEFAULT_LOCALE)
return JSONResponse(
status_code=HTTP_422_UNPROCESSABLE_ENTITY,
content={"detail": tr.translate(exc.errors(), current_locale)},
)

11-20: As you see, we selected the simplest variant to store translations,
you can use any that you need.
23-24: To not include locale query parameter into every handler, we
created a simple function get_locale, which we will include as a global
dependency with Depends.
29-36: An example of overridden function to return translated messages of the
validation exception.
Now we are ready to create a FastAPI application:
from fastapi import Depends, FastAPI, Request
from fastapi.exceptions import RequestValidationError

from pydantic import BaseModel

import tr

app = FastAPI(dependencies=[Depends(tr.get_locale)])

app.add_exception_handler(RequestValidationError, tr.validation_exception_handler)


class User(BaseModel):
name: str


@app.post("/user", response_model=User)
def create_user(request: Request, user: User):
pass

8: Add get_locale function as a global dependency.
!!! note
If you need to use i18n only for specific part of your
application, you can add this get_locale function to the specific
APIRouter. More information about APIRouter you can find
here.
10: Override default request validation error handler with
validation_exception_handler.
Run it
Run the server with:

$ uvicorn main:app --reload

INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started reloader process [28720]
INFO: Started server process [28722]
INFO: Waiting for application startup.
INFO: Application startup complete.



About the command uvicorn main:app --reload...
The command uvicorn main:app refers to:

main: the file main.py (the Python "module").
app: the object created inside of main.py with the line app = FastAPI().
--reload: make the server restart after code changes. Only do this for development.


Send it
Open your browser at http://127.0.0.1:8000/docs#/default/create_user_user_post.
Send POST-request with empty body and de_DE locale query param via swagger UI
or curl:
$ curl -X 'POST' \
'http://127.0.0.1:8000/user?locale=de_DE' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
}'

Check it
As a result, you will get the next response body:
{
"detail": [
{
"loc": [
"body",
"name"
],
"msg": "Feld erforderlich",
"type": "value_error.missing"
}
]
}

If you don't mention the locale param, English locale will be used by
default.
Use placeholder in error strings
You can use placeholders in error strings, but you must mark every placeholder with {}.
from decimal import Decimal

from pydantic import BaseModel, ValidationError, Field
from pydantic_i18n import PydanticI18n


translations = {
"en_US": {
"Decimal input should have no more than {} in total":
"Decimal input should have no more than {} in total",
},
"es_AR": {
"Decimal input should have no more than {} in total":
"La entrada decimal no debe tener más de {} en total",
},
}

tr = PydanticI18n(translations)


class CoolSchema(BaseModel):
my_field: Decimal = Field(max_digits=3)


try:
CoolSchema(my_field=1111)
except ValidationError as e:
translated_errors = tr.translate(e.errors(), locale="es_AR")

print(translated_errors)
# [
# {
# 'type': 'decimal_max_digits',
# 'loc': ('my_field',),
# 'msg': 'La entrada decimal no debe tener más de 3 digits en total',
# 'input': 1111,
# 'ctx': {
# 'max_digits': 3
# },
# 'url': 'https://errors.pydantic.dev/2.6/v/decimal_max_digits'
# }
# ]

(This script is complete, it should run "as is" for Pydantic 2+, an example for Pydantic 1 is
here)
Get current error strings from Pydantic
pydantic-i18n doesn't provide prepared translations of all current error
messages from pydantic, but you can use a special class method
PydanticI18n.get_pydantic_messages to load original messages in English. By
default, it returns a dict object:
from pydantic_i18n import PydanticI18n

print(PydanticI18n.get_pydantic_messages())
# {
# "Object has no attribute '{}'": "Object has no attribute '{}'",
# "Invalid JSON: {}": "Invalid JSON: {}",
# "JSON input should be string, bytes or bytearray": "JSON input should be string, bytes or bytearray",
# "Recursion error - cyclic reference detected": "Recursion error - cyclic reference detected",
# "Field required": "Field required",
# "Field is frozen": "Field is frozen",
# .....
# }

(This script is complete, it should run "as is" for Pydantic 2+, an example for Pydantic 1 is
here)
You can also choose JSON string or Babel format with output parameter values
"json" and "babel":
from pydantic_i18n import PydanticI18n

print(PydanticI18n.get_pydantic_messages(output="json"))
# {
# "Field required": "Field required",
# "Field is frozen": "Field is frozen",
# "Error extracting attribute: {}": "Error extracting attribute: {}",
# .....
# }

print(PydanticI18n.get_pydantic_messages(output="babel"))
# msgid "Field required"
# msgstr "Field required"
#
# msgid "Field is frozen"
# msgstr "Field is frozen"
#
# msgid "Error extracting attribute: {}"
# msgstr "Error extracting attribute: {}"
# ....

(This script is complete, it should run "as is" for Pydantic 2+, an example for Pydantic 1 is
here)
Loaders
pydantic-i18n provides a list of loaders to use translations.
DictLoader
DictLoader is the simplest loader and default in PydanticI18n. So you can
just pass your translations dictionary without any other preparation steps.
from pydantic import BaseModel, ValidationError
from pydantic_i18n import PydanticI18n


translations = {
"en_US": {
"Field required": "field required",
},
"de_DE": {
"Field required": "Feld erforderlich",
},
}

tr = PydanticI18n(translations)


class User(BaseModel):
name: str


try:
User()
except ValidationError as e:
translated_errors = tr.translate(e.errors(), locale="de_DE")

print(translated_errors)
# [
# {
# 'type': 'missing',
# 'loc': ('name',),
# 'msg': 'Feld erforderlich',
# 'input': {
#
# },
# 'url': 'https://errors.pydantic.dev/2.6/v/missing'
# }
# ]

(This script is complete, it should run "as is" for Pydantic 2+, an example for Pydantic 1 is
here)
JsonLoader
JsonLoader needs to get the path to some directory with the next structure:
|-- translations
|-- en_US.json
|-- de_DE.json
|-- ...

where e.g. en_US.json looks like:
{
"Field required": "Field required"
}

and de_DE.json:
{
"Field required": "Feld erforderlich"
}

Then we can use JsonLoader to load our translations:
from pydantic import BaseModel, ValidationError
from pydantic_i18n import PydanticI18n, JsonLoader

loader = JsonLoader("./translations")
tr = PydanticI18n(loader)


class User(BaseModel):
name: str


try:
User()
except ValidationError as e:
translated_errors = tr.translate(e.errors(), locale="de_DE")

print(translated_errors)
# [
# {
# 'type': 'missing',
# 'loc': ('name',
# ),
# 'msg': 'Feld erforderlich',
# 'input': {
#
# },
# 'url': 'https://errors.pydantic.dev/2.6/v/missing'
# }
# ]

(This script is complete, it should run "as is" for Pydantic 2+, an example for Pydantic 1 is
here)
BabelLoader
BabelLoader works in the similar way as JsonLoader. It also needs a
translations directory with the next structure:
|-- translations
|-- en_US
|-- LC_MESSAGES
|-- messages.mo
|-- messages.po
|-- de_DE
|-- LC_MESSAGES
|-- messages.mo
|-- messages.po
|-- ...

Information about translations preparation you can find on the
Babel docs pages{:target="_blank"} and e.g.
from this article{:target="_blank"}.
Here is an example of the BabelLoader usage:
from pydantic import BaseModel, ValidationError
from pydantic_i18n import PydanticI18n, BabelLoader

loader = BabelLoader("./translations")
tr = PydanticI18n(loader)


class User(BaseModel):
name: str


try:
User()
except ValidationError as e:
translated_errors = tr.translate(e.errors(), locale="de_DE")

print(translated_errors)
# [
# {
# 'type': 'missing',
# 'loc': ('name',),
# 'msg': 'Feld erforderlich',
# 'input': {
#
# },
# 'url': 'https://errors.pydantic.dev/2.6/v/missing'
# }
# ]

(This script is complete, it should run "as is" for Pydantic 2+, an example for Pydantic 1 is
here)
Write your own loader
If current loaders aren't suitable for you, it's possible to write your own
loader and use it with pydantic-i18n. To do it, you need to import
BaseLoader and implement the next items:

property locales to get a list of locales;
method get_translations to get content for the specific locale.

In some cases you will also need to change implementation of the gettext
method.
Here is an example of the loader to get translations from CSV files:
|-- translations
|-- en_US.csv
|-- de_DE.csv
|-- ...

en_US.csv content:
Field required,Field required

de_DE.csv content:
Field required,Feld erforderlich

import os
from typing import List, Dict

from pydantic import BaseModel, ValidationError
from pydantic_i18n import PydanticI18n, BaseLoader


class CsvLoader(BaseLoader):
def __init__(self, directory: str):
self.directory = directory

@property
def locales(self) -> List[str]:
return [
filename[:-4]
for filename in os.listdir(self.directory)
if filename.endswith(".csv")
]

def get_translations(self, locale: str) -> Dict[str, str]:
with open(os.path.join(self.directory, f"{locale}.csv")) as fp:
data = dict(line.strip().split(",") for line in fp)

return data


class User(BaseModel):
name: str


if __name__ == '__main__':
loader = CsvLoader("./translations")
tr = PydanticI18n(loader)

try:
User()
except ValidationError as e:
translated_errors = tr.translate(e.errors(), locale="de_DE")

print(translated_errors)
# [
# {
# 'type': 'missing',
# 'loc': ('name',),
# 'msg': 'Feld erforderlich',
# 'input': {
#
# },
# 'url': 'https://errors.pydantic.dev/2.6/v/missing'
# }
# ]

(This script is complete, it should run "as is" for Pydantic 2+, an example for Pydantic 1 is
here)
Acknowledgments
Thanks to Samuel Colvin and his
pydantic library.
Also, thanks to Sebastián Ramírez and his
FastAPI project, some scripts and
documentation structure and parts were used from there.
License
This project is licensed under the terms of the MIT license.

License:

For personal and professional use. You cannot resell or redistribute these repositories in their original state.

Customer Reviews

There are no reviews.