Skip to content

feat(web): localise extension nav-tab labels via optional display_name_i18n#129

Merged
hyoshi merged 1 commit into
mainfrom
feat/web-extensions-i18n-nav
May 22, 2026
Merged

feat(web): localise extension nav-tab labels via optional display_name_i18n#129
hyoshi merged 1 commit into
mainfrom
feat/web-extensions-i18n-nav

Conversation

@hyoshi
Copy link
Copy Markdown
Collaborator

@hyoshi hyoshi commented May 22, 2026

Summary

Built-in nav tabs (Setup / Demo / BYOD / Danger Zone) are already translated via data-i18n keys in i18n.json, but third-party web-extension tabs were stuck on a single English string. Operators toggling 日本語 saw the rest of the UI flip while extension tabs stayed put. This PR closes that consistency gap by letting an extension declare an optional display_name_i18n: Mapping[str, str] class attribute that the renderer prefers per-locale, falling back to display_name for any locale (or extension) that has not opted in.

class MyExtension:
    name = "acme-vault"
    display_name = "Acme Vault setup"            # fallback
    display_name_i18n = {"en": "Vault", "ja": "Vault 設定"}
    # ...routes() / view() unchanged

Lookup priority: display_name_i18n[active_locale]display_name_i18n["en"]display_name. The renderer listens for the existing mureo:locale_changed event (fired by app.js#setLocale) so the nav label flips the moment the operator toggles locale.

Public surface

  • mureo.web.extensions.WebExtensionEntry gains a display_name_i18n: Mapping[str, str] field with field(default_factory=dict). Existing 5-field constructors keep working unchanged.
  • The WebExtension Protocol is unchanged — the new attribute is read via getattr inside the discovery layer. Adding it to a @runtime_checkable Protocol would have broken isinstance(...) for every pre-feature extension.
  • GET /api/extensions gains a display_name_i18n key per entry (empty {} for extensions that did not declare any).
  • mureo/_data/web/extensions.js gains _resolveDisplayName(extension, locale) + a mureo:locale_changed listener.

Validation

Discovery treats the value as Mapping[str, str] and skips the entry with a WebExtensionWarning if anything else turns up: explicit None, list of pairs, int keys, int values. The JSON serialiser never sees a malformed shape.

Security

Labels are injected via textContent (never innerHTML), so an i18n value containing <script> is harmless DOM text. The configure-UI CSP (script-src 'self'; style-src 'self') is unchanged.

Backward compatibility

  • Extensions that do not declare display_name_i18n get an empty dict in their WebExtensionEntry. The renderer's fallback chain resolves to display_name, so the nav tab looks byte-identical to v0.9.5.
  • The new /api/extensions field is additive JSON; existing consumers ignore unknown keys.
  • No change to i18n.json — extension authors ship their own labels without touching the OSS catalog.

Docs

docs/plugin-authoring.md §13 gains a Localising the nav-tab label subsection with the example class attribute, the documented lookup priority, the empty-string fallthrough note, and the "no need to touch OSS i18n.json" guarantee.

Test plan

  • CI lint (ruff check mureo/, black --check mureo/) — clean locally
  • CI tests (Python 3.10 / 3.11 / 3.12 + Windows) — 9 new unit tests + 1 new integration test + 2 existing assertions updated; 80 tests pass in the two extension test files, 252 pass across tests/test_web_*
  • CodeQL — no new findings expected; net additions are pure-Python + plain JS, no eval / Function(...) / subprocess / shell
  • Manual: launch mureo configure with an extension that declares display_name_i18n → toggle English ⇄ 日本語 → confirm the extension nav tab updates without a page reload
  • Manual: an extension that does NOT declare display_name_i18n continues to render its English display_name in both locales (byte-identical to v0.9.5)

…e_i18n

Built-in nav tabs (Setup / Demo / BYOD / Danger Zone) are already
translated by `applyTranslations(document)` via `data-i18n` keys in
`i18n.json`, but third-party web-extension tabs were stuck on a
single English string. Operators who toggled 日本語 saw the rest of
the UI flip while extension tabs stayed put — inconsistent UX, and
extension authors had no clean way to participate in the existing
locale flow.

This commit closes the gap by letting an extension declare an
optional class attribute:

    class MyExtension:
        name = "acme-vault"
        display_name = "Acme Vault setup"            # fallback
        display_name_i18n = {"en": "Vault", "ja": "Vault 設定"}

The renderer picks `display_name_i18n[active_locale]` first, then
`display_name_i18n["en"]`, then the legacy `display_name`. A
`mureo:locale_changed` listener (already fired by `app.js#setLocale`)
re-runs the lookup so every nav label updates the moment the
operator toggles locale.

Backward compatibility:

* `WebExtension` Protocol is unchanged — the new attribute is read
  defensively via `getattr` so every pre-feature extension keeps
  loading as-is. Adding it to the Protocol would have broken
  `@runtime_checkable` isinstance() for existing implementations.
* `WebExtensionEntry` gains a `display_name_i18n: Mapping[str, str]`
  field with `field(default_factory=dict)` so existing 5-field
  constructors (notably in the test fixtures) continue to work.
* `GET /api/extensions` adds a `display_name_i18n` key per entry
  (empty `{}` for extensions that did not declare any). JSON-only
  addition; existing consumers ignore unknown keys.
* The renderer's fallback chain resolves to `display_name` when the
  map is empty, so the nav tab looks byte-identical to v0.9.5 for
  every extension that has not opted in.

Validation: discovery treats the value as `Mapping[str, str]` and
skips the entry with a `WebExtensionWarning` if anything else turns
up — explicit `None`, list of pairs, `int` keys, `int` values — so
the JSON serialiser never sees a malformed shape.

Security: labels are injected via `textContent` (never `innerHTML`),
so a label containing `<script>` or similar is harmless DOM text.
The configure-UI CSP (`script-src 'self'; style-src 'self'`) is
unchanged.

Tests: 9 new unit tests in `tests/test_web_extensions.py`
(`WebExtensionEntry` default + accept, discovery picks up i18n,
discovery defaults missing attr to empty, discovery skips non-Mapping
/ non-str key / non-str value / None) + 1 new integration test in
`tests/test_web_extension_routing.py` (`/api/extensions` returns
`display_name_i18n`) + 2 existing assertions updated to lock in the
empty-`{}` default in the JSON envelope. 80 tests pass in the two
extension test files; 252 tests pass across the wider `tests/test_web_*`
sweep. ruff and black clean on `mureo/`.

Docs: `docs/plugin-authoring.md` §13 gains a *Localising the nav-tab
label* subsection with the example class, the documented lookup
priority, the empty-string fallthrough note, and the "no need to
touch OSS i18n.json" guarantee.
@hyoshi hyoshi marked this pull request as ready for review May 22, 2026 02:10
@hyoshi hyoshi merged commit e319b2a into main May 22, 2026
9 checks passed
@hyoshi hyoshi deleted the feat/web-extensions-i18n-nav branch May 22, 2026 02:10
@hyoshi hyoshi mentioned this pull request May 22, 2026
3 tasks
hyoshi added a commit that referenced this pull request May 22, 2026
… nav tabs (#129) (#130)

See CHANGELOG.md for details.

Web extensions can now ship an optional `display_name_i18n: Mapping[str, str]`
class attribute alongside `display_name` so the configure-UI nav tab
follows the active locale (en / ja). The renderer's lookup priority is
`display_name_i18n[active_locale]` → `display_name_i18n["en"]` →
`display_name`; the existing `mureo:locale_changed` event flips every
extension label without a page reload.

Backward compatibility: `WebExtension` Protocol is unchanged
(`getattr`-based defensive read), `WebExtensionEntry` defaults the new
field to `{}`, and extensions that do not declare any localized
labels render byte-identical to v0.9.5.

Version bumped to 0.9.6 across .claude-plugin/plugin.json,
mureo/__init__.py, and pyproject.toml.
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