Appearance
@dynamicforms/translatable
Translatable strings for the @dynamicforms Vue libraries.
Two parties use this package:
- Library: a package with user-facing strings, such as
@dynamicforms/vue-grid. It declares its strings with English defaults, reads them withtranslate, and exportstranslateStrings. - Application: the app that uses such libraries. It owns the translations and the locale, and passes its translation function to each library's
translateStrings.
A library ships no translations and does not select a locale. The libraries list this package as a peer dependency, so the application installs it. The application imports from it only formatParams, to format placeholder values, and interpolate and the TranslateFunction type, to write a translation function without an i18n library.
The package depends only on Vue. The translation function is any function with the signature below. vue-i18n's and i18next's t have it. The examples and the tests use vue-i18n.
Translation function
ts
type TranslateFunction = (key: string, named: Record<string, unknown>) => string;Returns the translation of key with named substituted, or key unchanged if there is no translation. The application provides it; the library calls it.
named holds the values as the library has them: numbers as numbers, dates as dates.
Application
Connecting a library
Call each library's translateStrings once, with the translation function and the namespace that holds the library's keys in the application's translations. With vue-i18n:
ts
import { translateStrings as translateInputsStrings } from '@dynamicforms/vuetify-inputs';
import { translateStrings as translateGridStrings } from '@dynamicforms/vue-grid';
translateInputsStrings(i18n.global.t, 'inputs');
translateGridStrings(i18n.global.t, 'grid');json
{ "grid": { "SelectionCountSelected": "Izbranih elementov: {count}" } }With namespace grid, the library looks up key SelectionCountSelected as grid.SelectionCountSelected. Without a namespace, as SelectionCountSelected.
Each library's documentation lists its keys and the placeholders in their default text. Translations use the same placeholder names; the library passes their values and the translation function substitutes them.
Without an i18n library, write the translation function with interpolate, which substitutes {name} placeholders:
ts
import { interpolate } from '@dynamicforms/translatable';
const t = (key: string, named: Record<string, unknown>) => interpolate(dictionary[key] ?? key, named);
translateGridStrings(t);Formatting values
The translation function substitutes a value as it prints, so a number does not get the locale's digits or separators and a date prints as Date.prototype.toString does. formatParams wraps the translation function so that each value is formatted first. With vue-i18n:
ts
import { formatParams } from '@dynamicforms/translatable';
const { t, n, d } = i18n.global;
const tf = formatParams(t, (value) =>
typeof value === 'number' ? n(value) : value instanceof Date ? d(value) : value);
translateInputsStrings(tf, 'inputs');
translateGridStrings(tf, 'grid');Without an i18n library, with Intl for the locale the application holds:
ts
import { ref } from 'vue';
const locale = ref('sl');
const tf = formatParams(t, (value) => {
if (typeof value === 'number') return new Intl.NumberFormat(locale.value).format(value);
if (value instanceof Date) return new Intl.DateTimeFormat(locale.value).format(value);
return value;
});- One wrapped function serves every library.
formatruns on every read, so reactive state it reads (vue-i18n's locale throughnandd, orlocaleabove) is tracked, and strings follow a locale switch.- A value
formatreturns unchanged reaches the translation function as it is. - A string without a translation falls back to the library's English default with the values unformatted.
Resolution order
For each string, the library uses the first of:
- The translation function's result for
'<namespace>.<key>', unless it is the key unchanged. - The library's English default for the key.
- For a key the library does not declare: the English text the library passes with it, then the key itself.
An application can therefore connect a library before translating all of its keys.
Run-time keys
Some libraries also translate keys that only exist at run time, such as error codes sent by the application's own server. The library does not know these keys; the application defines them and translates them under the namespace it connects for them:
ts
translateErrorCodes(i18n.global.t, 'errors');json
{ "errors": { "quota_exceeded": "Kvota je presežena" } }A run-time key without a translation shows the English text the library passes with it (for an error, the message the server sent), then the key itself. The library's documentation states which function connects run-time keys and where the keys come from.
Locale changes
The library calls the translation function on every read, inside the reactive context of the render, computed or watcher that reads the string. A string updates when reactive state the translation function reads changes.
A translation function over non-reactive translations does not trigger updates. Call translateStrings again after changing them; each call re-evaluates all strings.
Notes for vue-i18n
treads the reactive current locale, so all strings follow a locale switch without atranslateStringscall.tcovers the current locale and thefallbackLocalechain in step 1 of the resolution order.treturns the key for a missing translation by default. Amissinghandler that returns a different value disables steps 2 and 3.tsubstitutes named parameters as they print, without the locale's digits, separators or date format. See Formatting values.nanddwithoutnumberFormatsordatetimeFormatsformat withIntl's defaults for the current locale.- vue-i18n logs a warning for each missing key in development unless
missingWarn: falseis set.
Library
Declaring strings
ts
import { createTranslatable } from '@dynamicforms/translatable';
export const { translate, translateStrings } = createTranslatable({
NoData: 'No data',
SelectionCountSelected: '{count} items selected',
});Keys are PascalCase and name the meaning, not the English text (NoData, not NothingToShow).
List @dynamicforms/translatable in peerDependencies and mark it external in the build, so the application installs one copy for all libraries.
Export translateStrings from the library's public API (install() options, a named export, or both) and document every key with its placeholders.
Reading strings
translate(key, params?, defaultValue?) returns the current translation of key with params substituted. Call it where the string is displayed, in a template or a computed:
vue
<template>
<span>{{ translate('SelectionCountSelected', { count }) }}</span>
</template>ts
const message = computed(() => translate('SelectionCountSelected', { count: 5 }));Returning a translated message
A library function that hands a translated message to its caller returns ComputedRef<string>, not string:
ts
export function noDataMessage(): ComputedRef<string> {
return computed(() => translate('NoData'));
}A returned string holds the translation of the locale active at the time of the call. When the caller stores it, for example as a field error or a notification, it stays in that locale after a locale switch. A ComputedRef re-evaluates on every read.
Run-time keys
For keys not known at build time, such as error codes sent by a server, type the dictionary as Record<string, string> and pass the English text as defaultValue:
ts
const { translate, translateStrings } = createTranslatable<Record<string, string>>({
not_found: 'Item with pk {pk} not found',
});
const message = computed(() => translate(body.detail_code, body.detail_params, body.detail));Without a translation, message is the declared default for not_found, body.detail for any other code, and the code itself if body.detail is empty.
To keep declared keys and run-time keys in separate namespaces, create two instances and export the translateStrings of both.