@planning.nl/vue3-i18n
v0.0.14
Published
This module offers lightweight and type safe i18n for Vue 3
Downloads
2
Readme
Vue3-i18n
This is a lightweight and typesafe frontend internationalization library for Vue 3 applications.
Basic Features
- define translation objects
- get and set the active locale(s)
- compact syntax for usage in vue templates
Why Vue3-i18n?
- Simple, small and easy to understand
- Ergonomic syntax
- Fast (translates 3M items per second)
- Flexible and extensible
- Typesafe
- IDE features such as find usages and rename
- Locales and translations are reactive (@vue/reactivity)
- Extensible translations for modules and libraries
Installation
yarn install @planning/vue3-i18n
npm install --save @planning/vue3-i18n
Basic usage
import { l, translate } from "@planning.nl/vue3-i18n";
export const t = translate({
hello: l({
en: "hello",
nl: "hallo",
fallback: "👋",
}),
group: {
world: l({
en: "world",
nl: "wereld",
fallback: "🌐"
})
}
});
locales.value = ["nl-NL"];
console.log(`${t.hello} ${t.group.world}`); // "hallo wereld"
locales.value = ["fr"];
console.log(`${t.hello} ${t.group.world}`); // "👋 🌐"
Component usage:
import { l, translate, number } from "@planning.nl/vue3-i18n";
export default defineComponent({
setup() {
return {
t: translate({
hello: l({
en: "hello",
nl: "hallo",
fallback: "👋",
})
}),
number
}
}
})
<template>
<p v-text="t.hello"></p>
<p>{{ t.hello }} <span>{{ number(10.23) }}</p>
</template>
You should never spread a translator object (
{...translate({})}
) because it is a proxy.
API
Translation Set
The translate
function does all the translation magic and produces a translator.
It expects a nested object that contains your translation items and translations. The entries in this object should either be plain objects (translation groups) or translation items.
Translatable items can be created using the l
function. It accepts a plain object with translations, keyed by locales.
The translation values can be of any type, not just strings.
import { l, translate } from "@planning.nl/vue3-i18n";
const translations = translate({
hello: l({
en: "hello",
"en-US": "hi",
nl: "hallo",
fallback: "👋",
}),
main: {
sub: l({ nl: "sub" })
}
})
A locale key is a string. It is made up of parts, separated by -
symbols. The first part represents the language, the
second group the region and the third group a possible variant.
The fallback
can be used to define the translation to be used when no locale matches.
Locale formats are defined in the BCP 47 locale code.
Translation
Fetching a translation can be done by simply traversing the translator object:
locales.value = ["nl"];
console.log(translations.main.sub); // "sub"
console.log(translations.hello); // "hallo"
When fetching a translation, one of the locale keys will be selected based on the active locales. The associated value will be returned.
The following rules apply:
- The key is used that has the most parts and matches the primary locale (
getLocales()[0]
). - If no such key can be found at all, the secondary, third, ... entry in
getLocales()
is checked. - If no locale can be matched, the
fallback
locale key is used. - If
fallback
is not specified, the first specified locale key is used.
Locale matching in this module doesn't exactly follow BCP 47. It was simplified for simplicity and performance.
Locales
The getLocales()
function returns the array of currently active locales. It returns a concatenated array of:
locales
navigator.languages
fallbackLocales
The locales
ref allows you to overrule the browser's locale (a custom language selector).
The fallbackLocales
ref can be handy if you wish to use the browser's locale. If the visitor doesn't have a supported
locale, you can provide a fallback. This has been set to ["en"]
by default.
Locale overriding
The function withLocales
can be used to fetch a translation for a specific locale. It accepts a list of locales, and
a callback that produces a value:
const hallo = withLocales(["nl", "en-US"], () => t.hello);
withLocales
is especially handy when a value needs to be fetched for another locale than the current one. For example a language selector widget usually contains a description in the target language itself.
String format patterns
Most i18n frameworks allow special patterns in translation strings as placeholders or references to other translations.
This library doesn't post-process strings at all, and it doesn't have any such patterns. You can use normal functions, as they provide flexibility as well as type safety:
const t = translate({
dear: l({ en: "dear", nl: "beste" }),
greetings: l({
en: (name: string) => `Hello ${t.dear} ${name}`,
nl: (name: string) => `Hallo ${t.dear} ${name}`,
}),
});
locales.value = ["nl-NL"];
console.log(t.greetings("Evan")); // "Hallo beste Evan";
locales.value = ["en"];
console.log(t.greetings("Evan")); // "Hello dear Evan";
The usage of functions means that back-references can't be transmitted using json. This library is thus not intended for complex translations provided by a backend, unless you build a pattern-to-function converter yourself.
Pluralization
The grammatical rules about pluralization may differ between languages. But many languages (including English) share the rule that nouns come in just two forms: singular and plural.
For this form of pluralization some helper functions are available:
plural
defines nouns with a singular and plural formpluralAmount
defines nouns along with an 'amount' quantifier
Both produce a (n?: number) => string
converter which can be used directly from the translator.
You can also patch these utility functions to extend them for languages that don't follow these patterns.
const t = translate({
banana: l({
en: plural("banana", "bananas"),
}),
cost: l({
en: pluralAmount("free", "one euro", "{n} euros"),
}),
});
console.log(t.bananas(2)); // bananas
console.log(t.cost()); // one euro
console.log(t.cost(10.55)); // 10.55 euros
You may prefer another method of pluralization, or you may need another plural rules for a specific locale. In that case you can create and use your own pluralization functions.
Number
You can use the number
function to format a number. This library relies on the Intl.NumberFormat
browser
feature for locale-aware number formatting.
The number
function accepts a number and additional Intl.NumberFormatOptions number format options.
console.log(number(10, { style: 'currency', currency: 'EUR' }));
The numberParts
returns the result in a Intl.NumberFormatPart
array.
Datetime
You can use the datetime
function to format a date. This library relies on the Intl.DateTimeFormat
browser
feature.
This module allows a way to override/add custom datetime formats:
patchLocale(dateTimeFormats, "en-US", {
long: {
year: "numeric",
month: "short",
day: "numeric",
weekday: "short",
hour: "numeric",
minute: "numeric",
},
custom: { weekday: "long" },
});
locales.value = ["en-US"];
console.log(datetime(new Date(), "long"));
console.log(datetime(new Date(), "custom", { weekday: "long" }));
The datetimeParts
function will return the result in a Intl.DateTimeFormatPart
array.
ucFirst
The ucFirst
function accepts a string and returns the same string with the first character capitalized:
console.log(ucFirst("hello")); // "Hello"
Capitalization is locale-dependent.
Mutations
The typical use case for vue3-i18n is a static translations set.
But there are situations where it is useful to dynamically add or change translations:
- injecting translations asyncronously (XHR)
- overriding the utility functions
- overriding existing translations for external libraries
There are a couple of ways to change a translation object:
- By directly changing the raw definition object
- By using
patch
orpatchStrict
- By using
patchLocale
orpatchLocaleStrict
Raw
You can change an existing translations set directly by changing the raw definition object. That can be obtained from
a translator using the _raw
property, which is a reference to the translations object that was originally passed to
translate
.
const obj = {
hello: l({
en: "hello",
nl: "hallo",
fallback: "👋",
}),
group: {
world: l({
en: "world",
nl: "wereld",
fallback: "🌍"
})
}
}
const t = translate(obj);
// Notice that obj === t._raw
t._raw.hello.locales["fr"] = "bonjour";
t._raw.hello.locales["de"] = "hallo";
t._raw.group.world.locales["fr"] = "monde";
t._raw.group.world.locales["de"] = "Welt";
t._raw.group.world.locales.fallback = "🌎";
When you have to add locales to a large translations set this quickly becomes tedious.
patch
The patch object allows an existing translator set to be patched with additions and changes using a translations object of the same structure.
This usually leads to less and better readable code than changing the raw object manually.
patch
iterates over both the translations object recursively, and merges the locales for the translatable items:
patch(t, {
hello: l({ fr: "bonjour", de: "hallo" })
});
As translations set are recursive structures, you can also patch a nested element:
patch(t.group, {
world: l({ fr: "monde", de: "Welt", fallback: "🌎" })
});
patchStrict
Using the patchStrict
function ensures that all translation items have been specified. If some keys have not been
specified a typescript error will occur. This makes sure you didn't forget one.
You can explicitly ignore a part of the translations set by setting it as undefined
:
patchStrict(t, {
hello: l({ it: "ciao" }),
group: undefined
});
patchStrict
simply invokespatch
. It only has stricter type checking.
patchLocale
When changing a single locale, patchLocale
provides an even cleaner syntax:
patchLocale(t, "fr", {
hello: "bonjour",
group: {
world: "monde"
}
});
Furthermore, you don't need to use the l
function but simply a value. This makes it a better choice for processing
lazy loaded translations.
patchLocaleStrict
patchLocaleStrict
enforces that all items are specified.
Advanced use cases
Utility customization
The numberParts
, datetimeParts
and ucFirst
utility functions can be localized.
They are defined in a translator object which is exposed as i18n
.
It's possible to provide your own implementation (for a specific locale):
const wrapped = withLocales(["nl"], () => i18n.numberParts);
patchLocale(i18n, "nl", {
numberParts: (v, o) => {
const parts = wrapped(v, o);
return parts.filter((p) => p.type !== "group");
},
});
locales.value = ["nl"];
console.log(number(99999.123)); // 99999,123
Notice that
number
anddatetime
simply concatenate the parts returned bynumberParts
anddatetimeParts
.
Reactive translation objects
If you have a translations object that will change dynamically, wrap the object into reactive
before passing it to
translate
:
const reactiveTranslations = translate(reactive({} as any));
console.log(reactiveTranslations.dynamic?.prop); // undefined
reactiveTranslations._raw.dynamic = { prop: l({ en: "hello", nl: "hallo" }) };
console.log(reactiveTranslations.dynamic?.prop); // "hello"
You don't need this when you're only adding/changing translations for existing items, as they are reactive by default.
You do need this when parts of your set may be added, removed or reassigned dynamically.
Translation Keys
When you'd like to get the translatable keys (type) of a translator object, be aware that the _raw
key is included.
This is probably not what you want.
You can can use keyof typeof translations["_raw"]
to get to the 'real' keys.
i18n for libraries
This lightweight module is also intended for providing i18n in Vue3 libraries and generic components.
You should define your translations in a seperate file and export it as part of your module. Then import the translations into your component(s) and use them where you need them. Add translations for the locales that you wish to ship with your module.
Add a peerDependency
and devDependency
towards @planning/vue3-i18n
to ensure that module is installed (only once).
This enables applications using your component to patch the translation set with additional locales, or even to override the defaults that you have set.
Even better, patchStrict
/ patchLocaleStrict
enforces that all translation items are translated. If you add a
new key to your module, the depending applications, after upgrading, will receive a typescript error which forces them to
provide translations for their own locales as well.
Browser support
Browser support for this module matches Vue3 browser support.
This module relies on Proxy
, which means that IE11 is not supported.