Skip to content

Language & Translations ​

The built-in texts of the atscript-ui components (buttons, empty states, filter chips, Yes / No, workflow error messages) are UI texts with stable codes: UI_CANCEL, UI_FORM_ADD, and so on. They render in English until you provide a locale and a source of translations.

vue
<!-- App.vue -->
<script setup lang="ts">
import { provideAsLocale } from "@atscript/vue-form";
import { locale, i18n } from "./i18n"; // your locale ref and i18n library

provideAsLocale(
  () => locale.value,
  (id, params) => (i18n.te(id) ? i18n.t(id, params ?? {}) : undefined),
);
</script>

<template>
  <RouterView :key="locale" />
</template>

The catalog — AS_UI_MESSAGES ​

@atscript/ui/messages exports AS_UI_MESSAGES: every built-in text by code, in English, with {name} placeholders (UI_FORM_ADD: "Add {item}"). Use it as the source list for your translation files. The subpath imports nothing (no Vue, no DOM), so it also loads on a server: @atscript/moost-wf registers it with moost's registerMessages on import, an app without workflows registers it itself (Translations on the server). Translate the code and keep the placeholders: "{item} hinzufügen". All codes start with UI_.

provideAsLocale(getter, translate?) ​

Call it once at the app root (from @atscript/vue-form). The locale drives Intl formatting in forms (Locale & Currency) and the language of the built-in texts of forms, tables and workflow forms. A text is looked up in this order:

  1. your translate(id, params, locale): its result is final (fill the params yourself); undefined, or the id itself (what i18n libraries answer for a missing key), falls through;
  2. texts the server sent for the current locale (see below);
  3. English from AS_UI_MESSAGES.

Each app has its own texts, so a server rendering many users at once keeps them apart and renders the same text the browser hydrates.

Switching the language. A new locale applies to what renders next. Remount the view to switch an open page: <RouterView :key="locale" />. Table and value-help metadata (/meta) and the labels built from it (boolean Yes / No) are cached per locale, so switching back is instant. Without a remount, a form's value-help picker asks in the new locale from its next search, and a table's filter pickers are rebuilt in it — through the form's or table's client factory, after a provideAsLocale getter change or a setUiLocale call alike. The rest of an open page keeps the locale it mounted in.

In your own components useAsLocale().t reads the same texts:

vue
<script setup lang="ts">
import { useAsLocale } from "@atscript/vue-form";

const { t } = useAsLocale();
</script>

<template>
  <button type="button">{{ t("UI_CANCEL") }}</button>
</template>

Texts sent by the server ​

Workflow forms (<AsWfForm> / useWfForm) send Accept-Language with the provided locale and merge the $i18n texts of the form schema the server answers with. See AsWfForm. A moost server with a translator answers in that language — form texts, validation messages, the built-in UI texts and the texts of expressions: see Translations on the server.

Tables, value help and action forms ​

Since 0.1.165. Tables (<AsTableRoot> / useTable), value-help pickers and the action forms of a table ask their /meta in the provided locale: the library passes locale to your client factory, and the @atscript/db-client Client sends it as Accept-Language. With a translator on the @atscript/moost-db server, labels, action texts and error bodies come back in that language, and the $i18n texts /meta serves are merged into the app's texts — the same way as for workflow forms. An action form merges its own $i18n too (the translated texts of its expressions) when its dialog opens. See Translations (i18n) in the atscript-db docs for the server side.

  • A custom client factory spreads the options it gets into the Client (new Client(url, { ...options, fetch })); one that caches clients keeps one per URL and locale.
  • Server rendering asks in each request's locale and merges into that render's texts (its provideAsLocale); nothing is shared between requests. Without a provider, a server render keeps the built-in texts in English: the process-wide texts never take what a request was served. Hand the render's texts to the browser app — see Server rendering.
  • Table presets and app prefs (usePresets, useAppPrefs) ask in the app's locale too (since 0.1.168), so their error and preflight messages come back translated.

Server rendering ​

Since 0.1.168. A server render merges what the server served (/meta$i18n, …) into that request's texts, but the browser app starts with an empty dict until its own requests return — the page would hydrate in English and switch back. Transfer the render's texts with the state your server already hands to the browser:

ts
// entry-server.ts
import { getAsServedTexts } from "@atscript/vue-form";

const ssrContext = {};
const html = await renderToString(app, ssrContext);
const state = { asTexts: getAsServedTexts(ssrContext) };
// inline it escaped: JSON.stringify(state).replace(/</g, "\\u003c")
ts
// entry-client.ts
import { provideAsServedTexts } from "@atscript/vue-form";

const { app } = createApp();
provideAsServedTexts(app, window.__SSR_STATE__?.asTexts);
app.mount("#app");

provideAsLocale starts with these texts. Only the app's own texts are collected (kept on the render's SSR context, never in module state); a render without provideAsLocale collects nothing.

What hydrates cleanly — the browser's first render matches the server's HTML:

  • A text the render read after the server served it (a page that awaits its data during the render — an async setup, onServerPrefetch) is transferred: both sides show the translation.
  • A text the render read before the server served it shows the fallback (your translate, else English) everywhere in that render — also where it is read again after the translation arrived — and is left out: the hydrating browser shows the same fallback, and its own request switches it afterwards.
  • Several provideAsLocale of one locale in an app agree: a text one of them was served is the others' too, on both sides.
  • Tables, filter pickers and form value-help pickers ask nothing during a server render; their texts arrive after hydration.

Validation messages in the browser ​

Since 0.1.165. Errors the form finds itself (before anything is sent) are worded in the app's language too. A validator message is looked up by its code (MIN_LENGTH, filled with the error's params), a message of your own by its text: your translate first, then the texts the server served for the locale ($i18n of /meta or of a workflow form), else the English message stays. So a client-side error reads like the server's would:

ts
provideAsLocale(
  () => "de",
  (id, params) => (id === "MIN_LENGTH" ? `Mindestens ${params?.min} Zeichen` : undefined),
);

Errors returned by @atscript/db-client writes (its preflight and the server's 400s) are already in the client's locale.

Without Vue — setUiLocale ​

An app that renders atscript-ui models without the Vue packages sets the locale and a translator once, browser-wide:

ts
import { setUiLocale } from "@atscript/ui";

setUiLocale("de", (id, params) => myI18n.t(id, params));

The framework-agnostic helpers that produce text (temporalShortcuts(), conditionLabel(), formatFilterExpr(), createTableDef() boolean labels, …) read it, and also take an explicit t — see the @atscript/ui-table reference. Never call setUiLocale on a server that renders for many users.

DOs and DON'Ts ​

  • Do branch on codes, never on text: every text changes with the locale.
  • Do keep placeholders in translations ("{count} Zeilen").
  • Don't call setUiLocale in a Vue app that uses provideAsLocale: in the browser the provider already serves code outside components.
  • One/many pairs (UI_TABLE_ROWS_ONE / UI_TABLE_ROWS) cover English plurals; your translate gets the real count, so it can apply your language's rules.
  • Model texts (@meta.label, validation messages) are not part of this catalog: the server translates them (Translations on the server); validator messages reported in the browser are looked up by their code.

See also ​

Released under the MIT License.