Skip to content

feat(plugins): localized plugin descriptions & option labels - #444

Merged
InstaZDLL merged 3 commits into
mainfrom
feat/localized-plugin-manifest-strings
Jul 27, 2026
Merged

feat(plugins): localized plugin descriptions & option labels#444
InstaZDLL merged 3 commits into
mainfrom
feat/localized-plugin-manifest-strings

Conversation

@InstaZDLL

@InstaZDLL InstaZDLL commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Closes #440.

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 in all 17 languages while the chrome around them was translated.

Approach

The format itself carries the translations. plugin.description, each option's label / description and a registry entry's description become a LocalizedString: 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.language via useLocalizedText(), so a language switch re-renders instantly with no backend round-trip. Fallback chain: exact code → base language (pt-BRpt) → 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:

  • a host predating this feature expects a string and hard-errors on a table — in a manifest it drops the plugin wholesale;
  • in 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_version can't rescue it: it is read after the decode that already failed.

So anything published carries its translations in a sibling *_i18n field (description_i18n, label_i18n), folded into the base field at parse time with the plain string taking the en slot. 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)
  • 17/17 plugin::manifest tests + 3 new plugin_store tests at the registry → MarketplaceEntry seam. Covered: plain strings unchanged, inline form, sibling merge, sibling-wins-per-key, fallback chain, blank entries skipped, empty language map rejected at parse
  • Replayed the v1.7.0 wire structs against the real downstream files (see the paired PRs) — the old manifest + registry structs still decode them and render English, which is the property the whole design rests on

Downstream (paired, mergeable independently)

  • InstaZDLL/waveflow-plugins — description_i18n for the store card in 16 locales, schema + CONTRIBUTING rules
  • InstaZDLL/waveflow-plugin-apple-artwork — translated description + prefer_hevc label/description

Both 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 runtime labels a source plugin 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

  • Nouvelles fonctionnalités
    • Prise en charge des chaînes localisées pour les descriptions, libellés et options des plugins (formats “publishable” via champs dédiés).
    • Résolution automatique selon la langue active avec mécanisme de fallback (langue exacte → langue de base → anglais → première valeur disponible).
    • Affichage localisé dans le store marketplace et l’écran de paramètres des plugins.
  • Documentation
    • Mise à jour des guides pour définir les formats de manifest et de catalogue, les règles de fusion et la compatibilité.
  • Correctifs
    • Gestion renforcée des cas limites (maps vides, champs incompatibles) pour éviter les dégradations d’affichage.

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
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Le 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.

Changes

Localisation des plugins

Layer / File(s) Summary
Contrat et parsing des manifests
src-tauri/crates/core/src/plugin/manifest.rs, src-tauri/crates/core/src/plugin/assets.rs, src-tauri/crates/core/src/plugin/host_impl.rs
LocalizedString accepte les chaînes simples et les cartes de langues. Les champs *_i18n sont fusionnés avant validation, avec fallback et rejet des cartes vides.
Catalogue et contrats backend
src-tauri/crates/app/src/commands/plugin_store.rs, src-tauri/crates/app/src/commands/plugins.rs
Les descriptions du registre, du marketplace et des plugins sont exposées sous forme localisée, avec fusion de description_i18n.
Résolution et types frontend
src/lib/localizedText.ts, src/hooks/useLocalizedText.ts, src/lib/tauri/plugins.ts
Le frontend définit LocalizedText, normalise la langue active et applique le fallback exact, langue de base, anglais, puis première valeur triée.
Affichage et documentation
src/components/views/settings/PluginOptions.tsx, src/components/views/settings/PluginStoreCard.tsx, src/components/views/settings/PluginsCard.tsx, docs/features/plugins.md, CLAUDE.md
Les descriptions et options sont résolues avant affichage, avec repli du libellé sur la clé et documentation du format publiable.

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
Loading

Possibly related PRs

  • InstaZDLL/WaveFlow#224 — introduit l’affichage des descriptions dans PluginsCard, ensuite adapté pour la résolution localisée.
  • InstaZDLL/WaveFlow#351 — concerne l’intégration du marketplace et de ses descriptions.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning La description couvre le fond, mais elle ne respecte pas le template requis: Summary, How I tested, Checklist et Screenshots ne sont pas renseignés comme attendu. Reformatez la description selon le template avec 1–3 bullets de résumé, des étapes de test concrètes, la checklist, et des captures si l’UI a changé.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Les changements implémentent #440: strings localisées pour manifest, options et store, résolution via la langue active et compatibilité ascendante.
Out of Scope Changes check ✅ Passed Je ne vois pas de changement hors périmètre: code, docs et garde-fous restent alignés sur la localisation des descriptions et labels.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Title check ✅ Passed Le titre suit Conventional Commits et résume bien la localisation des descriptions et libellés de plugins.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/localized-plugin-manifest-strings

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between e876bf6 and e76b70f.

📒 Files selected for processing (13)
  • CLAUDE.md
  • docs/features/plugins.md
  • src-tauri/crates/app/src/commands/plugin_store.rs
  • src-tauri/crates/app/src/commands/plugins.rs
  • src-tauri/crates/core/src/plugin/assets.rs
  • src-tauri/crates/core/src/plugin/host_impl.rs
  • src-tauri/crates/core/src/plugin/manifest.rs
  • src/components/views/settings/PluginOptions.tsx
  • src/components/views/settings/PluginStoreCard.tsx
  • src/components/views/settings/PluginsCard.tsx
  • src/hooks/useLocalizedText.ts
  • src/lib/localizedText.ts
  • src/lib/tauri/plugins.ts

Comment thread src-tauri/crates/app/src/commands/plugin_store.rs
Comment thread src/lib/localizedText.ts Outdated
`??` 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
@InstaZDLL
InstaZDLL merged commit 5ad7003 into main Jul 27, 2026
14 checks passed
@InstaZDLL
InstaZDLL deleted the feat/localized-plugin-manifest-strings branch July 27, 2026 04:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: backend Rust/Tauri backend (src-tauri/) scope: docs Docs, README, assets scope: frontend React/Vite frontend (src/) scope: plugins Plugin runtime, SDK, store, and bundled plugins size: xl > 500 lines type: feat New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: localized plugin descriptions & option labels

1 participant