Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .codespellrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[codespell]
skip = .git,./tests/e2e/vendor,.venv,.ruff_cache,.mypy_cache,.pytest_cache,uv.lock,htmlcov,*.woff2,*.png,*.ico,*.svg,.superpowers,.serena,.claude,CLAUDE.md,app/static/css/site.css,./app/locales/de.json,./app/locales/fr.json,./app/locales/pt-br.json,./docs/de,./docs/en/docs/hinschg-reference.md,./docs-tech/plans,./docs-tech/legal
skip = .git,./tests/e2e/vendor,.venv,.ruff_cache,.mypy_cache,.pytest_cache,uv.lock,htmlcov,*.woff2,*.png,*.ico,*.svg,.superpowers,.serena,.claude,CLAUDE.md,app/static/css/site.css,./app/locales/de.json,./app/locales/fr.json,./app/locales/es.json,./app/locales/pt-br.json,./docs/de,./docs/en/docs/hinschg-reference.md,./docs-tech/plans,./docs-tech/legal
check-hidden = false
# covert — correct English legal term (secret recording), app/locales/en.json:431
# Tage — German text in changelog example, CHANGELOG.md:809
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- **Spanish (Spain) interface** (`es`): the full locale, in formal *usted*, in the language picker and
chosen automatically for a browser that asks for Spanish; counts group digits with a period. Report categories
fall back to English, as they do for French and Portuguese.

## [2.1.1] — 2026-10-03

### Changed
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ an incident history are fine.

A change that renames a setting, a label or a behaviour updates, in the same commit:

- all four locales in `app/locales/` (`en`, `de`, `fr`, `pt-br`), checked against
- all five locales in `app/locales/` (`en`, `de`, `fr`, `es`, `pt-br`), checked against
[`docs-tech/i18n-review.md`](docs-tech/i18n-review.md);
- `docs/en/docs/index.html`, `README.md` and `docker-compose.prod.yml`;
- the Helm chart (`charts/openwhistle/`) and `ansible/roles/openwhistle/templates/env.j2`.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ zero vendor lock-in, and privacy-first by design.
- **4-eyes deletion** — Hard deletion requires two different admins (request + confirm);
the same admin, or an account one of them made, gets HTTP 409. GDPR Art. 17 compliant.
- **Immutable audit log** — Every admin action recorded with timestamp and username, shown as
readable labels in all four languages; CSV export keeps the machine codes; required by HinSchG §11 Abs. 5.
readable labels in all five languages; CSV export keeps the machine codes; required by HinSchG §11 Abs. 5.
- **Search by case number or content** — Find a case by any part of its number or by a word
in its description or messages. Content is decrypted in memory for that request only; no
searchable index is stored, and the confidential name never matches.
Expand Down Expand Up @@ -111,7 +111,7 @@ zero vendor lock-in, and privacy-first by design.
- **Optional virus scanning** — Attachments can be checked against a ClamAV `clamd` daemon before
they are stored (`CLAMAV_HOST`). Fail-closed: if the scanner is unreachable, the upload is
refused rather than stored unscanned.
- **Internationalisation** — English, German, French and Brazilian Portuguese UI; language
- **Internationalisation** — English, German, French, Spanish and Brazilian Portuguese UI; language
picker in the nav bar; a test keeps every key and placeholder present in every locale.
- **WCAG 2.1 AA** — Skip-to-content link, ARIA labels, live regions, visible focus indicators,
and keyboard-accessible language picker.
Expand Down
2 changes: 1 addition & 1 deletion app/api/reports.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,7 @@ async def set_language(
next_url: str = Form("/submit", alias="next"),
_csrf: None = Depends(validate_csrf),
) -> RedirectResponse:
safe_lang = {"en": "en", "de": "de", "fr": "fr", "pt-br": "pt-br"}.get(lang, "en")
safe_lang = {"en": "en", "de": "de", "fr": "fr", "es": "es", "pt-br": "pt-br"}.get(lang, "en")
parsed = urlsplit(next_url)
safe_path = _NEXT_ALLOWLIST.get(parsed.path)
if safe_path is None and (org_path := _ORG_SUBMIT_RE.fullmatch(parsed.path)):
Expand Down
6 changes: 3 additions & 3 deletions app/i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
from starlette.requests import Request

