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