Translate a plugin
Your plugin.yaml is written in one language (its locale: field, fr by
default) — labels, descriptions, value-set concept displays. This recipe adds a
translation for another locale without touching the manifest, using the
meridian-plugin i18n CLI, then covers the separate runtime strings your
behaviors emit through ctx.t. It assumes a scaffolded plugin (see
Scaffold a plugin); for the design behind derived keys
and edge-resolved fallback, see
Instance vs UI locale.
1. Extract a bundle skeleton
From the plugin's directory, run extract for the locale you want to add:
meridian-plugin i18n extract --locale en
(libs/plugin-cli/src/commands/i18n.ts, i18nExtractCommand). This writes or
refreshes i18n/en.yaml next to plugin.yaml, creating the i18n/ directory
if it doesn't exist yet. The command reports what it did:
@acme/vitals → i18n/en.yaml (14 clé(s) au total, 14 ajoutée(s)).
Run it again any time you add, rename, or remove a manifest entry — it is non-destructive: every key already translated in the bundle is kept untouched, only new keys are appended and stale ones (renamed or removed manifest entries) are reported as orphans rather than deleted:
⚠ 1 clé(s) orpheline(s) (hors manifeste, conservée(s) telles quelles) :
- nodes.old-node-id.label
2. Fill in the translations
Open i18n/en.yaml. Every key extract couldn't find an existing value for
is left empty, with the source-language text as a # source: comment right
above it so you know what to translate:
nodes:
patient.load-conditions:
label: ""
# source: Load conditions
inputs.count.description: ""
# source: Maximum number of conditions to return.
Replace each empty "" with the translation and remove the now-redundant
comment (leaving it is harmless — check, in the next step, ignores
comments). The bundle format is two levels — section → id → {path: translation} — because ids themselves contain dots (e.g.
patient.load-conditions):
| Section | Key derived from |
|---|---|
plugin._.description |
the manifest's top-level description |
nodes.<id>.label / .description |
a node's label/description |
nodes.<id>.inputs.<port>.description / outputs.<port>.description / config.<field>.description |
a node's port and config field docs |
categories.<name>.label |
a node's category (one entry per distinct label) |
types.<name>.doc / fields.<field>.doc |
a contributed type's docs |
valueSets.<id>.doc / concepts.<code> |
a value-set's docs and concept displays |
dimensions.<name>.label |
a contributed dimension's label |
conversions.<from>-><to>.label |
a conversion's label |
agents.<slug>.description / inputs.<port>.description / outputs.<port>.description |
an agent's docs (its name, which drives the node id, is never translated) |
eventSources.<name>.description / eventBindings.<name>.description |
an event source's or binding's description |
This table mirrors the manifest walker that both extract and the running
host use (libs/shared/src/plugin/i18n.ts, manifestI18nEntries) — you never
invent a key here; if a string doesn't show up after extract, it isn't a
translatable field. For the fields that back each section, see
Plugin manifest.
3. Add runtime strings your behaviors emit
Strings a behavior builds at execution time (validation titles, notification
bodies) don't come from the manifest, so extract can't derive them — they go
under the bundle's free-form runtime section, keyed however you like:
runtime:
_:
drug-safety-alert.title: Medication safety alert
drug-safety-alert.issue-line: "• [{severity}] {detail}"
Add the same keys, translated, to every locale bundle you ship. extract
preserves this section verbatim across re-runs (it's the runtimeRaw passed
through untouched in libs/plugin-cli/src/commands/i18n.ts), so it's safe to
run extract again after editing runtime by hand. For how a behavior reads
these keys through ctx.t(key, params) — instance locale → your plugin's
source locale → the raw key — see
Implement a node behavior §5.
4. Check coverage
meridian-plugin i18n check
(i18nCheckCommand) walks every i18n/*.yaml file and, for each, lists keys
the manifest expects but the bundle doesn't have (✗ … manquante(s)) and keys
the bundle has but the manifest doesn't recognize (⚠ … orpheline(s),
typically a stale key after a rename). It exits 1 if any locale is missing a
key — wire it into CI:
✓ en: à jour (14 clé(s)).
fr: aucun bundle i18n/ (rien à vérifier).
Two things to know before you rely on this gate:
- The source locale is exempt. A bundle whose filename matches the
manifest's
locale:(fr.yamlby default) is not required to repeat the manifest-derived keys — they already live inplugin.yaml. Its bundle, if present, exists only to carryruntimetranslations for that locale. checknever looks atruntimecoverage. Those keys are free-form and not derivable from the manifest, so there is nothing to diff them against — a missingruntimekey degrades silently to the untranslated key text at run time (see step 3 above and Set the instance language §3). Trackruntimecoverage by review, not by this command.
5. Ship the bundles
i18n/*.yaml must reach dist/ for an externally loaded plugin — npm run build (meridian-plugin build) copies every file under i18n/ into
dist/i18n/ automatically, next to the compiled manifest. See
Build and bundle for the full build output.
6. Verify
Confirm the host actually resolves your new locale, without needing a running
instance in that language. Start the API (or your dev server) and request the
plugin referential with ?locale=:
curl -s "http://localhost:4000/api/plugins?locale=en" | jq '.[] | select(.name=="@acme/vitals")'
Labels and descriptions should come back in English; any key you haven't
translated yet falls back to the source-locale text, then to the raw key —
never to an error. The same ?locale= parameter works on /api/catalog for
node/type metadata as consumed by the visual editor.
Related
- Scaffold a plugin — where
plugin.yamland thei18n/layout start. - Plugin manifest — the fields each i18n key is derived from, and the source
locale:. - Implement a node behavior — writing
ctx.t(key, params)in a behavior. - Build and bundle — how
i18n/*.yamlreachesdist/i18n/. - Instance vs UI locale — why manifest i18n, runtime i18n, and the console's own UI locale are three independent layers.
- Set the instance language — the operator side: choosing which locale
ctx.tresolves into, and auditingruntimecoverage for it. - CLI reference — every
meridian-plugincommand and flag.