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>
5.5 KiB
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
{{ 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:
"dir_results_count": "Showing {shown} of {total} suppliers"
"dir_results_count" (de): "{shown} von {total} Anbietern"
{{ 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:
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:
{% 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
- Add the key to
en.jsonandde.json(both files, same key). - Use it in the template:
{{ t.your_new_key }}. - 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
- Create
locales/<lang>.jsonwith the same keys asen.json. - Add
"<lang>"toSUPPORTED_LANGSini18n.py. - Add
"<lang>"toLANG_BLUEPRINTSif it should be URL-prefixed (it should). - Add the language switcher link in
base.html. - Update
test_i18n_parity.pyto 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:
- Key sets match —
en.jsonandde.jsonhave identical keys. - No empty values — every key has a non-empty string in both locales.
- No untranslated copy — no key has the same value in EN and DE (allowlist covers proper nouns:
EBITDA,MOIC,IRR,DSCR,Dashboard,RevPAH, etc.).