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 .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ SM_DATABASE_URL=sqlite+aiosqlite:///./app.db
# shipped one of the named locales. Like every SM_ variable it takes precedence
# over the value edited at /admin/settings, so leave it commented unless you
# mean to pin the list for this process.
# SM_I18N_SUPPORTED_LOCALES=["en","es"]
# SM_I18N_SUPPORTED_LOCALES=["en","de","es"]
# SM_I18N_DEFAULT_LOCALE=en

# ── Process identity ────────────────────────────────────────────────────
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ jobs:
# Package metadata, READMEs, release matrix, and the hand-maintained ty
# extra-paths / pytest testpaths lists (#520) must match what is on disk.
- run: make ci-check-metadata
- run: make ci-check-locales
# Backticked repo paths in docs/, CLAUDE.md, README.md and skills must exist (#519).
- run: make ci-doc-paths
# Fixed sleeps in tests are timing bets; poll with simple_module_test.wait_for.
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ All notable changes to this project are documented in this file. The format is b
- `host/client_app` no longer re-declares `@simple-module-py/ui`'s runtime dependencies, nor `@base-ui/react`, `@radix-ui/react-slot`, `autoprefixer` or `postcss` (nothing loaded them) (#521).

### Added
- German catalogues for all framework and module screens, available by adding `de` to `i18n_supported_locales`,
with enforced key and placeholder parity in CI (#492).
- `make ci-doc-paths` (`scripts/check_doc_paths.py`, part of `make lint` and the
`python-lint` job) fails when a backticked repo-rooted path in `docs/`,
`CLAUDE.md`, `README.md` or `skills/**/SKILL.md` does not exist; tutorial
Expand Down
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: install install-py install-js dev dev-api dev-ui build test test-py test-py-pg test-js test-e2e bench memray-run memray-flamegraph loadtest loadtest-seed loadtest-memray bench-nav lint doctor migrate migration downgrade migration-history migrations-roundtrip-pg docker-up docker-down kill new-module gen-pages openapi gen-i18n docker-build docker-app docker-compose-app sync-module-deps ci-python-lint ci-python-typecheck ci-js-lint ci-js-typecheck ci-check-file-size ci-check-test-sleeps ci-check-hardcoded-strings ci-check-untranslated ci-check-metadata ci-doc-paths ci-docs-build ci-i18n-fresh ci-css-build ci-doctor ci-build-packages worker beat worker-docker
.PHONY: install install-py install-js dev dev-api dev-ui build test test-py test-py-pg test-js test-e2e bench memray-run memray-flamegraph loadtest loadtest-seed loadtest-memray bench-nav lint doctor migrate migration downgrade migration-history migrations-roundtrip-pg docker-up docker-down kill new-module gen-pages openapi gen-i18n docker-build docker-app docker-compose-app sync-module-deps ci-python-lint ci-python-typecheck ci-js-lint ci-js-typecheck ci-check-file-size ci-check-test-sleeps ci-check-hardcoded-strings ci-check-untranslated ci-check-metadata ci-check-locales ci-doc-paths ci-docs-build ci-i18n-fresh ci-css-build ci-doctor ci-build-packages worker beat worker-docker

# UV_SYNC_FLAGS is empty locally; CI passes --locked so a stale uv.lock fails the build.
UV_SYNC_FLAGS ?=
Expand Down Expand Up @@ -124,7 +124,7 @@ loadtest: ## Run locust against a server already on $(LOCUST_H
loadtest-memray: ## Start uvicorn under memray, load-test, emit flamegraph
scripts/loadtest_memray.sh $(LOCUST_ARGS)

lint: ci-python-lint ci-python-typecheck ci-js-lint ci-js-typecheck ci-check-metadata ci-doc-paths ci-i18n-fresh ci-check-file-size ci-check-test-sleeps ci-check-hardcoded-strings ci-check-untranslated
lint: ci-python-lint ci-python-typecheck ci-js-lint ci-js-typecheck ci-check-metadata ci-check-locales ci-doc-paths ci-i18n-fresh ci-check-file-size ci-check-test-sleeps ci-check-hardcoded-strings ci-check-untranslated

# Kept granular so pr.yml can run them in parallel.
ci-python-lint:
Expand Down Expand Up @@ -154,6 +154,9 @@ ci-js-typecheck: gen-pages
npx tsc --noEmit -p "$$cfg" || exit 1; \
done

ci-check-locales:
uv run python scripts/check_locales.py

# Package metadata, release matrix, ty extra-paths / pytest testpaths (#520), and READMEs.
ci-check-metadata:
uv run python scripts/check_metadata.py
Expand Down
5 changes: 5 additions & 0 deletions docs/framework/i18n.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

Translations live in each module as JSON files under `<package>/locales/<lang>.json`. They're collected into a single `I18nRegistry` at boot and surfaced to React via the Inertia shared props.

The framework ships English (`en`) and German (`de`) catalogues. Only `en` is
enabled by default; add `de` to `i18n_supported_locales` (Settings screen or
`SM_I18N_SUPPORTED_LOCALES`) to offer German. Both have full key and placeholder parity enforced by `make ci-check-locales`. Other
languages are community-contributed; their differences are reported, not enforced.

## Declaring locale files

```text
Expand Down
2 changes: 2 additions & 0 deletions framework/core/simple_module_core/feature_flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ class FeatureFlagDefinition:
name: str
description: str = ""
default_enabled: bool = False
description_key: str = ""
"""Catalog key for ``description``; empty or unresolved keeps the literal."""


class FeatureFlagRegistry:
Expand Down
13 changes: 9 additions & 4 deletions framework/core/simple_module_core/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ class PermissionGroup:

name: str
permissions: list[str] = field(default_factory=list)
label_key: str = "" # catalog key for ``name``; empty or unresolved keeps the name


class PermissionRegistry:
Expand Down Expand Up @@ -174,12 +175,16 @@ def source_labels(self, name: str) -> dict[str, str]:
"""Human labels supplied by source *name*, keyed by permission string."""
return dict(self._source_output(name)[1]) if name in self._sources else {}

def add_group(self, name: str, permissions: list[str]) -> None:
"""Register a group of related permissions."""
def add_group(self, name: str, permissions: list[str], *, label_key: str = "") -> None:
"""Register a group; ``label_key`` translates ``name`` per request (like menus)."""
if name in self._groups:
self._groups[name].permissions.extend(permissions)
if label_key:
self._groups[name].label_key = label_key
else:
self._groups[name] = PermissionGroup(name=name, permissions=list(permissions))
self._groups[name] = PermissionGroup(
name=name, permissions=list(permissions), label_key=label_key
)
self._invalidate()

def add(self, permission: str) -> None:
Expand Down Expand Up @@ -212,7 +217,7 @@ def all_permissions(self) -> list[str]:
def groups(self) -> list[PermissionGroup]:
"""Static groups plus one group per source (merged by name)."""
merged = {
n: PermissionGroup(name=n, permissions=list(g.permissions))
n: PermissionGroup(name=n, permissions=list(g.permissions), label_key=g.label_key)
for n, g in self._groups.items()
}
for name in self._sources:
Expand Down
27 changes: 27 additions & 0 deletions framework/hosting/simple_module_hosting/locales/de.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"setup": {
"title": "Installation einrichten",
"subtitle": "Noch ein paar Schritte, bevor diese Anwendung einsatzbereit ist.",
"connections": {
"heading": "Verbindungen",
"description": "Die für diese Installation benötigten Dienste werden geprüft.",
"retest": "Erneut testen",
"testing": "Wird getestet…"
},
"migrations": {
"apply": "Migrationen anwenden"
},
"steps": {
"heading": "Einrichtungsschritte",
"migrations": {
"title": "Datenbankmigrationen anwenden",
"description": "Datenbankschema auf die von diesem Code erwartete Version aktualisieren."
}
},
"working": "In Bearbeitung…",
"done": "Fertig. Wird neu geladen…"
},
"errors": {
"token_not_allowed": "API-Tokens können auf dieser Route nicht verwendet werden. Verwenden Sie eine angemeldete Sitzung oder eine Route, deren Berechtigung zum Geltungsbereich des Tokens gehört."
}
}
110 changes: 110 additions & 0 deletions framework/hosting/simple_module_hosting/ui_locales/de.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
{
"errors": {
"generic_title": "Etwas ist schiefgelaufen",
"generic_description": "Auf der Seite ist ein unerwarteter Fehler aufgetreten. Laden Sie sie neu oder kehren Sie zur Startseite zurück.",
"reload_button": "Seite neu laden",
"go_home_button": "Zur Startseite"
},
"nav": {
"admin": "Administration"
},
"switcher": {
"label": "Sprache ändern",
"single_locale": "Nur {locale} ist aktiviert"
},
"topbar": {
"log_out": "Abmelden"
},
"relative_time": {
"unknown": "unbekannt",
"just_now": "gerade eben",
"seconds_ago": "vor {count} Sek.",
"minutes_ago": "vor {count} Min.",
"hours_ago": "vor {count} Std.",
"days_ago": "vor {count} T.",
"months_ago": "vor {count} Mon.",
"months_ago_one": "vor {count} Mon.",
"months_ago_other": "vor {count} Mon.",
"years_ago": "vor {count} J.",
"years_ago_one": "vor {count} J.",
"years_ago_other": "vor {count} J.",
"in_minutes": "in {count} Min.",
"in_hours": "in {count} Std.",
"in_days": "in {count} T.",
"expired": "abgelaufen"
},
"nav_groups": {
"access": "Zugriff",
"appearance": "Darstellung",
"content": "Inhalt",
"system": "System",
"navigation": "Navigation",
"account": "Konto"
},
"command_palette": {
"trigger": "Suchen",
"title": "Suchen",
"description": "Wechseln Sie zu einer Seite, auf die Sie Zugriff haben.",
"placeholder": "Wechseln zu…",
"empty": "Keine Treffer."
},
"admin": {
"panel_badge": "Administrationsbereich",
"back_to_app": "Zurück zur App"
},
"sidebar": {
"open": "Seitenleiste öffnen",
"close": "Seitenleiste schließen",
"drawer": "Navigationsmenü",
"back": "Zurück",
"skip_to_content": "Zum Inhalt springen"
},
"public_nav": {
"docs": "Dokumentation",
"modules": "Module",
"github": "GitHub",
"open_dashboard": "Dashboard öffnen",
"sign_in": "Anmelden",
"open_menu": "Menü öffnen",
"close_menu": "Menü schließen"
},
"demo": {
"banner_read_only": "Demomodus — Sie sind mit einem gemeinsam genutzten Konto angemeldet. Änderungen werden nicht gespeichert.",
"banner_writable": "Demomodus — Sie sind mit einem Konto angemeldet, das alle Besucher dieser Demo gemeinsam nutzen."
},
"error_page": {
"correlation_id_copy": "Referenz-ID kopieren",
"forbidden_description": "Ihrer Rolle fehlt die für diese Seite benötigte Berechtigung. Bitten Sie einen Administrator, sie zu erteilen.",
"forbidden_permission": "Ihrer Rolle fehlt {permission}. Bitten Sie einen Administrator, diese Berechtigung zu erteilen.",
"forbidden_title": "Kein Zugriff",
"generic_description": "Ein unerwarteter Fehler ist aufgetreten.",
"generic_title": "Fehler",
"go_back": "Zurück",
"go_home": "Zur Startseite",
"invalid_request_description": "Die aufgerufene Adresse enthält Parameter, die nicht gelesen werden konnten.",
"invalid_request_title": "Ungültige Anfrage",
"maintenance_description": "Wir nehmen Änderungen vor und sind bald wieder da. Vielen Dank für Ihre Geduld.",
"maintenance_title": "Wartungsarbeiten",
"not_found_description": "Diese Seite oder dieser Datensatz existiert nicht. Möglicherweise wurde er gelöscht.",
"not_found_title": "Nicht gefunden",
"payload_too_large_description": "Die gesendeten Daten überschreiten die vom Server erlaubte Größe. Senden Sie weniger Daten und versuchen Sie es erneut.",
"payload_too_large_title": "Anfrage zu groß",
"rate_limited_description": "Sie haben in kurzer Zeit viele Anfragen gestellt. Warten Sie einen Moment und versuchen Sie es erneut.",
"rate_limited_title": "Zu viele Anfragen",
"retry": "Erneut versuchen",
"server_error_description": "Auf dem Server ist ein Fehler aufgetreten. Geben Sie diese ID an, wenn Sie ihn melden.",
"server_error_title": "Ein Fehler ist aufgetreten",
"session_expired_description": "Sie wurden wegen Inaktivität abgemeldet. Melden Sie sich erneut an, um fortzufahren.",
"session_expired_title": "Sitzung abgelaufen",
"sign_in": "Anmelden",
"unauthorized_description": "Sie müssen angemeldet sein, um diese Seite aufzurufen.",
"unauthorized_title": "Anmeldung erforderlich",
"unavailable_description": "Der Dienst ist vorübergehend nicht verfügbar. Bitte versuchen Sie es in Kürze erneut.",
"unavailable_title": "Dienst nicht verfügbar"
},
"pagination": {
"showing": "{from}–{to} von {total}",
"previous": "Zurück",
"next": "Weiter"
}
}
55 changes: 55 additions & 0 deletions host/locales/de.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"admin": {
"title": "Administration",
"description": "Benutzer, Zugriff, Darstellung und Systemeinstellungen verwalten.",
"empty": "Für Ihr Konto sind keine Administrationswerkzeuge verfügbar.",
"overview": "Übersicht"
},
"landing": {
"badge": "✦ FastAPI + Inertia als Komplettpaket",
"hero_title_line1": "Modulare Python-Apps,",
"hero_title_line2": "aus einem Baukasten",
"hero_subtitle": "Jede Funktion ist ein eigenständiges Modul mit eigenen Routen, Migrationen, Berechtigungen und Inertia-Ansichten. Fügen Sie es hinzu, und der Host bindet es beim Start ein.",
"cta_dashboard": "Dashboard öffnen",
"cta_get_started": "Projekt erstellen",
"cta_docs": "Dokumentation lesen",
"features": {
"module_system_title": "Modulsystem",
"module_system_description": "Legen Sie ein Paket in modules/ ab. Seine URLs, Migrationen und Menüeinträge werden automatisch registriert.",
"auth_title": "Authentifizierung inklusive",
"auth_description": "E-Mail- und Cookie-Sitzungen, Einladungen, Passwortzurücksetzung und optionales Keycloak SSO sind sofort verfügbar.",
"schema_title": "Schema zuerst",
"schema_description": "Jedes Modul deklariert seine Modelle, Routen und Berechtigungen zentral. Der Host liest diese Deklaration beim Start.",
"inertia_title": "Inertia-Ansichten",
"inertia_description": "React-Seiten direkt aus Python-Views. Keine separate API-Schicht, die synchron gehalten werden muss.",
"diagnostics_title": "Diagnose",
"diagnostics_description": "Jedes Modul kann Zustandsprüfungen registrieren. make doctor und die Doctor-Seite zeigen alle Ergebnisse.",
"devtools_title": "Entwicklungswerkzeuge",
"devtools_description": "make new-module erstellt ein funktionsfähiges Modul mit Tests und Seitengerüst."
},
"head_title": "Willkommen",
"copy_command": "Befehl kopieren",
"command_copied": "Kopiert",
"how_it_works_eyebrow": "So funktioniert es",
"how_it_works_heading": "Ein Prozess · viele Module · alles verbunden.",
"quickstart_eyebrow": "Schnellstart",
"quickstart_heading": "Funktionsfähige App in fünf Befehlen.",
"quickstart_body_prefix": "Starten Sie auf",
"quickstart_body_suffix": "mit bereits eingebundenen Benutzern, Dashboard und Berechtigungen. Melden Sie sich mit dem bei der Einrichtung erstellten Administratorkonto an.",
"module_users_description": "E-Mail- und Cookie-Sitzungen über fastapi-users",
"module_dashboard_description": "Startseite für angemeldete Benutzer mit Modulkacheln",
"module_permissions_description": "Berechtigungsregister pro Modul",
"cta_heading": "Läuft die App bereits?",
"cta_github": "GitHub →",
"copy_label": "Kopieren",
"terminal_helper": "Für den lokalen Betrieb ist kein Konto nötig. Die Anmeldung wird nur für die gehostete Administrationsoberfläche benötigt.",
"cta_body": "Öffnen Sie die Administrationsoberfläche, um Benutzer, Module und Hintergrundaufgaben zu verwalten.",
"cta_sign_in": "An der Administrationsoberfläche anmelden",
"copied_label": "✓ Kopiert"
},
"offline": {
"title": "Sie sind offline",
"description": "Änderungen werden möglicherweise erst gespeichert, wenn die Verbindung wiederhergestellt ist.",
"restored": "Wieder online"
}
}
Loading
Loading