Files
padelnomics/docs/I18N.md
Deeman 4ae00b35d1 refactor: flatten padelnomics/padelnomics/ → repo root
git mv all tracked files from the nested padelnomics/ workspace
directory to the git repo root. Merged .gitignore files.
No code changes — pure path rename.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 00:44:40 +01:00

155 lines
5.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# i18n
How internationalisation works in padelnomics: locale files, language routing, template usage, and how to add new keys or languages.
---
## Architecture
```
src/padelnomics/
├── i18n.py # Loader + get_translations() + get_calc_item_names()
├── locales/
│ ├── en.json # 1222 keys, flat key→value
│ └── de.json # 1222 keys, must match en.json exactly
└── app.py # registers tformat filter + t/lang in template context
```
Both JSON files are loaded at import time. A key-parity assertion runs on startup — mismatched keys crash the process immediately rather than serving partially translated pages.
---
## Language routing
Lang-prefixed blueprints (`public`, `planner`, `directory`, `leads`, `suppliers`, `content`) all mount under `/<lang>/`. The `<lang>` segment is validated in `before_request`; unsupported values get a 404.
Blueprints without a lang prefix (`auth`, `dashboard`, `billing`, `admin`) detect language from a cookie (`lang`) → `Accept-Language` header → fallback `"en"`.
`g.lang` is set on every request and used by `get_translations(g.lang)` to populate the `t` context variable available in all templates.
---
## Template usage
### Basic lookup
```jinja2
{{ t.nav_planner }}
{{ t.landing_faq_q1 }}
```
`t` is a plain dict; `t.key` and `t["key"]` are equivalent in Jinja2.
### Parameterised strings
Use the `tformat` filter for strings with runtime values. The JSON value uses `{named_placeholder}` syntax:
```json
"dir_results_count": "Showing {shown} of {total} suppliers"
"dir_results_count" (de): "{shown} von {total} Anbietern"
```
```jinja2
{{ t.dir_results_count | tformat(shown=suppliers|length, total=total_count) }}
```
### JS-embedded strings
Server-render translation values into `<script>` blocks using `| tojson` to get a properly quoted JS string literal:
```jinja2
btn.textContent = {{ t.sup_step4_checkout | tojson }};
var loadingText = {{ t.sup_step4_loading | tojson }};
```
### Looping over a key list
Feature lists and similar repeated items use a key list rather than a parallel translated list:
```jinja2
{% for key in plan.feature_keys %}
<li>{{ t[key] }}</li>
{% endfor %}
```
---
## Key naming conventions
Keys are `snake_case` with a module prefix. Suffixes hint at the UI role.
| Prefix | Module / area | Count |
|--------|---------------|-------|
| `sup_` | Supplier signup wizard, dashboard | 238 |
| `dir_` | Directory (listing, filters, enquiry) | 83 |
| `landing_` | Landing page (hero, journey, FAQ, SEO) | 78 |
| `planner_` | Financial planner (steps, sections, charts) | 76 |
| `tip_` | Tooltip copy | 70 |
| `sl_` | Slider labels in planner inputs | 60 |
| `suppliers_` | Supplier-facing static copy | 51 |
| `scenario_` | Scenario names, labels | 50 |
| `auth_` | Auth pages (login, signup, verify) | 42 |
| `features_` | Features page | 26 |
| `export_` | PDF export page | 23 |
| `about_` | About page | 13 |
| `nav_` | Navigation | 12 |
| `bp_` | Business plan PDF section headings | 9 |
| `plan_` | Supplier plan feature strings | 16 |
| `wf_` | Waterfall table row labels | 9 |
| `q*_` / `qs_` / `qv_` | Quote wizard steps | ~115 |
Common suffixes: `_title`, `_h1`, `_h2`, `_h3`, `_subtitle`, `_desc`, `_label`, `_btn`, `_hint`, `_body`, `_p`, `_item`.
---
## Two translation namespaces
**UI strings** (`locales/en.json`, `locales/de.json`) — everything rendered in templates. Loaded via `get_translations(lang)`.
**Calculator item names** (`_CALC_ITEM_NAMES` in `i18n.py`) — CAPEX/OPEX line item display names used only by `calculator.py`. Kept inline (~36 keys × 2 langs) because they belong to a different domain and are never referenced from templates.
Do not mix the two: `get_translations()` returns UI strings, `get_calc_item_names()` returns calc item names.
---
## Adding keys
1. Add the key to `en.json` and `de.json` (both files, same key).
2. Use it in the template: `{{ t.your_new_key }}`.
3. Run `uv run pytest tests/test_i18n_parity.py` — verifies key sets match, all values non-empty, and no key has identical EN/DE copy (with an allowlist for proper nouns like EBITDA, Dashboard).
The startup assertion also catches mismatches immediately on next server boot.
---
## Adding a language
1. Create `locales/<lang>.json` with the same keys as `en.json`.
2. Add `"<lang>"` to `SUPPORTED_LANGS` in `i18n.py`.
3. Add `"<lang>"` to `LANG_BLUEPRINTS` if it should be URL-prefixed (it should).
4. Add the language switcher link in `base.html`.
5. Update `test_i18n_parity.py` to include the new locale in its parity checks.
---
## Not yet translated
These areas have no `{% if lang %}` blocks and serve English copy only. They need translation work but were out of scope for the initial i18n foundation:
- `/dashboard/` — user scenario list
- `/billing/` — billing webhooks and receipts
- Supplier dashboard tabs (overview, leads, listing, boosts)
- Business plan PDF — section headings are translated via `bp_*` keys, but formatted values within (payback period strings, metric labels in the PDF template) remain English
**Admin panel** (`/admin/`) is intentionally English-only — it's an internal tool, not user-facing.
---
## Parity test
`tests/test_i18n_parity.py` runs three checks:
1. **Key sets match**`en.json` and `de.json` have identical keys.
2. **No empty values** — every key has a non-empty string in both locales.
3. **No untranslated copy** — no key has the same value in EN and DE (allowlist covers proper nouns: `EBITDA`, `MOIC`, `IRR`, `DSCR`, `Dashboard`, `RevPAH`, etc.).