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>
This commit is contained in:
154
docs/I18N.md
Normal file
154
docs/I18N.md
Normal file
@@ -0,0 +1,154 @@
|
||||
# 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.).
|
||||
Reference in New Issue
Block a user