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

5.5 KiB
Raw Blame History

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

  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 matchen.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.).