feat(plugins): localized plugin descriptions & option labels - #444
Merged
Conversation
Plugin descriptions and option labels are authored in each plugin's
manifest.toml (store descriptions in registry.json), outside the app's
i18next files — so t() could never reach them and they stayed English
regardless of the app language.
Make the format itself carry the translations: plugin.description, each
option's label/description and a registry entry's description now accept
either a plain string or a { lang -> text } map (LocalizedString, serde
untagged). Plain strings keep parsing byte-identically, so no published
plugin needs a change.
The host echoes the value through untouched and the UI resolves it
against the active i18next language via useLocalizedText, so a language
switch re-renders instantly with no backend round-trip. The fallback
chain (exact code -> base language -> en -> any entry) is implemented in
LocalizedString::resolve and mirrored in resolveLocalizedText.
A localized field declaring zero languages is refused at parse time
rather than rendering a blank row.
Refs #440
Claude-Session: https://claude.ai/code/session_0153GyoLLYHdHHmqqBHqoXYN
The inline `{ lang -> text }` table added in the previous commit is not
safe to publish: a WaveFlow predating it expects a string and hard-errors
on the table. In a manifest that drops the whole plugin; in registry.json
— one document every installed version fetches — the catalogue fails to
decode from all three sources, so the store goes dark on that build.
min_app_version can't rescue it, being read after the failed decode.
Add a sibling field (`description_i18n`, `label_i18n`) carrying the
translations next to an untouched plain string, folded into the base
field at parse time (plain string takes the `en` slot, sibling wins per
key). An older host ignores the unknown field and keeps rendering
English; a current one renders the translation. No version bump, no
broken store, nobody loses access to a plugin.
The inline form stays supported for manifests that never reach an older
host, and the docs now say plainly which is which.
Refs #440
Claude-Session: https://claude.ai/code/session_0153GyoLLYHdHHmqqBHqoXYN
📝 WalkthroughWalkthroughLe format des manifests et du catalogue accepte les textes localisés. Le backend fusionne les variantes publiables, tandis que le frontend applique le fallback selon la langue active pour les descriptions, libellés et options de plugins. ChangesLocalisation des plugins
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Manifest
participant Backend
participant Frontend
participant PluginUI
Manifest->>Backend: fournit les champs localisés et *_i18n
Backend->>Backend: fusionne description et description_i18n
Backend->>Frontend: expose les valeurs localisées
Frontend->>Frontend: résout selon la langue active et le fallback
Frontend->>PluginUI: transmet le texte affichable
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src-tauri/crates/app/src/commands/plugin_store.rs`:
- Line 405: Ajouter un test d’intégration du registre couvrant la construction
de MarketplaceEntry autour de la fusion description/description_i18n dans
l’expression merged_with. Vérifier qu’un manifeste utilisant la valeur de
description localisée produit bien la description attendue dans
MarketplaceEntry, en préservant le comportement de compatibilité avec les
anciens hôtes.
In `@src/lib/localizedText.ts`:
- Around line 40-50: Update the localized translation fallback around the exact
and sorted-key lookups to treat empty or whitespace-only strings as unavailable.
Continue through language fallbacks and deterministic keys until a non-blank
translation is found, and return null when none is usable so callers can apply
their own fallback.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 515a9e35-0d1f-4f99-8e0b-a0c856353257
📒 Files selected for processing (13)
CLAUDE.mddocs/features/plugins.mdsrc-tauri/crates/app/src/commands/plugin_store.rssrc-tauri/crates/app/src/commands/plugins.rssrc-tauri/crates/core/src/plugin/assets.rssrc-tauri/crates/core/src/plugin/host_impl.rssrc-tauri/crates/core/src/plugin/manifest.rssrc/components/views/settings/PluginOptions.tsxsrc/components/views/settings/PluginStoreCard.tsxsrc/components/views/settings/PluginsCard.tsxsrc/hooks/useLocalizedText.tssrc/lib/localizedText.tssrc/lib/tauri/plugins.ts
`??` treats "" as a hit, so `{ fr: "", en: "…" }` resolved to the empty
string instead of falling back to English. On an option label that also
defeats the `?? option.key` fallback and leaves the control with no
accessible name — an empty aria-label.
Skip blank (and whitespace-only) entries at every step of the chain, on
both sides of the contract, and return None when nothing renderable is
left so callers can substitute their own label. An untranslated slot left
empty is a common authoring accident; it shouldn't outrank a real string.
Also add tests at the registry -> MarketplaceEntry seam: the sibling
merge, an entry without a sibling (every published entry today, and the
only form older clients read), and one with nothing renderable.
Both from CodeRabbit review on #444.
Refs #440
Claude-Session: https://claude.ai/code/session_0153GyoLLYHdHHmqqBHqoXYN
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.
Closes #440.
Plugin descriptions and option labels are authored in each plugin's
manifest.toml(store descriptions inregistry.json), outside the app's i18next files — sot()could never reach them and they stayed English in all 17 languages while the chrome around them was translated.Approach
The format itself carries the translations.
plugin.description, each option'slabel/descriptionand a registry entry'sdescriptionbecome aLocalizedString: a plain string (unchanged) or a{ lang -> text }map.Resolution is frontend-side. The host hands the value through untouched and the UI resolves it against
i18next.languageviauseLocalizedText(), so a language switch re-renders instantly with no backend round-trip. Fallback chain: exact code → base language (pt-BR→pt) →en→ any entry.The part the issue didn't anticipate
The issue proposed replacing the string with the map. That form is not safe to publish, and I kept it out of the publishing path:
registry.json, which is one document every installed version fetches, it fails the catalogue decode across all three delivery sources, so the store goes dark on that build — for every plugin, not just the one that changed.min_app_versioncan't rescue it: it is read after the decode that already failed.So anything published carries its translations in a sibling
*_i18nfield (description_i18n,label_i18n), folded into the base field at parse time with the plain string taking theenslot. Old hosts ignore the unknown field and render English; new ones render the translation. No version bump, nobody loses access to a plugin. The inline map form stays supported for manifests that never reach an older host, and the docs say plainly which is which.Verification
cargo check --workspace --all-targets,cargo test --workspace,bun run typecheck,bun run lint, prettier — all clean (clippy isn't available on my machine and isn't in CI either, so it was not run)plugin::manifesttests + 3 newplugin_storetests at the registry →MarketplaceEntryseam. Covered: plain strings unchanged, inline form, sibling merge, sibling-wins-per-key, fallback chain, blank entries skipped, empty language map rejected at parseDownstream (paired, mergeable independently)
description_i18nfor the store card in 16 locales, schema + CONTRIBUTING rulesprefer_hevclabel/descriptionBoth are inert for users until this ships, and harmless to older clients before and after.
Not in scope
plugin.name(brand token, and the issue scopes it out) and the runtimelabels asourceplugin returns for its catalogue entries ("Top stations") — those come from the guest wasm, not the manifest, and would need a host-side locale import.Summary by CodeRabbit