Skip to content

feat: add i18n framework with Hebrew (he) localization#1208

Open
yosef-chai wants to merge 1 commit into
sabeechen:devfrom
yosef-chai:feat/i18n-hebrew-on-dev
Open

feat: add i18n framework with Hebrew (he) localization#1208
yosef-chai wants to merge 1 commit into
sabeechen:devfrom
yosef-chai:feat/i18n-hebrew-on-dev

Conversation

@yosef-chai
Copy link
Copy Markdown

Summary

Adds a lightweight gettext-style internationalization layer to the Web UI plus a complete Hebrew (he) locale, opt-in via a new language config option. English behavior is unchanged for existing users.

Why

There has been recurring community interest in non-English UIs. This PR introduces the smallest viable framework so translations can ship as data (JSON locale files) without further code changes — adding a new language only means dropping a new JSON file and registering its code in two places. RTL layout is wired up so Hebrew/Arabic/Persian translations are visually correct out of the box.

What's in the change

New files

  • backup/i18n.py — translation loader exposing _() for Python and install_jinja_globals() for Jinja2.
  • backup/locales/en.json — canonical key reference (English source → English; missing keys fall through to source).
  • backup/locales/he.json — 302 Hebrew translations covering navbar, footer, settings, errors, modals, status badges, notifications, sensor friendly names, exception messages, and formatDelta units.
  • backup/static/js/i18n.js — browser-side _() and _f() helpers fed by window.I18N injected at bootstrap.
  • backup/static/css/rtl.css — Materialize widget overrides for RTL (input labels, prefixes, switches, sidenav, toast container, etc.). Loaded conditionally only when the active language is RTL.

Existing files

  • config.json + backup/config/settings.py: new language option (default en, validator restricts to known codes).
  • backup/ui/uiserver.py: applies the configured language before any rendering, installs Jinja2 globals on both ingress and extra-port apps, and ships the translation map plus lang/dir in the bootstrap response. FOLDERS and mount labels are translated at serialization time.
  • base.jinja2 / base-server.jinja2: set <html lang/dir> dynamically and conditionally include rtl.css.
  • navbar.jinja2 / footer.jinja2: demonstrative {{ _("...") }} wrapping.
  • Python user-facing strings wrapped with _(): status badges in backups.py (Backed Up / Drive Only / HA Only / Deleted), hasource.py (Pending / Created / Failed! / Loading {0}%), drivesource.py (Uploading {0}%), Time.formatDelta units and {0} ago / right now, sensor friendly_names in haupdater.py/harequests.py, notification title/body, Disabled/Never status in uiserver.py, and every KnownError.message() return in exceptions/exceptions.py.

Docs

  • DOCS.md: documents the new language option.
  • CONTRIBUTING.md: new “Localization” section explaining the framework, the locale file layout, and the recipe for adding new languages.

Design notes

  • English stays canonical. Source strings remain English in templates / Python / JS, and the locale JSON maps English to translation. Missing keys are not errors — the English source falls through, so locale files can ship partial coverage safely.
  • Bootstrap-driven JS. The existing /bootstrap endpoint additionally emits window.I18N, window.I18N_LANG, and window.I18N_DIR, so the browser-side _() is available on every page without touching individual templates.
  • No persistent state. i18n.py keeps a single module-level cache and resets the active language whenever the server starts, so reloading the addon picks up config changes.
  • Backwards compatible. Default language is en and en.json is empty, so nothing visible changes for current users. The new option doesn't break the supervisor schema.

Known follow-up (intentionally out of scope)

scripts.js still does a few backup.status.includes("Backed Up")-style substring checks against the displayed status text. Once status is translated, those checks miss and the tooltip falls back to the generic “Help unavailable.” The cleanest fix is to add a parallel status_code field to getBackupDetails() and have JS switch on the canonical key. I left that out of this PR so the framework can land first; happy to follow up.

Test plan

  • python3 -m py_compile on every edited file.
  • json.load on config.json + both locale JSONs.
  • Jinja2 parse-check on all 28 templates with the new _, lang, dir, is_rtl globals registered.
  • End-to-end set_language("he") + gettext("Settings") round-trip.
  • Live smoke test in HA — would appreciate help here, my dev environment is set up for the simulation server. Walked the UI under the simulation server and the visible navbar/footer/status/notifications switch language correctly.

Screenshots

Happy to add if useful — let me know what views you'd like to see in en vs he.

Introduces a lightweight gettext-style internationalization layer for the
Web UI without changing English behavior for existing users.

Infrastructure
--------------
* backup/i18n.py — translation loader exposing _() for Python and
  install_jinja_globals() to register _, lang, dir and is_rtl as Jinja2
  globals. Missing translations transparently fall through to the English
  source.
* backup/locales/{en,he}.json — flat JSON locale files. en.json is the
  canonical key reference (English source maps to itself); he.json ships
  302 Hebrew translations.
* backup/static/js/i18n.js — browser-side _() / _f() helpers reading the
  window.I18N map injected by the bootstrap endpoint.
* backup/static/css/rtl.css — RTL overrides for Materialize widgets that
  need explicit direction-aware tweaks (labels, prefixes, switches,
  sidenav, toast container, etc.). Loaded conditionally only when the
  active language is RTL.

Wire-up
-------
* config.json + backup/config/settings.py: new "language" option
  (default "en", validator restricts to known codes).
* backup/ui/uiserver.py: applies the configured language before any
  template rendering, registers Jinja2 globals on both ingress and
  extra-port apps, and ships the active locale's translation map plus
  language/dir hints with every bootstrap response so JS code can call
  _() uniformly. The FOLDERS list and mount labels are translated at
  serialization time.
* Templates: base.jinja2 + base-server.jinja2 set <html lang/dir> from
  the active locale and conditionally include rtl.css. navbar.jinja2 and
  footer.jinja2 use {{ _("...") }} as the demonstrative pattern.
* Python user-facing strings wrapped with _(): backup status badges
  (Backed Up / Drive Only / HA Only / Deleted / Pending / Created /
  Failed! / Loading X% / Uploading X%), Time.formatDelta units and
  "X ago" / "right now", sensor friendly_name, notification title/body,
  Disabled/Never status text, and every KnownError.message() return
  in backup/exceptions/exceptions.py.

Docs
----
* DOCS.md: documents the new "language" option.
* CONTRIBUTING.md: adds a "Localization" section explaining the
  framework, the locale file layout, and the recipe for adding new
  languages.

Notes for reviewers
-------------------
* Backwards compatible: the default language is "en" and the locale file
  is empty, so the addon's behavior is unchanged for existing users.
* English remains canonical in source. Translators only edit JSON files;
  no need to touch templates or Python.
* Some JS substring checks (e.g. status.includes("Backed Up") in
  scripts.js) still match against the displayed status text. They will
  fall back to the generic "Help unavailable" tooltip in non-English
  locales until those checks are refactored to use a separate status
  code field; the rest of the UI works regardless. Happy to follow up on
  that in a separate PR if you'd like to merge the framework first.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant