Skip to content

feat(admin): a plugin can translate its own texts - #375

Merged
BechsteinDigital merged 1 commit into
mainfrom
feat/admin-plugin-translate
Sep 1, 2026
Merged

feat(admin): a plugin can translate its own texts#375
BechsteinDigital merged 1 commit into
mainfrom
feat/admin-plugin-translate

Conversation

@BechsteinDigital

Copy link
Copy Markdown
Owner

The snippet system already takes plugin files. RegistrySnippetSyncService reads {"snippets": {"de-DE": "snippets/de-DE.json"}} out of a package's registry.json and even checks that every key carries the pluginId as a prefix, so no package can overwrite another's texts. The admin shell loads all of them and translates with t(key, fallback).

What was missing was the way in. @callora/admin/extensions exported no translation function, so a plugin had two options: hardcode its texts, or bundle vue-i18n itself. The second does not work — the vite preset makes only vue external, so the plugin would get a second i18n instance that knows none of the loaded snippets. A translation function that always returns the fallback and never says why.

Which is why every plugin UI in this repo has its texts in the source, in one language.

The shape

t() goes through globalThis.CalloraAdmin like everything else in that barrel, and the shell fills it with the t it already has. The plugin bundle stays free of the shell — the same reason Vue is shared rather than bundled.

import { t } from '@callora/admin/extensions'

t('pbx.number.taken', 'Diese Nummer führt hier bereits irgendwohin.')

The second parameter is what makes this adoptable one line at a time. Until a key exists in the plugin's snippet files, the fallback is what shows — never the key itself. A screen displaying pbx.person.blocked to an operator is worse than one that was never translated. It is the same bargain the shell's own t already makes with its screens.

One deliberate asymmetry

This is the only function in that barrel that does not warn when the shell is absent. Every other one registers something and can honestly say "that did not happen"; this one has to return a string, and the fallback is a correct answer rather than a degraded one. A warning per rendered label would drown a test run in noise for a case that works exactly as designed.

Verification

Admin suite: 599 tests green across 89 files, vue-tsc clean. Three new tests cover the three states: key known, key unknown, shell absent — the last one asserting that nothing is warned.

The snippet system already takes plugin files: RegistrySnippetSyncService reads
{"snippets": {"de-DE": "..."}} out of a package's registry.json and even checks that
every key carries the pluginId as a prefix, so no package can overwrite another's
texts. The admin shell loads all of them and translates with t(key, fallback).

What was missing was the way in. @callora/admin/extensions exported no translation
function, and a plugin bundling vue-i18n itself does not help: the vite preset makes
only vue external, so the plugin would get a second instance that knows none of the
loaded snippets — a translation function that always returns the fallback and never
says why.

So t() goes through globalThis.CalloraAdmin like everything else in that barrel, and
the shell fills it with the t it already has. A plugin bundle stays free of the shell.

The second parameter is what makes this adoptable one line at a time: until a key
exists in the plugin's snippet files, the fallback is what shows — never the key
itself. A screen displaying `pbx.person.blocked` to an operator is worse than one
that was never translated.

It is the only function in that barrel that does not warn when the shell is absent.
Every other one registers something and can honestly say "that did not happen"; this
one has to return a string, and the fallback is a correct answer rather than a
degraded one. A warning per rendered label would drown a test run in noise for a case
that works as designed.

Admin suite: 599 tests green, types clean.
@BechsteinDigital
BechsteinDigital merged commit 90afff6 into main Sep 1, 2026
5 checks passed
@BechsteinDigital
BechsteinDigital deleted the feat/admin-plugin-translate branch September 1, 2026 20:16
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