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:
Deeman
2026-02-22 00:44:40 +01:00
parent 5e471567b9
commit 4ae00b35d1
235 changed files with 45 additions and 42 deletions

154
docs/I18N.md Normal file
View 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.).