feat(web): localise extension nav-tab labels via optional display_name_i18n#129
Merged
Conversation
…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
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Built-in nav tabs (Setup / Demo / BYOD / Danger Zone) are already translated via
data-i18nkeys ini18n.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 optionaldisplay_name_i18n: Mapping[str, str]class attribute that the renderer prefers per-locale, falling back todisplay_namefor any locale (or extension) that has not opted in.Lookup priority:
display_name_i18n[active_locale]→display_name_i18n["en"]→display_name. The renderer listens for the existingmureo:locale_changedevent (fired byapp.js#setLocale) so the nav label flips the moment the operator toggles locale.Public surface
mureo.web.extensions.WebExtensionEntrygains adisplay_name_i18n: Mapping[str, str]field withfield(default_factory=dict). Existing 5-field constructors keep working unchanged.WebExtensionProtocol is unchanged — the new attribute is read viagetattrinside the discovery layer. Adding it to a@runtime_checkableProtocol would have brokenisinstance(...)for every pre-feature extension.GET /api/extensionsgains adisplay_name_i18nkey per entry (empty{}for extensions that did not declare any).mureo/_data/web/extensions.jsgains_resolveDisplayName(extension, locale)+ amureo:locale_changedlistener.Validation
Discovery treats the value as
Mapping[str, str]and skips the entry with aWebExtensionWarningif anything else turns up: explicitNone, list of pairs,intkeys,intvalues. The JSON serialiser never sees a malformed shape.Security
Labels are injected via
textContent(neverinnerHTML), so an i18n value containing<script>is harmless DOM text. The configure-UI CSP (script-src 'self'; style-src 'self') is unchanged.Backward compatibility
display_name_i18nget an emptydictin theirWebExtensionEntry. The renderer's fallback chain resolves todisplay_name, so the nav tab looks byte-identical to v0.9.5./api/extensionsfield is additive JSON; existing consumers ignore unknown keys.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 OSSi18n.json" guarantee.Test plan
ruff check mureo/,black --check mureo/) — clean locallytests/test_web_*eval/Function(...)/ subprocess / shellmureo configurewith an extension that declaresdisplay_name_i18n→ toggleEnglish ⇄ 日本語→ confirm the extension nav tab updates without a page reloaddisplay_name_i18ncontinues to render its Englishdisplay_namein both locales (byte-identical to v0.9.5)