title | description |
---|---|
Internationalization (i18n) |
Brisa has built-in support for internationalized routing, language detection and consume translations. Learn more here. |
Brisa has built-in support for internationalized (i18n). You can provide a list of locales, the default locale, and domain-specific locales and Brisa will automatically handle the routing.
To get started, add the src/i18n.(ts|js)
or src/i18n/index.(ts|js)
file.
Locales are UTS Locale Identifiers, a standardized format for defining locales.
Generally a Locale Identifier is made up of a language, region, and script separated by a dash: language-region-script
. The region and script are optional. An example:
en-US
- English as spoken in the United Statesnl-NL
- Dutch as spoken in the Netherlandsnl
- Dutch, no specific region
If user locale is nl-BE
and it is not listed in your configuration, they will be redirected to nl
if available, or to the default locale otherwise.
If you don't plan to support all regions of a country, it is therefore a good practice to include country locales that will act as fallbacks.
import { I18nConfig } from "brisa";
const i18nConfig: I18nConfig = {
// These are all the locales you want to support in
// your application
locales: ["en-US", "fr", "nl-NL"],
// This is the default locale you want to be used when visiting
// a non-locale prefixed path e.g. `/hello`
defaultLocale: "en-US",
// This is a list of locale domains and the default locale they
// should handle (these are only required when setting up domain routing)
domains: {
"example.com": {
defaultLocale: "en-US",
},
"example.nl": {
defaultLocale: "nl-NL",
},
},
};
export default i18nConfig;
export default {
// These are all the locales you want to support in
// your application
locales: ["en-US", "fr", "nl-NL"],
// This is the default locale you want to be used when visiting
// a non-locale prefixed path e.g. `/hello`
defaultLocale: "en-US",
// This is a list of locale domains and the default locale they
// should handle (these are only required when setting up domain routing)
domains: {
"example.com": {
defaultLocale: "en-US",
},
"example.nl": {
defaultLocale: "nl-NL",
},
},
};
There are two locale handling strategies: Sub-path Routing and Domain Routing.
Sub-path Routing puts the locale in the url path.
export default {
locales: ["en-US", "fr", "nl-NL"],
defaultLocale: "en-US",
};
With the above configuration en-US
, fr
, and nl-NL
will be available to be routed to, and en-US
is the default locale. If you have a src/pages/blog.js
the following urls would be available:
/en-us/blog
/fr/blog
/nl-nl/blog
By using domain routing you can change the defaultLocale
of each domain:
export default {
locales: ["en-US", "fr", "nl-NL"],
defaultLocale: "en-US",
domains: {
"example.com": {
defaultLocale: "en-US",
},
"example.nl": {
defaultLocale: "nl-NL",
protocol: "http", // by default is https
dev: true, // by default is false
},
},
};
For example if you have src/pages/blog.js
the following urls will be available:
example.com/blog
→example.com/en-us/blog
example.nl/blog
→example.nl/nl-nl/blog
If the browser language is not supported as locale, then it will redirect to defaultLocale
.
When a user visits the application root (generally /
), Brisa will try to automatically detect which locale the user prefers based on the Accept-Language
header and the current domain.
If a locale other than the default locale is detected, the user will be redirected to either:
- When using Domain Routing: The domain with that locale specified as the default
When using Domain Routing, if a user with the Accept-Language
header fr;q=0.9
visits example.com
, they will be redirected to example.fr
since that domain handles the fr
locale by default.
When using Sub-path Routing, the user would be redirected to /fr
.
You can access the locale information via the request context
:
locale
contains the currently active locale.locales
contains all configured locales.defaultLocale
contains the configured default locale.pages
contains the configured pages.t
function to consume translations.
Example in a page:
import { type RequestContext } from "brisa";
type Props = {
name: string;
};
export default function Home({ name }: Props, requestContext: RequestContext) {
const { locale, t, defaultLocale } = requestContext.i18n;
if (locale === defaultLocale) {
return (
<h1>
{i18n.t("strong-hello", { name }, { elements: { strong: <strong /> } })}
</h1>
);
}
return <h1>{t("hello", { name })}</h1>;
}
import en from "./messages/en.json";
import es from "./messages/es.json";
export default {
defaultLocale: "en",
locales: ["en", "es"],
messages: { en, es },
};
{
"hello": "Hello {{name}}!",
"strong-hello": "Hello <strong>{{name}}</strong>!"
}
{
"hello": "¡Hola {{name}}!",
"strong-hello": "¡Hola <strong>{{name}}</strong>!"
}
Brisa supports to consume translations inspired by libraries such as i18next and next-translate.
Tip
Good to know: It only occupies 400B of client code if you consume translations in the web-components, if you only use it in server-components, pages, layout, api, middleware, it is 0B of client code.
In order to consume translations, you need first to define the messages
property in src/i18n.js
file:
import { I18nConfig } from "brisa";
import en from "./messages/en.json";
import es from "./messages/es.json";
const i18nConfig: I18nConfig<typeof en> = {
defaultLocale: "en",
locales: ["en", "es"],
messages: { en, es },
};
export default i18nConfig;
{
"hello": "Hello {{name}}!",
"strong-hello": "Hello <strong>{{name}}</strong>!"
}
{
"hello": "¡Hola {{name}}!",
"strong-hello": "¡Hola <strong>{{name}}</strong>!"
}
After this, you can consume translations in every part of your app through the request context: middleware
, api
routes, page
routes, all page components, responseHeaders
, layout
, Head
of each page...
Important
Important in TypeScript: The generic type <typeof en>
in I18nConfig
enables type-safe consumption of translations with the t
function by resolving the keys, keys with plurals and nested keys from the preferred locale. This allows IDE autocompletion and type checking of translation keys throughout the codebase, improving productivity and avoiding translation bugs due to typos or invalid keys.
The generic I18nConfig<typeof en>
allows you to activate type-safe consuming translations with the t
function. Displaying to you all the keys from the preferred locale messages, resolving plurals and nested values.
Example in a component:
import { type RequestContext } from "brisa";
type Props = { name: string };
export default function Hello({ name }: Props, { i18n }: RequestContext) {
return (
<main>
<h1>{i18n.t("hello", { name })}</h1>
<h2>
{i18n.t("strong-hello", { name }, { elements: { strong: <strong /> } })}
</h2>
</main>
);
}
export default function Hello({ name }, { i18n }) {
return <h1>{i18n.t("hello", { name })}</h1>;
}
The t
function:
- Input:
- i18nKey:
string
(key) - query:
object
(optional) (example:{ name: 'Leonard' }
). See more - options:
object
(optional)- fallback:
string
|string[]
- fallback if i18nKey doesn't exist. See more. - returnObjects:
boolean
- Get part of the JSON with all the translations. See more. - default:
string
- Default translation for the key. If fallback keys are used, it will be used only after exhausting all the fallbacks. - elements -
JSX.Element[]
|Record<string, JSX.Element>
- Useful to use HTML inside translations. In case of Array each index corresponds to the defined tag<0>
/<1>
. In case of object each key corresponds to the defined tag<example>
.
- fallback:
- i18nKey:
- Output:
string
|JSX.Element
Interpolation allows integrating dynamic values into your translations.
All values get escaped to mitigate XSS attacks.
Example to display: "Hello Brisa
":
Keys:
{
"hello": "Hello {{name}}!"
}
Sample:
t("hello", { name: "Brisa" }); // Hello Brisa!
You can change the delimiter that is used for interpolation touching the configuration of src/i18n.js
:
export default {
locales: ["en-US", "fr", "nl-NL"],
defaultLocale: "en-US",
interpolation: {
prefix: "[[",
suffix: "]]",
},
};
And now you can adapt the interpolation messages to use [[
and ]]
:
{
"hello": "Hello [[name]]!"
}
To consume the translations is the same as before.
Sample:
t("hello", { name: "Brisa" }); // Hello Brisa!
You can format params using the interpolation.formatter
config function.
The formatter is the communication layer with Intl EcmaScript features.
For example it helps to transform values with decimals, currencies, etc, depending on the locale.
Sample adding the number
format:
import { I18nConfig } from "brisa";
const formatters = {
es: new Intl.NumberFormat("es-ES"),
en: new Intl.NumberFormat("en-EN"),
};
const i18nConfig: I18nConfig = {
// ...
interpolation: {
format: (value, format, lang) => {
if (format === "number") return formatters[lang].format(value);
return value;
},
},
};
export default i18nConfig;
const formatters = {
es: new Intl.NumberFormat("es-ES"),
en: new Intl.NumberFormat("en-EN"),
};
export default {
// ...
interpolation: {
format: (value, format, lang) => {
if (format === "number") return formatters[lang].format(value);
return value;
},
},
};
In English:
{
"example": "The number is {{count, number}}"
}
In Spanish:
{
"example": "El número es {{count, number}}"
}
Using:
t("example", { count: 33.5 });
Returns:
- In English:
The number is 33.5
- In Spanish:
El número es 33,5
It's possible to use nested messages in the JSONs:
{
"nested": {
"example": "Hello {{name}}"
}
}
To consume it by default is used the .
key separator:
t("nested.example", { name: "Brisa" });
You can change the keySeparator
of nested messages in the src/i18n.js
configuration:
return {
// ...
keySeparator: "|",
};
Then:
t("nested|example");
You can use the returnObjects: true
, in the t
options to get all.
t("nested", { name: "Brisa" }, { returnObjects: true });
// { "example": "Hello Brisa" }
Also you can use a dot ".
" as all:
t(".", { name: "Brisa" }, { returnObjects: true });
// { "nested": { "example": "Hello Brisa" } }
Taking account that these are the messages:
{
"nested": {
"example": "Hello {{name}}"
}
}
If no translation exists you can define fallbacks (string|string[]
) to search for other translations:
Sample of a fallback:
t(
"message-key",
{ count: 1 },
{
fallback: "fallback-key",
},
);
Sample with list of fallbacks:
t(
"message-key",
{ count: 42 },
{
fallback: ["fallback-key", "fallback-key-2"],
},
);
Brisa support 6 plural forms (taken from CLDR Plurals page) by adding to the key this suffix (or nesting it under the key with no _
prefix):
_zero
_one
(singular)_two
(dual)_few
(paucal)_many
(also used for fractions if they have a separate class)_other
(required—general plural form—also used if the language only has a single form)
See more info about plurals here.
Only the last one, _other
, is required because it’s the only common plural form used in all locales.
All other plural forms depends on locale. For example English has only two: _one
and _other
(1 cat vs. 2 cats). Some languages have more, like Russian and Arabic.
In addition, Brisa also support an exact match by specifying the number (_0
, _999
) and this works for all locales. Here is an example:
// **Note**: Only works if the name of the variable is {{count}}.
t("cart-message", { count });
And the messages:
{
"cart-message_0": "The cart is empty", // when count === 0
"cart-message_one": "The cart has only {{count}} product", // singular
"cart-message_other": "The cart has {{count}} products", // plural
"cart-message_999": "The cart is full" // when count === 999
}
or
{
"cart-message": {
"0": "The cart is empty", // when count === 0
"one": "The cart has only {{count}} product", // singular
"other": "The cart has {{count}} products", // plural
"999": "The cart is full", // when count === 999
}
}
Brisa supports consuming HTML inside translations, all interpolations are escaped to avoid XSS attacks and elements are defined from where they are consumed.
You can use numbers
or names
:
{
"hello": "Hello <0>{{name}}</0>!",
"hello-2": "Hello <bold>{{name}}</bold>!"
}
Then to consume it you need to define the elements using an Record<string, JSX.Element>
or an JSX.Element[]
:
t("hello", { name: "Brisa" }, { elements: [<strong />] });
or
t("hello-2", { name: "Brisa" }, { elements: { bold: <strong /> } });
If the translation does not exist and all fallback keys fail (if any), then as default behavior, the key is shown.
t("hello", { name: "Brisa" }); // Hello Brisa
t("no-existing-key"); // no-existing-key
You can overwrite the default value using the default
option:
t("no-existing-key", {}, { default: "No value" }); // No value
With the allowEmptyStrings
option of /src/i18n/index.ts
you can change how translated empty strings should be handled.
{
"hello": ""
}
If omitted or passed as true
(By default is true
), it returns an empty string.
If passed as false
, returns the key name itself.
import { I18nConfig } from "brisa";
import en from './messages/en.json';
import es from './messages/es.json';
const i18nConfig: I18nConfig<typeof en> = {
defaultLocale: "en",
locales: ["en", "es"],
messages: { en, es }
allowEmptyStrings: false,
};
export default i18nConfig;
Now t('hello')
returns "hello"
instead of an empty string ""
.
Many times we want the URL to be different in different languages. For example:
/en/about-us
→src/pages/about-us.tsx
/es/sobre-nosotros
→src/pages/about-us.tsx
export default {
locales: ["en-US", "es"],
defaultLocale: "en-US",
pages: {
"/about-us": {
es: "/sobre-nosotros",
},
"/user/[username]": {
es: "/usuario/[username]",
},
},
};
The key of each page item will be the name of the route. It works also with dynamic and catch-all routes.
It will automatically be taken into account in redirects, navigation and the hrefLang
generation (see here how to active hrefLang
).
During navigation you do not have to add the locale in the href
of the a
tag, if you add it it will not do any conversion.
The fact of not adding the locale Brisa takes care of transforming the link:
function MyComponent({}, { i18n: { t } }) {
return <a href="/about-us">{t("about-us")}</a>;
}
Will be transformed to this HTML in es
:
<a href="/es/sobre-nosotros">Sobre nosotros</a>
As long as you do not put the locale in the href
of a
tag, then no conversion is done. It is useful to change the language:
import { type RequestContext } from "brisa";
export function ChangeLocale(props: {}, { i18n, route }: RequestContext) {
const { locales, locale, pages, t } = i18n;
return (
<ul>
{locales.map((lang) => {
const pathname = pages[route.name]?.[lang] ?? route.pathname;
if (lang === locale) return null;
return (
<li>
<a href={`/${lang}${pathname}`}>{t(`change-to-${lang}`)}</a>
</li>
);
})}
</ul>
);
}
export function ChangeLocale(props: {}, { i18n, route }) {
const { locales, locale, pages, t } = i18n;
return (
<ul>
{locales.map((lang) => {
const pathname = pages[route.name]?.[lang] ?? route.pathname;
if (lang === locale) return null;
return (
<li>
<a href={`/${lang}${pathname}`}>{t(`change-to-${lang}`)}</a>
</li>
);
})}
</ul>
);
}
Brisa supports overriding the accept-language header with a BRISA_LOCALE=the-locale
cookie. This cookie can be set using a language switcher and then when a user comes back to the site it will leverage the locale specified in the cookie when redirecting from /
to the correct locale location.
For example, if a user prefers the locale fr
in their accept-language header but a BRISA_LOCALE=en
cookie is set the en
locale when visiting /
the user will be redirected to the en
locale location until the cookie is removed or expired.
Since Brisa knows what language the user is visiting it will automatically add the lang
and dir
attributes to the <html>
tag.
The lang
attribute is added to define the language of the content assisting search engines and browsers, meanwhile the dir
attribute is added to indicate to search engines and browsers the directionality of the content text.
Brisa by default doesn't add the hreflang
meta tags, but you can activate it to automatic generate the hrefLang
.
To activate the generation of hrefLang
you need the hrefLangOrigin
property to specify one or more origins.
For one origin:
export default {
locales: ["en-US", "es"],
defaultLocale: "en-US",
hrefLangOrigin: "https://www.example.com",
};
For multi-origins:
export default {
locales: ["en-US", "es"],
defaultLocale: "en-US",
hrefLangOrigin: {
es: "https://www.example.com",
en: "https://www.example.co.uk",
},
};
In the case of using domain routing, maybe you wonder why you have to repeat domains and origins? 🤔
domains
defines the default language per domain for routing and language detection.hrefLangOrigin
defines the origin/domain to use in the href attribute of hreflang for each language.
The main difference between them is that you can have multiple domains in domains
with the same defaultLocale
, but in hrefLangOrigin
you want to prioritize a specific one per language. Besides, you may not have domains
defined but want to use the hrefLangOrigin
to only 1 origin.
hrefLang
is automatic managed by Brisa, however rel=canonical
links not.
For these domains
that have the same defaultLocale
we recommend to manage in the layout the canonical links in order to prevent duplicate content issues in search engine optimization.
The finalURL
is a field you have access to in the RequestContext and is the URL of your page, regardless of the fact that for the users it is another one.
For example, if the user enters to /es/sobre-nosotros/
the finalURL
can be /about-us
because your page is in src/pages/about-us/index.tsx
.
export default function SomeComponent(
{},
{ i18n, finalURL, route }: RequestContext,
) {
console.log(`${finalURL} - ${i18n.locale} - ${route.pathname}`);
// /about-us - es - /es/sobre-nosotros/
}