import { T, Translate } from "interminimal";
// Multlingual fat string
const message = { en: "Hello", fr: "Bonjour" };
function MyThing() {
const [lang, setLang] = useState("en");
const langs = ["en", "fr", "de"];
return (
// Select the current language, dictionary is inherited
<Translate lang={lang}>
<T as="h1" tag="heading" />
{/* Buttons */}
{langs.map(lang => (
<T key={lang} as="button" tag={lang} onClick={() => setLang(lang)} />
))}
<T as="p" text={message} />
</Translate>
);
}
const dictionary = {
$$dict: {
// Language names
en: { en: "English", fr: "Anglais" },
fr: { fr: "Français" },
de: { en: "German", fr: "Allemand", de: "Deutsch" },
// Page heading
heading: {
en: "Let's Translate!",
fr: "Traduisons!",
de: "Lassen Sie uns übersetzen!"
}
}
};
function MyApp() {
return (
// Set the dictionary to use
<Translate dictionary={dictionary}>
<MyThing />
</Translate>
);
}
If you're developing your own components that work with Interminimal you might like to check out the API documentation.
There's a live Demo of Interminal. You can also check out its source
As you can see in the example above you can translate your text using just two components: Translate
which creates a translation context and T
which translates individual chunks of text.
Translate
- the translation contextThe translation context dictates the translation settings for all components below it. Contexts may be freely nested with each overriding one or more properties from the context above it. The most interesting properties of a translation context are lang
, defaultLang
and dictionary
.
function MyThing() {
return (
<Translate dictionary={dictionary} lang="en">
{/* english */}
<T as="p" text={oneString} />
<Translate lang="cy">
{/* welsh */}
<T as="p" text={anotherString} />
</Translate>
</Translate>
);
}
lang
a single BCP 47 language tag, or an array of tags. e.g. ["en-GB", "en-AU"]
defaultLang
a BCP 47 language which as the assumed language for non-translated contentdictionary
a translation dictionaryTo access the currently active translation context use the useTranslation()
hook which returns a LangContext
.
T
- the translatorThe T
component represents translatable text. It will attempt to render using the language specified by the containing translation context.
Here are some of the things T
can do:
{ en: "Hello", fr: "Bonjour" }
lang
attribute only when necessaryalt
, title
)We represent multilingual content as objects that have IETF BCP 47 language codes as keys and corresponding translated text as values. Internally Interminimal casts fat strings into TString
objects.
Here's a simple example:
{ en: "Hello", fr: "Bonjour" }
To resolve a translation T
checks for all the languages in the context's language stack returning the first that matches. The language stack is expanded so that, for example "en-GB" will try "en-GB" first then fall back to "en". You can try this out using the Language Stack Calculator.
Fat strings can also handle pluralisation - which is quite involved for languages such as Welsh. Here's how we'd represent translations for a count of cats. The %1
in the strings is a placeholder for the number of cats - we'll see how to use that soon.
// Combien de moggie?
const cats = {
en: { one: "%1 cat", other: "%1 cats" },
de: { one: "%1 Katze", other: "%1 Katzen" },
cy: {
zero: "%1 cathod",
one: "%1 gath",
two: "%1 gath",
few: "%1 cath",
many: "%1 chath",
other: "%1 cath"
}
};
The keys in fat strings are BCP 47 language codes. They must be canonical; "en-GB" is fine; "EN-GB" is an error. In development mode (when NODE_ENV !== "production"
) any non-canonical tags will throw an error.
Interminimal attempts to find the best translation for the requested languages:
const colour = {
"en": "color",
"en-GB": "colour",
"de": "Farbe"
};
// and later...
ts.toLang(["en-AU"]); // -> color
ts.toLang(["en-GB"]); // -> colour
ts.toLang(["en"]); // -> color
ts.toLang(["de-AT"]); // -> Farbe
The language stack you ask for is expanded to make a search path. For example
ts.toLang(["en-US-x-foo-bar", "en-GB-x-bar"]);
// Looks for
// en-US-x-foo-bar, en-US-x-foo, en-US, en-GB-x-bar, en-GB, en
// in that order
You can find out how a particular language stack expands using the Language Stack Calculator.
Some text is the same in any language. For example, the name of a product may not have to be translated and should be treated as if it's already in the target language. In that case use "*"
as the language key:
const brand = { "*": "Hexten" };
If necessary overrides may still be provided for specific languages:
const brand = { "*": "Hexten", "fr": "Hexadécimal dix" };
Translation dictionaries have a $$dict
key that contains an object that maps tags to fat strings:
const dictionary = {
$$dict: {
greeting: { en: "Hello", de: "Güten Tag" },
cats: {
en: { one: "%1 cat", other: "%1 cats" },
de: { one: "%1 Katze", other: "%1 Katzen" }
}
}
};
In order to use a dictionary pass it to a Translate
to add it to the translation context:
function MyApp() {
return (
// Set the dictionary to use
<Translate dictionary={dictionary}>
<MyThing />
</Translate>
);
}
Nested translation contexts may provide topical dictionaries to override or augment dictionaries in containing contexts. This allows, for example, a component to provide its own translations which will temporarily augment the main dictionary.
T
The simplest use of T
is to translate a simple fat string. By default the translated text is wrapped in a span
.
const greeting = { en: "Hello", fr: "Bonjour" };
return <T text={greeting} />;
// when en: <span>Hello</span>
// when fr: <span>Bonjour</span>
// when de: <span lang="en">Hello</span>
Note that when we ask for a language for which there's no translation we get the default language (in this case "en") and the span
has lang
attribute to declare that it's a different language from the surrounding text.
We can also look up translations by their dictionary tag. This code has the same effect as the example above.
// Assumes the dictionary example from above
return <T tag="greeting" />;
// This also works
return <T text={["greeting"]} />;
When text
is a single element array it is treated as a tag. This is to allow us to mix fat strings and dictionary tags in our data structures and render them with the same code. It also helps when we translate properties - more on that soon.
If we need an element other than span
we can tell T
what to render:
return <T as="h1" tag="greeting" />;
// <h1>Hello<h1> / <h1>Güten Tag</h1> / <h1 lang="en">Hello<h1>
The as
property can be an HTML element name or a function or class React component.
If you prefer you can use the tBind
helper to make a new component which always renders as a particular element:
import { tBind, tBindMulti } from "interminimal";
const Toption = tBind("option");
const [Tdiv, Tp] = tBindMulti(["div", "p"]);
return <Tdiv tag="language" />;
tBind
also accepts an HTML element name or a React component.
With the exception of text
, tag
, content
, as
and count
, T
passes remaining properties to the underlying element.
return <T as="option" value="X" tag="language" />;
// <option value="X">English</option>
We can also ask for properties to be translated by prefixing them with t-
.
// Fat string
const caption = { en: "Hello", de: "Hallo" };
return <T as="img" t-alt={caption} src="i/pic.jpg" />;
// Dictionary
return <T as="img" t-alt={["caption"]} src="i/pic.jpg" />;
As noted above a fat string whose value is a single element array is treated as a dictionary tag. That allows us to get property translations from either the dictionary or fat strings.
We don't have to limit ourselves properties containing human text. We could switch the image too.
// Fat string
const caption = { en: "Hello", de: "Hallo" };
const pic = { en: "i/pic.jpg", de: "i/pic-de.jpg" };
return <T as="img" t-alt={caption} t-src={pic} />;
Translations can be templated. Placeholders in the translated string (%1
, %2
etc) are replaced with the corresponding child components of the T
.
If you know your fat strings are not templated you should use the content
property of T
to disable template substitution.
When you do want template substitution this is how it works.
const dictionary = {
$$dict: {
// Numbers
one: { en: "One", fr: "Un", de: "Ein", cy: "Un" },
two: { en: "Two", fr: "Deux", de: "Zwei", cy: "Dau" },
// Message
info: {
// Demo substitution
en: "The word for 1 is %1 and the word for 2 is %2",
fr: "Le mot pour 2 est %2 et le mot pour 1 est %1"
}
}
};
return (
<Translate dictionary={dictionary}>
<T tag="info">
<T tag="one" />
<T tag="two" />
</T>
</Translate>
);
// when cy
// <span lang="en">
// The word for 1 is
// <span lang="cy">Un</span>
// and the word for 2 is
// <span lang="cy">Dau</span>
// </span>
//
// when fr
// <span>
// Le mot pour 2 est
// <span>Deux</span>
// et le mot pour 1 est
// <span>Un</span>
// </span>
In that example there is no Welsh ("cy") translation for the "info"
tag so we get the default English version of that text but the numbers are still correctly translated into Welsh. Because the Welsh words "Un" and "Dau" are inside English text they get a lang="cy"
attribute to switch back to Welsh. This is because T
is aware of the "ambient" language and adds the lang
attribute if the ambience doesn't match its translation.
The French version of the info text reverses the order of the substitutions.
The children of the templated T
can be of any kind. For example we could embolden the translation for "two".
return (
<T tag="info">
<T tag="one" />
<strong>
<T tag="two" />
</strong>
</T>
);
The example above looks up the tags "one"
and "two"
in the dictionary and will translate them independently of the containing template. Sometimes it's more convenient to keep all the parts of the string together. Suppose we want to add translation to this markup:
<span>
Here's a
<a href="/">useful link</a>
and here's some
<i>italic text<i>
</span>
Here's how we might do it:
// Simpler to translate
const message = {
en: "Here's a %1[useful link] and here's some %2[italic text]",
fr: "Voici %2[du texte en italique] et un %1[lien utile]"
};
return (
<T text={message}>
<T as="a" href="/" tag="%1" />
<T as="i" tag="%2" />
</T>
);
// En Français:
// <span>
// Voici
// <i>du texte en italique</i>
// et un
// <a href="/">lien utile</a>
// </span>
Within the nested components any text [in square brackets]
after the placeholder will be available as tags named after that placeholder: %1
, %2
etc.
For child components that need multiple text substitutions you can nest placeholders:
const silly = {
en:
"Top level %1[Level one %1[Level two] and" +
" %2[also level two with %1[level three]]]",
fr:
"Niveau supérieur %1[Niveau un %1[Niveau deux]" +
" et %2[aussi niveau deux avec %1[niveau trois]]]"
};
return (
<T text={silly}>
<T tag="%1">
<T tag="%1" />
<T tag="%2">
<T tag="%1" />
</T>
</T>
</Tp>
);
We can also interpolate the contents of tags (looked up in the dictionary chain) into template strings. A template string like "The name of this site is %{site}"
will look for a tag called site
and substitute it. This substitution happens early in the rendering of the string so the substituted text can provide further placeholders.
Often you will receive translated text that should not be parsed as a template. If, for example, you recieve the translated text of a blog post from an API you need to be able to render it without worrying that it might contain tokens like %123
which would be interpreted as placeholders - and which would cause your application to throw an error.
In such cases use the content
attribute of T
:
// Don't parse postBody as a template string.
return <T content={postBody}>;
Note that you can pass a ["tag"]
as the content
property to cause dictionary lookup:
return <T content={["literal"]}>;
We've seen that %\d+
introduces a placeholder into a template string. Placeholders may optionally be followed by [text in brackets]
that will be available to child components. There may be no spaces between the placeholder and the opening [
.
To interpolate the contents of a translation from the dictionary we use %{tagName}
.
A %
followed by %
, [
or ]
escapes that character:
%
use %%
[
after a placeholder use %[
]
inside placeholder text use %]
The brackets [
and ]
are only special after a placeholder - you can use them anywhere else without escaping them.
Apart from their use in %{tagName}
{
and }
also have no special significance and may be used unescaped.
Any %
that isn't followed by a digit or %
, [
or ]
is passed through unaltered so for most text you won't have to worry about escaping special characters.
Different languages have different rules for forming plurals. They also have different kinds of plurals: Welsh has six. Here's how we count cats in English, German and Welsh
// A pluralised, templated fat string.
const cats = {
en: { one: "%1 cat", other: "%1 cats" },
de: { one: "%1 Katze", other: "%1 Katzen" },
cy: {
zero: "%1 cathod",
one: "%1 gath",
two: "%1 gath",
few: "%1 cath",
many: "%1 chath",
other: "%1 cath"
}
};
function Cats({ count }) {
// We stringify count because otherwise when we try to count 0 cats
// React treats the falsy 0 as a non-existent component because 0 is
// false but "0" is true.
return (
<T as="div" text={cats} count={count}>
{String(count)}
</T>
);
}
function CountCats() {
const counts = [0, 1, 1.5, 2, 3, 6, 42];
return (
<div>
{counts.map((count, i) => (
<Cats key={i} count={count} />
))}
</div>
);
}
// You're here for the Welsh cats, right?
// 0 cathod
// 1 gath
// 1.5 cath
// 2 gath
// 3 cath
// 6 chath
// 42 cath
The count
property of T
is used select the language-appropriate plural.
Plurals don't have to be templated but it's often a good idea because it allows the translator to vary where the number appears in the text.
The ECMAScript Internationalization API provides language sensitive string comparison, number formatting, date and time formatting, pluralisation rules and more. With the exception of pluralisation Interminimal doesn't attempt to wrap any of the Intl
APIs but it does make it easy to work with them. It's a conscious decision not attempt to integrate Intl
functionality more tightly:
Intl
support isn't complete across all browsersIntl
with Interminimal so why abstract it further?Bear in mind when using the Intl
APIs that locale and feature support varies by browser and will also likely be different between NodeJS and any particular browser. That can cause problems for Server Side Rendering - for example, at the time of writing, Node 18 has support for Welsh Date/Time formatting but Chrome does not. An SSR Welsh page will therefore render date / time strings differently on initial render and hydration.
You can test browser support for the languages you're interested in using the Language Stack Calculator.
With that caveat Intl
is extremely useful so let's see how it works. Here's a component that wraps Intl.DateTimeFormat
:
const TDateFormat = ({ date, ...opt }) => {
const ctx = useTranslation();
// Use our languages stack to find a format for our locale
const dtf = new Intl.DateTimeFormat(ctx.languages, opt);
// Find out which language was matched...
const { locale } = dtf.resolvedOptions();
// Format the date and create a literal ts with the available
// locale
const ts = TString.literal(dtf.format(date), locale);
// Format it using T
return <T text={ts} />;
};
We use it like this:
return <TDateFormat date={theDate} dateStyle="full" timeStyle="full" />;
Most of the Intl
APIs can be wrapped into components in a similar way. For example here's a component that wraps Intl.ListFormat
. It translates the formatting of lists of items ("This, that, and something else"
).
const TList = ({ children, ...opt }) => {
const ctx = useTranslation();
// Use our languages stack to find a format for our locale
const lf = new Intl.ListFormat(ctx.languages, opt);
// Find out which language was matched...
const { locale } = lf.resolvedOptions();
// Make the children into a list of args, %1, %2 etc
const list = Array.from(
{ length: Children.count(children) },
(_v, i) => `%${i + 1}`
);
// Format the list into a template string and make the translated
// template and locale into a TString
const ts = TString.literal(lf.format(list), locale);
// Format it using T
return <T text={ts}>{children}</T>;
};
Use it like this:
return (
<TList>
<T tag="one" />
<T tag="two" />
<T tag="three" />
</TList>
);
// en: One, Two, and Three
// de: Eins, zwei und drei
Generally speaking the pattern is
TString
with the correct lang (locale
)<T text={ts}>
A similar technique is likely to work with internationalisation APIs other than Intl
.
Translate
creates a translation context. Any use of T
in components below the Translate
will attempt to translate text according to the context settings. Translate
can be nested to any depth. Each Translate
inherits and potentially overrides settings from its parent context. The most interesting properties of a translation context are lang
, defaultLang
and dictionary
.
T
attempts to translate text in fat strings or from the dictionary. It can translate content and properties. By default T
renders as a span but it can be asked to render as any element or component.
T
supports plurals and templating. Templating can nest to any depth. Templates can re-order child components.
You can read more in the API documentation.
Generated using TypeDoc