fastapi-locale 0.1.0rc1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastapi_locale-0.1.0rc1/.gitignore +40 -0
- fastapi_locale-0.1.0rc1/CHANGELOG.md +25 -0
- fastapi_locale-0.1.0rc1/LICENSE +21 -0
- fastapi_locale-0.1.0rc1/PKG-INFO +91 -0
- fastapi_locale-0.1.0rc1/README.md +58 -0
- fastapi_locale-0.1.0rc1/docs/about/changelog.md +2 -0
- fastapi_locale-0.1.0rc1/docs/about/contributing.md +2 -0
- fastapi_locale-0.1.0rc1/docs/about/license.md +5 -0
- fastapi_locale-0.1.0rc1/docs/about/security.md +2 -0
- fastapi_locale-0.1.0rc1/docs/adr/0001-record-architecture-decisions.md +23 -0
- fastapi_locale-0.1.0rc1/docs/adr/0002-gettext-catalogs-through-babel.md +39 -0
- fastapi_locale-0.1.0rc1/docs/adr/0003-request-locale-in-a-context-variable.md +53 -0
- fastapi_locale-0.1.0rc1/docs/adr/0004-middleware-resolves-dependencies-refine.md +51 -0
- fastapi_locale-0.1.0rc1/docs/adr/0005-lazy-text-type.md +56 -0
- fastapi_locale-0.1.0rc1/docs/adr/0006-validation-messages-keyed-on-error-type.md +51 -0
- fastapi_locale-0.1.0rc1/docs/adr/0007-named-placeholder-formatting.md +42 -0
- fastapi_locale-0.1.0rc1/docs/adr/0008-load-catalogs-at-setup.md +37 -0
- fastapi_locale-0.1.0rc1/docs/adr/0009-localized-openapi.md +59 -0
- fastapi_locale-0.1.0rc1/docs/adr/0010-toolchain.md +46 -0
- fastapi_locale-0.1.0rc1/docs/adr/README.md +18 -0
- fastapi_locale-0.1.0rc1/docs/architecture/software-architecture.md +212 -0
- fastapi_locale-0.1.0rc1/docs/assets/favicon.svg +1 -0
- fastapi_locale-0.1.0rc1/docs/design/detailed-design.md +424 -0
- fastapi_locale-0.1.0rc1/docs/development/development-guide.md +171 -0
- fastapi_locale-0.1.0rc1/docs/diagrams/activity-accept-language.puml +26 -0
- fastapi_locale-0.1.0rc1/docs/diagrams/activity-accept-language.svg +1 -0
- fastapi_locale-0.1.0rc1/docs/diagrams/activity-catalog-workflow.puml +37 -0
- fastapi_locale-0.1.0rc1/docs/diagrams/activity-catalog-workflow.svg +1 -0
- fastapi_locale-0.1.0rc1/docs/diagrams/activity-locale-resolution.puml +31 -0
- fastapi_locale-0.1.0rc1/docs/diagrams/activity-locale-resolution.svg +1 -0
- fastapi_locale-0.1.0rc1/docs/diagrams/activity-message-lookup.puml +33 -0
- fastapi_locale-0.1.0rc1/docs/diagrams/activity-message-lookup.svg +1 -0
- fastapi_locale-0.1.0rc1/docs/diagrams/class-design.puml +142 -0
- fastapi_locale-0.1.0rc1/docs/diagrams/class-design.svg +1 -0
- fastapi_locale-0.1.0rc1/docs/diagrams/components.puml +62 -0
- fastapi_locale-0.1.0rc1/docs/diagrams/components.svg +1 -0
- fastapi_locale-0.1.0rc1/docs/diagrams/deployment.puml +34 -0
- fastapi_locale-0.1.0rc1/docs/diagrams/deployment.svg +1 -0
- fastapi_locale-0.1.0rc1/docs/diagrams/domain-model.puml +125 -0
- fastapi_locale-0.1.0rc1/docs/diagrams/domain-model.svg +1 -0
- fastapi_locale-0.1.0rc1/docs/diagrams/include/style.iuml +21 -0
- fastapi_locale-0.1.0rc1/docs/diagrams/module-dependencies.puml +75 -0
- fastapi_locale-0.1.0rc1/docs/diagrams/module-dependencies.svg +1 -0
- fastapi_locale-0.1.0rc1/docs/diagrams/sequence-lazy-text.puml +40 -0
- fastapi_locale-0.1.0rc1/docs/diagrams/sequence-lazy-text.svg +1 -0
- fastapi_locale-0.1.0rc1/docs/diagrams/sequence-request.puml +46 -0
- fastapi_locale-0.1.0rc1/docs/diagrams/sequence-request.svg +1 -0
- fastapi_locale-0.1.0rc1/docs/diagrams/sequence-startup.puml +52 -0
- fastapi_locale-0.1.0rc1/docs/diagrams/sequence-startup.svg +1 -0
- fastapi_locale-0.1.0rc1/docs/diagrams/sequence-validation-error.puml +54 -0
- fastapi_locale-0.1.0rc1/docs/diagrams/sequence-validation-error.svg +1 -0
- fastapi_locale-0.1.0rc1/docs/diagrams/state-catalog-store.puml +16 -0
- fastapi_locale-0.1.0rc1/docs/diagrams/state-catalog-store.svg +1 -0
- fastapi_locale-0.1.0rc1/docs/diagrams/state-request-locale.puml +29 -0
- fastapi_locale-0.1.0rc1/docs/diagrams/state-request-locale.svg +1 -0
- fastapi_locale-0.1.0rc1/docs/diagrams/system-context.puml +45 -0
- fastapi_locale-0.1.0rc1/docs/diagrams/system-context.svg +1 -0
- fastapi_locale-0.1.0rc1/docs/diagrams/use-case.puml +40 -0
- fastapi_locale-0.1.0rc1/docs/diagrams/use-case.svg +1 -0
- fastapi_locale-0.1.0rc1/docs/getting-started/installation.md +32 -0
- fastapi_locale-0.1.0rc1/docs/getting-started/quickstart.md +103 -0
- fastapi_locale-0.1.0rc1/docs/getting-started/tutorial.md +195 -0
- fastapi_locale-0.1.0rc1/docs/guide/api-documentation.md +62 -0
- fastapi_locale-0.1.0rc1/docs/guide/command-line.md +31 -0
- fastapi_locale-0.1.0rc1/docs/guide/configuration.md +56 -0
- fastapi_locale-0.1.0rc1/docs/guide/deployment.md +53 -0
- fastapi_locale-0.1.0rc1/docs/guide/lazy-text.md +51 -0
- fastapi_locale-0.1.0rc1/docs/guide/locale-resolution.md +45 -0
- fastapi_locale-0.1.0rc1/docs/guide/migration.md +50 -0
- fastapi_locale-0.1.0rc1/docs/guide/testing.md +46 -0
- fastapi_locale-0.1.0rc1/docs/guide/translating.md +49 -0
- fastapi_locale-0.1.0rc1/docs/guide/troubleshooting.md +47 -0
- fastapi_locale-0.1.0rc1/docs/guide/user-language.md +48 -0
- fastapi_locale-0.1.0rc1/docs/guide/validation-errors.md +65 -0
- fastapi_locale-0.1.0rc1/docs/index.md +57 -0
- fastapi_locale-0.1.0rc1/docs/modeling/domain-model.md +127 -0
- fastapi_locale-0.1.0rc1/docs/modeling/use-case-model.md +234 -0
- fastapi_locale-0.1.0rc1/docs/project-documents.md +47 -0
- fastapi_locale-0.1.0rc1/docs/reference/context.md +9 -0
- fastapi_locale-0.1.0rc1/docs/reference/dependencies.md +9 -0
- fastapi_locale-0.1.0rc1/docs/reference/errors.md +17 -0
- fastapi_locale-0.1.0rc1/docs/reference/lazy.md +11 -0
- fastapi_locale-0.1.0rc1/docs/reference/setup.md +11 -0
- fastapi_locale-0.1.0rc1/docs/reference/sources.md +9 -0
- fastapi_locale-0.1.0rc1/docs/reference/testing.md +3 -0
- fastapi_locale-0.1.0rc1/docs/reference/translation.md +23 -0
- fastapi_locale-0.1.0rc1/docs/requirements/software-requirements-specification.md +281 -0
- fastapi_locale-0.1.0rc1/docs/research/existing-libraries.md +159 -0
- fastapi_locale-0.1.0rc1/docs/testing/test-plan.md +155 -0
- fastapi_locale-0.1.0rc1/examples/basic/README.md +21 -0
- fastapi_locale-0.1.0rc1/examples/basic/app.py +82 -0
- fastapi_locale-0.1.0rc1/examples/basic/locales/de/LC_MESSAGES/messages.po +55 -0
- fastapi_locale-0.1.0rc1/examples/basic/locales/hi/LC_MESSAGES/messages.po +55 -0
- fastapi_locale-0.1.0rc1/examples/basic/locales/messages.pot +53 -0
- fastapi_locale-0.1.0rc1/examples/basic/pyproject.toml +5 -0
- fastapi_locale-0.1.0rc1/examples/basic/uv.lock +3 -0
- fastapi_locale-0.1.0rc1/pyproject.toml +223 -0
- fastapi_locale-0.1.0rc1/src/fastapi_locale/__init__.py +98 -0
- fastapi_locale-0.1.0rc1/src/fastapi_locale/_accept_language.py +52 -0
- fastapi_locale-0.1.0rc1/src/fastapi_locale/_catalog.py +244 -0
- fastapi_locale-0.1.0rc1/src/fastapi_locale/_context.py +224 -0
- fastapi_locale-0.1.0rc1/src/fastapi_locale/_errors_catalog.py +281 -0
- fastapi_locale-0.1.0rc1/src/fastapi_locale/_formatting.py +45 -0
- fastapi_locale-0.1.0rc1/src/fastapi_locale/_locale.py +100 -0
- fastapi_locale-0.1.0rc1/src/fastapi_locale/_openapi.py +71 -0
- fastapi_locale-0.1.0rc1/src/fastapi_locale/_translator.py +171 -0
- fastapi_locale-0.1.0rc1/src/fastapi_locale/cli/__init__.py +74 -0
- fastapi_locale-0.1.0rc1/src/fastapi_locale/cli/__main__.py +5 -0
- fastapi_locale-0.1.0rc1/src/fastapi_locale/cli/commands.py +237 -0
- fastapi_locale-0.1.0rc1/src/fastapi_locale/cli/settings.py +77 -0
- fastapi_locale-0.1.0rc1/src/fastapi_locale/config.py +82 -0
- fastapi_locale-0.1.0rc1/src/fastapi_locale/dependencies.py +29 -0
- fastapi_locale-0.1.0rc1/src/fastapi_locale/exceptions.py +31 -0
- fastapi_locale-0.1.0rc1/src/fastapi_locale/handlers.py +49 -0
- fastapi_locale-0.1.0rc1/src/fastapi_locale/lazy.py +139 -0
- fastapi_locale-0.1.0rc1/src/fastapi_locale/locales/de/LC_MESSAGES/fastapi_locale.po +556 -0
- fastapi_locale-0.1.0rc1/src/fastapi_locale/locales/es/LC_MESSAGES/fastapi_locale.po +554 -0
- fastapi_locale-0.1.0rc1/src/fastapi_locale/locales/fastapi_locale.pot +534 -0
- fastapi_locale-0.1.0rc1/src/fastapi_locale/locales/fr/LC_MESSAGES/fastapi_locale.po +552 -0
- fastapi_locale-0.1.0rc1/src/fastapi_locale/locales/hi/LC_MESSAGES/fastapi_locale.po +545 -0
- fastapi_locale-0.1.0rc1/src/fastapi_locale/locales/pt_BR/LC_MESSAGES/fastapi_locale.po +546 -0
- fastapi_locale-0.1.0rc1/src/fastapi_locale/localization.py +161 -0
- fastapi_locale-0.1.0rc1/src/fastapi_locale/middleware.py +66 -0
- fastapi_locale-0.1.0rc1/src/fastapi_locale/openapi.py +34 -0
- fastapi_locale-0.1.0rc1/src/fastapi_locale/py.typed +0 -0
- fastapi_locale-0.1.0rc1/src/fastapi_locale/sources.py +102 -0
- fastapi_locale-0.1.0rc1/src/fastapi_locale/testing.py +30 -0
- fastapi_locale-0.1.0rc1/tests/__init__.py +0 -0
- fastapi_locale-0.1.0rc1/tests/benchmark/__init__.py +0 -0
- fastapi_locale-0.1.0rc1/tests/benchmark/test_budget.py +39 -0
- fastapi_locale-0.1.0rc1/tests/conftest.py +100 -0
- fastapi_locale-0.1.0rc1/tests/data/locales/ar/LC_MESSAGES/messages.po +28 -0
- fastapi_locale-0.1.0rc1/tests/data/locales/de/LC_MESSAGES/admin.po +7 -0
- fastapi_locale-0.1.0rc1/tests/data/locales/de/LC_MESSAGES/fastapi_locale.po +8 -0
- fastapi_locale-0.1.0rc1/tests/data/locales/de/LC_MESSAGES/messages.po +46 -0
- fastapi_locale-0.1.0rc1/tests/data/locales/fr/LC_MESSAGES/messages.po +24 -0
- fastapi_locale-0.1.0rc1/tests/data/locales/hi/LC_MESSAGES/messages.po +27 -0
- fastapi_locale-0.1.0rc1/tests/data/locales/ja/LC_MESSAGES/messages.po +23 -0
- fastapi_locale-0.1.0rc1/tests/data/locales/pt/LC_MESSAGES/messages.po +27 -0
- fastapi_locale-0.1.0rc1/tests/data/locales/pt_BR/LC_MESSAGES/messages.po +8 -0
- fastapi_locale-0.1.0rc1/tests/data/locales/ru/LC_MESSAGES/messages.po +25 -0
- fastapi_locale-0.1.0rc1/tests/e2e/__init__.py +0 -0
- fastapi_locale-0.1.0rc1/tests/e2e/conftest.py +72 -0
- fastapi_locale-0.1.0rc1/tests/e2e/test_cli.py +180 -0
- fastapi_locale-0.1.0rc1/tests/e2e/test_server.py +78 -0
- fastapi_locale-0.1.0rc1/tests/integration/__init__.py +0 -0
- fastapi_locale-0.1.0rc1/tests/integration/test_background.py +18 -0
- fastapi_locale-0.1.0rc1/tests/integration/test_dependencies.py +26 -0
- fastapi_locale-0.1.0rc1/tests/integration/test_headers.py +68 -0
- fastapi_locale-0.1.0rc1/tests/integration/test_http_errors.py +44 -0
- fastapi_locale-0.1.0rc1/tests/integration/test_isolation.py +37 -0
- fastapi_locale-0.1.0rc1/tests/integration/test_lazy_responses.py +41 -0
- fastapi_locale-0.1.0rc1/tests/integration/test_openapi.py +62 -0
- fastapi_locale-0.1.0rc1/tests/integration/test_resolution.py +88 -0
- fastapi_locale-0.1.0rc1/tests/integration/test_setup.py +90 -0
- fastapi_locale-0.1.0rc1/tests/integration/test_testing_helpers.py +41 -0
- fastapi_locale-0.1.0rc1/tests/integration/test_user_locale.py +63 -0
- fastapi_locale-0.1.0rc1/tests/integration/test_validation_errors.py +117 -0
- fastapi_locale-0.1.0rc1/tests/integration/test_websocket.py +17 -0
- fastapi_locale-0.1.0rc1/tests/unit/__init__.py +0 -0
- fastapi_locale-0.1.0rc1/tests/unit/test_accept_language.py +53 -0
- fastapi_locale-0.1.0rc1/tests/unit/test_catalog.py +148 -0
- fastapi_locale-0.1.0rc1/tests/unit/test_config.py +51 -0
- fastapi_locale-0.1.0rc1/tests/unit/test_context.py +103 -0
- fastapi_locale-0.1.0rc1/tests/unit/test_error_localizer.py +112 -0
- fastapi_locale-0.1.0rc1/tests/unit/test_formatting.py +42 -0
- fastapi_locale-0.1.0rc1/tests/unit/test_lazy.py +96 -0
- fastapi_locale-0.1.0rc1/tests/unit/test_locale.py +88 -0
- fastapi_locale-0.1.0rc1/tests/unit/test_openapi.py +72 -0
- fastapi_locale-0.1.0rc1/tests/unit/test_translator.py +111 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
|
|
5
|
+
# Packaging
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
*.egg-info/
|
|
9
|
+
|
|
10
|
+
# Environments
|
|
11
|
+
.venv/
|
|
12
|
+
.env
|
|
13
|
+
.env.*
|
|
14
|
+
|
|
15
|
+
# Tests and tools
|
|
16
|
+
.pytest_cache/
|
|
17
|
+
.coverage
|
|
18
|
+
.coverage.*
|
|
19
|
+
coverage.xml
|
|
20
|
+
htmlcov/
|
|
21
|
+
.hypothesis/
|
|
22
|
+
.benchmarks/
|
|
23
|
+
.mypy_cache/
|
|
24
|
+
.ruff_cache/
|
|
25
|
+
.import_linter_cache/
|
|
26
|
+
|
|
27
|
+
# Compiled catalogs are build output; .po files are the source
|
|
28
|
+
*.mo
|
|
29
|
+
|
|
30
|
+
# Documentation build
|
|
31
|
+
site/
|
|
32
|
+
|
|
33
|
+
# Editors and OS
|
|
34
|
+
.vscode/
|
|
35
|
+
.idea/
|
|
36
|
+
*.swp
|
|
37
|
+
.DS_Store
|
|
38
|
+
|
|
39
|
+
# Local scratch files and visual aids, never committed
|
|
40
|
+
.tmp/
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes are listed here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project uses
|
|
5
|
+
[semantic versioning](https://semver.org).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0rc1] - 2026-09-28
|
|
10
|
+
|
|
11
|
+
First release candidate. Installs only with `pip install --pre fastapi-locale` or an exact version.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Per-request locale from query parameter, cookie and `Accept-Language`, with RFC 4647 matching and
|
|
16
|
+
`Content-Language` and `Vary` response headers.
|
|
17
|
+
- gettext, ngettext, pgettext, npgettext and their domain variants, with named placeholders.
|
|
18
|
+
- `LazyText` for translatable text in Pydantic models, response models and `HTTPException.detail`.
|
|
19
|
+
- Localized 422 validation errors for every Pydantic error type, with built-in German, Spanish, French,
|
|
20
|
+
Hindi and Portuguese (Brazil) translations.
|
|
21
|
+
- `set_locale()` to apply a signed-in user's language for the rest of a request.
|
|
22
|
+
- `LocaleDep` and `TranslatorDep` dependencies, `use_locale()`, and test helpers.
|
|
23
|
+
- `fastapi-locale` command: extract, init, update, compile and check.
|
|
24
|
+
- Localized OpenAPI schema: Swagger UI and ReDoc follow the request locale, with built-in
|
|
25
|
+
translations of FastAPI's own schema text. Turn off with `localize_openapi=False`.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 fastapi-locale contributors
|
|
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.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fastapi-locale
|
|
3
|
+
Version: 0.1.0rc1
|
|
4
|
+
Summary: Internationalization for FastAPI: gettext catalogs, per-request locales, lazy text and localized validation errors.
|
|
5
|
+
Project-URL: Homepage, https://github.com/fastapi-extensions/fastapi-locale
|
|
6
|
+
Project-URL: Documentation, https://fastapi-locale.readthedocs.io
|
|
7
|
+
Project-URL: Repository, https://github.com/fastapi-extensions/fastapi-locale
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/fastapi-extensions/fastapi-locale/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/fastapi-extensions/fastapi-locale/blob/main/CHANGELOG.md
|
|
10
|
+
Author: fastapi-locale contributors
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: babel,fastapi,gettext,i18n,internationalization,l10n,localization,pydantic
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Framework :: FastAPI
|
|
16
|
+
Classifier: Framework :: Pydantic :: 2
|
|
17
|
+
Classifier: Intended Audience :: Developers
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
25
|
+
Classifier: Topic :: Software Development :: Internationalization
|
|
26
|
+
Classifier: Topic :: Software Development :: Localization
|
|
27
|
+
Classifier: Typing :: Typed
|
|
28
|
+
Requires-Python: >=3.11
|
|
29
|
+
Requires-Dist: babel>=2.12.0
|
|
30
|
+
Requires-Dist: fastapi>=0.115.0
|
|
31
|
+
Requires-Dist: pydantic>=2.7.0
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# fastapi-locale
|
|
35
|
+
|
|
36
|
+
Internationalization for [FastAPI](https://fastapi.tiangolo.com), built on standard gettext catalogs.
|
|
37
|
+
|
|
38
|
+
- Picks a locale for every request from a query parameter, cookie or `Accept-Language`, with RFC 4647
|
|
39
|
+
matching and correct `Content-Language` and `Vary` headers.
|
|
40
|
+
- Returns FastAPI's 422 validation errors in the client's language, with correct plural forms.
|
|
41
|
+
- Lazy text that works in Pydantic models, response models and `HTTPException.detail`.
|
|
42
|
+
- Dependency injection first: `LocaleDep` and `TranslatorDep`, replaceable in tests.
|
|
43
|
+
- Lets a dependency switch to the signed-in user's saved language for the rest of the request.
|
|
44
|
+
- A command line tool to extract, update, compile and check catalogs in CI.
|
|
45
|
+
|
|
46
|
+
> **Status:** in development, not yet released. The design is in [docs/](docs/project-documents.md).
|
|
47
|
+
|
|
48
|
+
## Quick start
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from fastapi import FastAPI, HTTPException
|
|
52
|
+
|
|
53
|
+
from fastapi_locale import LocaleConfig, Localization, TranslatorDep, gettext_lazy
|
|
54
|
+
|
|
55
|
+
i18n = Localization(
|
|
56
|
+
LocaleConfig(default_locale="en", supported_locales=["en", "hi", "de"], catalog_dirs=["locales"])
|
|
57
|
+
)
|
|
58
|
+
app = FastAPI()
|
|
59
|
+
i18n.install(app)
|
|
60
|
+
|
|
61
|
+
NOT_FOUND = gettext_lazy("Item not found")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@app.get("/items/{item_id}")
|
|
65
|
+
def read_item(item_id: int, tr: TranslatorDep) -> dict[str, str]:
|
|
66
|
+
if item_id != 1:
|
|
67
|
+
raise HTTPException(status_code=404, detail=NOT_FOUND)
|
|
68
|
+
return {"name": tr.gettext("Sample item")}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
A request with `Accept-Language: de` now gets German text, German validation errors and
|
|
72
|
+
`Content-Language: de`.
|
|
73
|
+
|
|
74
|
+
## Translation workflow
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
fastapi-locale extract # scan the code, write locales/messages.pot
|
|
78
|
+
fastapi-locale init --locale hi # start a new language
|
|
79
|
+
fastapi-locale update # merge new messages into every .po file
|
|
80
|
+
fastapi-locale compile # build .mo files
|
|
81
|
+
fastapi-locale check # fail CI when catalogs are stale or broken
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Contributing
|
|
85
|
+
|
|
86
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). The engineering documents (requirements, models, architecture,
|
|
87
|
+
decisions and test plan) are in [docs/](docs/project-documents.md).
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# fastapi-locale
|
|
2
|
+
|
|
3
|
+
Internationalization for [FastAPI](https://fastapi.tiangolo.com), built on standard gettext catalogs.
|
|
4
|
+
|
|
5
|
+
- Picks a locale for every request from a query parameter, cookie or `Accept-Language`, with RFC 4647
|
|
6
|
+
matching and correct `Content-Language` and `Vary` headers.
|
|
7
|
+
- Returns FastAPI's 422 validation errors in the client's language, with correct plural forms.
|
|
8
|
+
- Lazy text that works in Pydantic models, response models and `HTTPException.detail`.
|
|
9
|
+
- Dependency injection first: `LocaleDep` and `TranslatorDep`, replaceable in tests.
|
|
10
|
+
- Lets a dependency switch to the signed-in user's saved language for the rest of the request.
|
|
11
|
+
- A command line tool to extract, update, compile and check catalogs in CI.
|
|
12
|
+
|
|
13
|
+
> **Status:** in development, not yet released. The design is in [docs/](docs/project-documents.md).
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from fastapi import FastAPI, HTTPException
|
|
19
|
+
|
|
20
|
+
from fastapi_locale import LocaleConfig, Localization, TranslatorDep, gettext_lazy
|
|
21
|
+
|
|
22
|
+
i18n = Localization(
|
|
23
|
+
LocaleConfig(default_locale="en", supported_locales=["en", "hi", "de"], catalog_dirs=["locales"])
|
|
24
|
+
)
|
|
25
|
+
app = FastAPI()
|
|
26
|
+
i18n.install(app)
|
|
27
|
+
|
|
28
|
+
NOT_FOUND = gettext_lazy("Item not found")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@app.get("/items/{item_id}")
|
|
32
|
+
def read_item(item_id: int, tr: TranslatorDep) -> dict[str, str]:
|
|
33
|
+
if item_id != 1:
|
|
34
|
+
raise HTTPException(status_code=404, detail=NOT_FOUND)
|
|
35
|
+
return {"name": tr.gettext("Sample item")}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
A request with `Accept-Language: de` now gets German text, German validation errors and
|
|
39
|
+
`Content-Language: de`.
|
|
40
|
+
|
|
41
|
+
## Translation workflow
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
fastapi-locale extract # scan the code, write locales/messages.pot
|
|
45
|
+
fastapi-locale init --locale hi # start a new language
|
|
46
|
+
fastapi-locale update # merge new messages into every .po file
|
|
47
|
+
fastapi-locale compile # build .mo files
|
|
48
|
+
fastapi-locale check # fail CI when catalogs are stale or broken
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Contributing
|
|
52
|
+
|
|
53
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). The engineering documents (requirements, models, architecture,
|
|
54
|
+
decisions and test plan) are in [docs/](docs/project-documents.md).
|
|
55
|
+
|
|
56
|
+
## License
|
|
57
|
+
|
|
58
|
+
MIT
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# ADR-0001: Record architecture decisions
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Date: 2026-09-26
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
The library will be maintained in the open, and contributors will ask why it works the way it does. Those
|
|
9
|
+
answers should not live only in issue threads or in one person's head.
|
|
10
|
+
|
|
11
|
+
## Decision
|
|
12
|
+
|
|
13
|
+
Record each significant decision as a short Markdown file in `docs/adr/`, numbered in order. Each record
|
|
14
|
+
has these sections: Status, Date, Context, Options considered, Decision, Consequences, and Related
|
|
15
|
+
requirements where they apply.
|
|
16
|
+
|
|
17
|
+
Status is one of Proposed, Accepted, Superseded by ADR-NNNN, or Rejected. An accepted record is not edited
|
|
18
|
+
except to change its status; a new record replaces it.
|
|
19
|
+
|
|
20
|
+
## Consequences
|
|
21
|
+
|
|
22
|
+
- Reviewers can check a pull request against the recorded decisions.
|
|
23
|
+
- Changing a decision takes a new record, which makes the change and its reasons visible.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# ADR-0002: Use GNU gettext catalogs loaded through Babel
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Accepted: 2026-09-28
|
|
5
|
+
- Date: 2026-09-26
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
Translations need a storage format that translators already know, that handles plural forms correctly for
|
|
10
|
+
every language, and that has tooling for extracting messages from Python code.
|
|
11
|
+
|
|
12
|
+
## Options considered
|
|
13
|
+
|
|
14
|
+
1. **GNU gettext (`.po` / `.mo`) read with Babel.** The standard in Python (Django, Flask-Babel, CPython
|
|
15
|
+
itself). Supported by Poedit, Weblate, Crowdin, Transifex and Lokalise. Babel adds CLDR plural rules,
|
|
16
|
+
message extraction and compilation.
|
|
17
|
+
2. **gettext through the standard library only.** No extra dependency, but no extraction tooling, no CLDR
|
|
18
|
+
data and no locale formatting later.
|
|
19
|
+
3. **JSON or YAML catalogs.** Easy to read, but every project invents its own plural and context
|
|
20
|
+
conventions, and the translation tools support them unevenly.
|
|
21
|
+
4. **Fluent (Project Fluent).** Expressive, but little adoption in the Python web world and a heavier
|
|
22
|
+
runtime.
|
|
23
|
+
|
|
24
|
+
## Decision
|
|
25
|
+
|
|
26
|
+
Use option 1. Catalogs are `.po` files in the repository, compiled to `.mo` for runtime. Babel is a
|
|
27
|
+
runtime dependency, used for loading `.mo` files and plural rules, and the CLI builds on Babel's
|
|
28
|
+
extraction and compilation.
|
|
29
|
+
|
|
30
|
+
## Consequences
|
|
31
|
+
|
|
32
|
+
- Translators can use the tools they already have (C-01).
|
|
33
|
+
- Plural rules come from CLDR through each catalog's `Plural-Forms` header (TRN-02).
|
|
34
|
+
- Babel becomes a required dependency. It is mature and widely used, so the risk is low.
|
|
35
|
+
- JSON catalogs can still be added later behind the same catalog store interface, as the SRS notes.
|
|
36
|
+
|
|
37
|
+
## Related requirements
|
|
38
|
+
|
|
39
|
+
C-01, C-04, CAT-01, CAT-02, TRN-02, CLI-01, CLI-02
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# ADR-0003: Hold the request locale in a context variable with a per-request holder
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Accepted: 2026-09-28
|
|
5
|
+
- Date: 2026-09-26
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
Code deep inside a request (a service function, a Pydantic serializer, a background task) needs to know
|
|
10
|
+
the request's locale without it being passed through every call. FastAPI runs async code on the event
|
|
11
|
+
loop and sync code in a thread pool, and many requests run at the same time.
|
|
12
|
+
|
|
13
|
+
Experiments with FastAPI 0.141.1 showed:
|
|
14
|
+
|
|
15
|
+
- A value set in a `ContextVar` by pure ASGI middleware is visible in sync dependencies, the endpoint,
|
|
16
|
+
exception handlers and background tasks.
|
|
17
|
+
- A value set with `ContextVar.set()` inside a dependency or a sync endpoint is **not** visible to
|
|
18
|
+
exception handlers or response serialization, because those run in a different context copy.
|
|
19
|
+
|
|
20
|
+
## Options considered
|
|
21
|
+
|
|
22
|
+
1. **Thread-local or global state.** Wrong under asyncio: concurrent requests on one thread overwrite
|
|
23
|
+
each other. Rejected.
|
|
24
|
+
2. **`request.state` only.** Safe, but code without the `Request` object cannot reach it, which rules out
|
|
25
|
+
lazy text and plain `gettext()` calls.
|
|
26
|
+
3. **`ContextVar` holding the locale value.** Safe and reachable everywhere, but changes made in a
|
|
27
|
+
dependency are lost, as the experiment shows.
|
|
28
|
+
4. **`ContextVar` holding a small mutable per-request object (`RequestLocale`).** The middleware creates
|
|
29
|
+
one object per request and sets it once. Later changes modify the object, so every context copy that
|
|
30
|
+
refers to it sees them.
|
|
31
|
+
|
|
32
|
+
## Decision
|
|
33
|
+
|
|
34
|
+
Use option 4. The middleware creates a `RequestLocale`, sets it in a module-level `ContextVar` and resets
|
|
35
|
+
the variable with its token when the request ends. `set_locale()` changes the object in place.
|
|
36
|
+
`use_locale()` sets a new, separate object for the length of a block and restores the previous one with its
|
|
37
|
+
token.
|
|
38
|
+
|
|
39
|
+
The `RequestLocale` is also stored in the ASGI scope so code that has the `Request` can reach it
|
|
40
|
+
explicitly.
|
|
41
|
+
|
|
42
|
+
## Consequences
|
|
43
|
+
|
|
44
|
+
- Correct under asyncio and in the thread pool (C-02, NFR-07).
|
|
45
|
+
- Changes made by the user's dependency reach the 422 handler and the `Content-Language` header
|
|
46
|
+
(see ADR-0004).
|
|
47
|
+
- Tasks started with `asyncio.create_task` inside a request share the object. A change by one task is seen
|
|
48
|
+
by the others. This is the intended behaviour for one request and is documented.
|
|
49
|
+
- Several applications in one process each create their own holders, so they do not interfere (DI-04).
|
|
50
|
+
|
|
51
|
+
## Related requirements
|
|
52
|
+
|
|
53
|
+
C-02, LOC-01, LOC-13, LOC-14, LOC-15, TRN-05, TRN-06, DI-04, NFR-07
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# ADR-0004: Resolve the locale in middleware and let dependencies refine it
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Accepted: 2026-09-28
|
|
5
|
+
- Date: 2026-09-26
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
FastAPI developers expect dependency injection, so resolving the locale in a dependency looks natural.
|
|
10
|
+
But a locale must also be available where no route dependency runs: the 422 validation handler, HTTP
|
|
11
|
+
exception handlers, and the response headers.
|
|
12
|
+
|
|
13
|
+
Many applications also store a preferred language on the user. The user is usually loaded in an auth
|
|
14
|
+
dependency, which runs after any middleware. This was open issue OI-05 in the SRS.
|
|
15
|
+
|
|
16
|
+
Experiments with FastAPI 0.141.1 showed:
|
|
17
|
+
|
|
18
|
+
- A locale set only in an app-level dependency (as fastapi-i18n does) is missing in the 422 handler.
|
|
19
|
+
- FastAPI runs a route's sub-dependencies before it validates the endpoint's own body and parameters. A
|
|
20
|
+
`current_user` dependency that changes the per-request holder (ADR-0003) is therefore seen by the 422
|
|
21
|
+
handler when the body is invalid, and by the middleware when it writes `Content-Language`.
|
|
22
|
+
|
|
23
|
+
## Options considered
|
|
24
|
+
|
|
25
|
+
1. **Dependency only.** Idiomatic, but 422 errors and exception handlers would ignore the locale. Rejected.
|
|
26
|
+
2. **Middleware only.** Always available, but middleware cannot use the user that a dependency loads.
|
|
27
|
+
3. **Middleware resolves, dependencies may refine.** The middleware resolves from the request (query,
|
|
28
|
+
cookie, header). An application dependency may call `set_locale()` to replace it for the rest of the
|
|
29
|
+
request.
|
|
30
|
+
4. **The 422 handler calls an async user hook** to look up the user's language. Duplicates the
|
|
31
|
+
application's auth logic inside an error handler, and runs it twice per request.
|
|
32
|
+
|
|
33
|
+
## Decision
|
|
34
|
+
|
|
35
|
+
Use option 3. `LocaleMiddleware`, a pure ASGI middleware (not `BaseHTTPMiddleware`), resolves the locale
|
|
36
|
+
before routing. Dependencies read it through `LocaleDep` and `TranslatorDep` and can change it with
|
|
37
|
+
`set_locale()`.
|
|
38
|
+
|
|
39
|
+
## Consequences
|
|
40
|
+
|
|
41
|
+
- Localized 422 errors work with no extra setup, and they follow the user's language when the
|
|
42
|
+
application sets it in a dependency.
|
|
43
|
+
- Known limit: if the user dependency's own parameters fail validation (for example a malformed
|
|
44
|
+
`Authorization` header parameter), that dependency does not run and the response uses the locale from the
|
|
45
|
+
request. The user guide will state this.
|
|
46
|
+
- WebSocket connections get the same resolution (LOC-12).
|
|
47
|
+
- Pure ASGI middleware avoids the known context and streaming problems of `BaseHTTPMiddleware`.
|
|
48
|
+
|
|
49
|
+
## Related requirements
|
|
50
|
+
|
|
51
|
+
LOC-01, LOC-10, LOC-11, LOC-12, LOC-13, ERR-01, DI-02, DI-03
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# ADR-0005: Lazy text as a dedicated Pydantic-aware type
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Accepted: 2026-09-28
|
|
5
|
+
- Date: 2026-09-26
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
Text defined at import time (default field values, shared error messages) must be translated later, in
|
|
10
|
+
the locale of the request that sends it. Django calls this `gettext_lazy`.
|
|
11
|
+
|
|
12
|
+
Experiments with FastAPI 0.141.1 and Pydantic 2.13.5 (see [Existing libraries](../research/existing-libraries.md),
|
|
13
|
+
section 6) showed that Babel's `LazyProxy`, which starlette-babel and starlette-i18n use, fails as
|
|
14
|
+
`HTTPException.detail`, in a response model, inside a returned dict, and in OpenAPI generation.
|
|
15
|
+
|
|
16
|
+
## Options considered
|
|
17
|
+
|
|
18
|
+
1. **Babel `LazyProxy`.** Fails in every FastAPI path tested. Rejected.
|
|
19
|
+
2. **A `str` subclass** holding the msgid and overriding `__str__`. JSON encoders read the underlying
|
|
20
|
+
string, so `json.dumps` writes the msgid, untranslated. Rejected.
|
|
21
|
+
3. **A dedicated class with Pydantic core schema hooks.** Serializes through a plain serializer that calls
|
|
22
|
+
`str()`, which translates in the active locale. Tested: works as a model field typed with the class, and
|
|
23
|
+
shows in OpenAPI as `{"type": "string"}`.
|
|
24
|
+
4. **No lazy text**; translate eagerly everywhere. Simple, but import-time text cannot be localized.
|
|
25
|
+
|
|
26
|
+
## Decision
|
|
27
|
+
|
|
28
|
+
Use option 3, a final class `LazyText`:
|
|
29
|
+
|
|
30
|
+
- Created by `gettext_lazy`, `ngettext_lazy`, `pgettext_lazy` and `npgettext_lazy`.
|
|
31
|
+
- `str()` and `format()` translate in the active locale.
|
|
32
|
+
- Pydantic validation accepts `LazyText` or `str`; JSON serialization always produces a string. Python-mode
|
|
33
|
+
`model_dump()` keeps the `LazyText`, so it can be rendered later.
|
|
34
|
+
- JSON schema is `{"type": "string"}`.
|
|
35
|
+
- Registered in `fastapi.encoders.ENCODERS_BY_TYPE` so `jsonable_encoder` renders it.
|
|
36
|
+
- Carries a `__pydantic_serializer__`, so Pydantic also renders it where the declared type is `Any` or
|
|
37
|
+
`object`. FastAPI 0.141 serializes a route annotated `-> dict[str, object]` with Pydantic directly,
|
|
38
|
+
without `jsonable_encoder`, which an integration test found.
|
|
39
|
+
- The library's HTTP exception handler renders `detail` through `jsonable_encoder`, because FastAPI's
|
|
40
|
+
default handler passes `detail` straight to `json.dumps`.
|
|
41
|
+
- Equal when message, plural, context, domain and parameters are equal; hash on message, plural, context
|
|
42
|
+
and domain. Not equal to a plain `str`, because that comparison would depend on the active locale.
|
|
43
|
+
- Supports pickling, so it can be passed to task queues.
|
|
44
|
+
|
|
45
|
+
## Consequences
|
|
46
|
+
|
|
47
|
+
- Works in response models, `HTTPException.detail` and plain dict responses (LZY-01 to LZY-05).
|
|
48
|
+
- Model fields that hold lazy text must be typed `LazyText`, not `str`. Pydantic's error makes the mistake
|
|
49
|
+
obvious, and the user guide covers it.
|
|
50
|
+
- `ENCODERS_BY_TYPE` is not documented FastAPI API. A test fails if FastAPI changes it, so the break is
|
|
51
|
+
caught in CI before a release.
|
|
52
|
+
- Lazy text in OpenAPI metadata is handled separately (ADR-0009).
|
|
53
|
+
|
|
54
|
+
## Related requirements
|
|
55
|
+
|
|
56
|
+
LZY-01 to LZY-06, ERR-09
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# ADR-0006: Key validation messages on the Pydantic error type
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Accepted: 2026-09-28
|
|
5
|
+
- Date: 2026-09-26
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
FastAPI's 422 responses carry Pydantic's English `msg`. pydantic-core 2.46.5 defines 104 error types; 47
|
|
10
|
+
have values in `ctx` such as `gt` or `min_length`. Some English messages build plurals inside `ctx`
|
|
11
|
+
(`string_too_short` uses `{expected_plural}`), which is wrong for most other languages.
|
|
12
|
+
|
|
13
|
+
pydantic-i18n, the only existing package in this area, matches the rendered English `msg` with a regular
|
|
14
|
+
expression. That breaks silently when Pydantic changes its wording.
|
|
15
|
+
|
|
16
|
+
## Options considered
|
|
17
|
+
|
|
18
|
+
1. **Match the English `msg` text.** Fragile across Pydantic releases. Rejected.
|
|
19
|
+
2. **Use Pydantic's own templates as msgids.** Ties catalogs to Pydantic's wording, which changes, and
|
|
20
|
+
keeps the English plural logic.
|
|
21
|
+
3. **Library-owned templates keyed on `type`.** The library keeps one English template per error type,
|
|
22
|
+
with named placeholders that match `ctx` keys, and uses the type as the gettext message context.
|
|
23
|
+
|
|
24
|
+
## Decision
|
|
25
|
+
|
|
26
|
+
Use option 3.
|
|
27
|
+
|
|
28
|
+
- Templates live in the library's own gettext domain, `fastapi_locale`, with `msgctxt` set to the error
|
|
29
|
+
type.
|
|
30
|
+
- Types whose message depends on a number have a plural template and name the `ctx` key that picks the
|
|
31
|
+
form (for example `min_length`). English-only `ctx` keys such as `expected_plural` are ignored.
|
|
32
|
+
- The built-in catalog directory is loaded first. An application overrides any message by shipping its own
|
|
33
|
+
`fastapi_locale` catalog in its catalog directories, because later directories win (CAT-04).
|
|
34
|
+
- An error type with no template keeps Pydantic's `msg`.
|
|
35
|
+
- For `value_error` and `assertion_error`, only the fixed prefix is translated. The rest of the message is
|
|
36
|
+
what the application raised, which it can translate with `gettext()` because validation runs inside the
|
|
37
|
+
request.
|
|
38
|
+
|
|
39
|
+
## Consequences
|
|
40
|
+
|
|
41
|
+
- Catalogs no longer depend on Pydantic's wording (A-02).
|
|
42
|
+
- Plurals follow the target language's rules (ERR-04).
|
|
43
|
+
- The library must follow new error types in pydantic-core. A test lists all error types of the installed
|
|
44
|
+
pydantic-core and fails when one has no template (NFR-09). It uses `list_all_errors`, which is not public
|
|
45
|
+
Pydantic API, so it runs only in the test suite.
|
|
46
|
+
- Known limit: some `ctx` values are already English, for example `literal_error` renders `expected` as
|
|
47
|
+
`'a' or 'b'`. Those values are passed through as they are.
|
|
48
|
+
|
|
49
|
+
## Related requirements
|
|
50
|
+
|
|
51
|
+
ERR-01 to ERR-08, NFR-09
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# ADR-0007: Named placeholder formatting instead of str.format
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Accepted: 2026-09-28
|
|
5
|
+
- Date: 2026-09-26
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
Translated messages contain values such as a name or a count. Translators must be able to move them,
|
|
10
|
+
because word order differs between languages. Catalogs are often edited by outside contributors, so a
|
|
11
|
+
translated string must not be able to run code or read arbitrary data.
|
|
12
|
+
|
|
13
|
+
## Options considered
|
|
14
|
+
|
|
15
|
+
1. **`str.format(**params)`.** A translated string like `{user.__class__}` or `{items[0]}` can read
|
|
16
|
+
attributes and items of the arguments. Rejected for security (NFR-05).
|
|
17
|
+
2. **`%` formatting (`%(name)s`).** Safe from attribute access, but a translation with a missing key raises
|
|
18
|
+
`KeyError`, and the `%` syntax is easy to break when editing.
|
|
19
|
+
3. **Own substitution of `{name}` only.** A small regular expression replaces `{identifier}` with the
|
|
20
|
+
string form of the named value. `{{` and `}}` produce literal braces. Anything else is left as written.
|
|
21
|
+
|
|
22
|
+
## Decision
|
|
23
|
+
|
|
24
|
+
Use option 3. The same syntax is used for application messages and validation error templates, and it
|
|
25
|
+
matches the `{gt}` style of Pydantic's `ctx` keys.
|
|
26
|
+
|
|
27
|
+
If a translation names a placeholder that has no value, the placeholder is left as written and a warning
|
|
28
|
+
is logged once per message and locale. The request never fails because of it (NFR-06).
|
|
29
|
+
|
|
30
|
+
`fastapi-locale check` reports translations whose placeholders differ from the msgid, so these mistakes
|
|
31
|
+
are caught in CI before they reach production.
|
|
32
|
+
|
|
33
|
+
## Consequences
|
|
34
|
+
|
|
35
|
+
- Translators can reorder values freely (TRN-03).
|
|
36
|
+
- Translated strings cannot read attributes or items.
|
|
37
|
+
- Format specifications such as `{price:.2f}` are not supported. Numbers that need locale formatting will
|
|
38
|
+
use the formatting helpers planned in FMT-02.
|
|
39
|
+
|
|
40
|
+
## Related requirements
|
|
41
|
+
|
|
42
|
+
TRN-03, NFR-05, NFR-06, CLI-04
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# ADR-0008: Load catalogs when the application is set up
|
|
2
|
+
|
|
3
|
+
- Status: Accepted
|
|
4
|
+
- Accepted: 2026-09-28
|
|
5
|
+
- Date: 2026-09-26
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
Catalogs must be loaded before the first request, and no file access may happen while a request is being
|
|
10
|
+
handled (C-03). The first draft of the SRS said "at startup (lifespan)".
|
|
11
|
+
|
|
12
|
+
## Options considered
|
|
13
|
+
|
|
14
|
+
1. **In the ASGI lifespan startup event.** Fits the idea of startup, but FastAPI applications set their own
|
|
15
|
+
`lifespan`, and combining two is awkward. Starlette's `TestClient` also skips lifespan unless it is used
|
|
16
|
+
as a context manager, so tests would see unloaded catalogs.
|
|
17
|
+
2. **On the first request.** Adds latency to that request and does file access during request handling.
|
|
18
|
+
Rejected.
|
|
19
|
+
3. **When `Localization` is created.** Catalogs load when the application module builds its
|
|
20
|
+
`Localization(config)`, before the server accepts connections.
|
|
21
|
+
|
|
22
|
+
## Decision
|
|
23
|
+
|
|
24
|
+
Use option 3. Loading is fast (it reads compiled `.mo` files), and a failure raises `CatalogLoadError` at
|
|
25
|
+
import time, so a broken deployment never starts. The SRS requirement CAT-03 is worded to match: catalogs
|
|
26
|
+
are loaded during application setup, before the first request.
|
|
27
|
+
|
|
28
|
+
## Consequences
|
|
29
|
+
|
|
30
|
+
- Works the same with or without lifespan, in tests and in production.
|
|
31
|
+
- Errors appear at import, which is where developers look first.
|
|
32
|
+
- Importing the application module reads files. This is the same as most configuration loading and is
|
|
33
|
+
documented.
|
|
34
|
+
|
|
35
|
+
## Related requirements
|
|
36
|
+
|
|
37
|
+
C-03, CAT-03, CAT-05
|