_LOCALES_DIR = Path(__file__).parent / "locales"
_SUPPORTED = frozenset({"en", "de", "fr", "pt-br"})
_SUPPORTED = frozenset({"en", "de", "fr", "es", "pt-br"})
_DEFAULT = "en"
# Explicit dict lookup severs CodeQL taint flow from user input to file path.
_LANG_MAP: dict[str, str] = {"en": "en", "de": "de", "fr": "fr", "pt-br": "pt-br"}
_LANG_MAP: dict[str, str] = {"en": "en", "de": "de", "fr": "fr", "es": "es", "pt-br": "pt-br"}
# "pt" is treated as an alias for "pt-br" in Accept-Language negotiation.
_LANG_ALIAS: dict[str, str] = {"pt": "pt-br"}

Expand Down Expand Up @@ -54,7 +54,7 @@ def get_lang(request: Request) -> str:
# (a process-global mutation, unsafe for a stateless async app under
# concurrent requests) and not Intl (server-side Python has none) — just the
# one separator character each locale's number convention actually uses.
_THOUSANDS_SEPARATOR: dict[str, str] = {"en": ",", "de": ".", "fr": " ", "pt-br": "."}
_THOUSANDS_SEPARATOR: dict[str, str] = {"en": ",", "de": ".", "fr": " ", "es": ".", "pt-br": "."}


def format_count(n: int, lang: str) -> str:
Expand Down
4 changes: 0 additions & 4 deletions app/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
"nav.submit_report": "Meldung abgeben",
"nav.check_status": "Status prüfen",
"nav.toggle_theme": "Dunkelmodus",
"nav.lang_switch_label_de": "Auf Deutsch wechseln",
"nav.lang_switch_label_en": "Switch to English",
"footer.tagline": "Sicheres Hinweisgebersystem",
"footer.open_source": "Open Source (GPLv3)",
"footer.admin": "Admin",
Expand Down Expand Up @@ -344,8 +342,6 @@
"status.closed.body": "Diese Meldung wurde vollständig untersucht und abgeschlossen. Bitte lesen Sie den Kommunikationsverlauf unten für das Ergebnis und die ergriffenen Maßnahmen.",
"nav.aria_label": "Hauptnavigation",
"nav.lang_picker_label": "Sprache wählen",
"nav.lang_switch_label_fr": "Passer au français",
"nav.lang_switch_label_pt-br": "Auf Portugiesisch (Brasilien) wechseln",
"footer.admin_title": "Anmeldung Meldestelle",
"a11y.skip_to_content": "Zum Hauptinhalt springen",
"submit.progress.label": "Formularfortschritt",
Expand Down
4 changes: 0 additions & 4 deletions app/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
"nav.submit_report": "Submit report",
"nav.check_status": "Check status",
"nav.toggle_theme": "Dark mode",
"nav.lang_switch_label_de": "Auf Deutsch wechseln",
"nav.lang_switch_label_en": "Switch to English",
"footer.tagline": "Secure whistleblower reporting",
"footer.open_source": "Open source (GPLv3)",
"footer.admin": "Admin",
Expand Down Expand Up @@ -344,8 +342,6 @@
"status.closed.body": "This report has been fully investigated and closed. Please review the communication thread below for the outcome and any actions taken.",
"nav.aria_label": "Main navigation",
"nav.lang_picker_label": "Select language",
"nav.lang_switch_label_fr": "Passer au français",
"nav.lang_switch_label_pt-br": "Switch to Portuguese (Brazil)",
"footer.admin_title": "Reporting office login",
"a11y.skip_to_content": "Skip to main content",
"submit.progress.label": "Form progress",
Expand Down
Loading
Loading