# Internationalization ## The `lang` route parameter All page paths starts with a language parameter, e.g. `/sv/utforska-scandic/wi-fi`. ### Get the language in a client component We have a hook called `useLang` that directly returns the `lang` parameter from the path. ### Get the language in a server component In order to not prop drill that all the way from a page we use React's `cache` in a way that resembles React`s context, but on the server side. For this to work we must set the language with `setLang` on the topmost layout This was inspired by [server-only-context](https://github.com/manvalls/server-only-context) ## Translations a.k.a. UI labels a.k.a. Lokalise ### Quickstart: How to use react-intl in the codebase > For more information read about [the workflow below](#markdown-header-the-workflow). - **Do not destructure `formatMessage` from either `getIntl()` or `useIntl()`.** ❌ Do not do this: ```typescript const { formatMessage } = useIntl() const message = formatMessage(...) ``` ✅ instead do this: ```typescript const intl = useIntl() const message = intl.formatMessage(...) ``` ❌ Do not do this: ```typescript const { formatMessage } = await getIntl() const message = formatMessage(...) ``` ✅ instead do this: ```typescript const intl = await getIntl() const message = intl.formatMessage(...) ``` - **Do not pass variables as defaultMessage.** The `defaultMessage` needs to be a literal string so that the tooling can properly find and extract defined messages. Do not use template strings with variable interpolation either. ❌ Do not do this: ```typescript const data = await getSomeData() ... const message = intl.formatMessage({ defaultMessage: data.type, }) ... const message = intl.formatMessage({ defaultMessage: `Certification: ${data.type}`, }) ``` ✅ instead do this: This is a hard one to give a general solution/rule for, but in essence it should use either use a "switch approach" or `defineMessage/defineMessages` ([docs](https://formatjs.github.io/docs/react-intl/api/#definemessagesdefinemessage)) in some way. The most common reason for this scenario is that the data contains one or several words/sentences we want translated. The preferred solution is to use a "switch approach" (or something equivalent), checking some property of the entity to decide what message to use. - It is explicit about what cases are supported: this helps finding bugs by lowering complexity by making it easier to find where a string is used. - The declaration is coupled with the usage: keeping our messages up to date and clutter free over time. - The formatjs eslint plugin helps enforce proper usage. - TypeScript will force us to keep the list up to date with the typings of `data.type`. Preferably ALL data points of this manner should be an enum or an array of strings `as const` (or equivalent) for the best TS support here. ```typescript ... const data = await getSomeData() ... let message = intl.formatMessage({defaultMessage: 'N/A'})) // or some other default/"no match" message switch (data.type) { case "Restaurant": message = intl.formatMessage({defaultMessage: "Restaurant"}) break; case "Bar": message = intl.formatMessage({defaultMessage: "Bar"}) break; case "Pool": message = intl.formatMessage({defaultMessage: "Pool"}) break; case "Some certification": message = intl.formatMessage({defaultMessage: "Certification: {name}"}, { name: "Some certification"}) break; default: // TS will throw if it reaches here if typings for `data.type` are properly defined and the above cases are exhaustive. // This will help us keep messages up to date. const type: never = data.type console.warn(`Unsupported type given: ${type}`) } ``` If the above is not possible the escape hatch is using something like the following. Avoid using this because: - This decouples the message declaration from the message consumption: causing stale messages to linger around and clutter up the codebase. - It makes it a lot harder to find where a string is being used in the codebase. - The formatjs eslint plugin is unable to enforce placeholders: this decreases confidence in our messages and potentially hiding bugs. ```typescript import { defineMessages } from "react-intl" ... const data = await getSomeData() ... const restaurantMessage = defineMessage({ defaultMessage: "Restaurant", }) const barMessage = defineMessage({ defaultMessage: "Bar", }) const poolMessage = defineMessage({ defaultMessage: "Pool", }) // OR const messages = defineMessages({ restaurant: { defaultMessage: "Restaurant", }, bar: { defaultMessage: "Bar", }, pool: { defaultMessage: "Pool", } }) ... return (
{intl.formatMessage(messages.restaurant)}
// or .bar or .pool, etc. ) ... // Since calls to defineMessage/defineMessages get their messages extracted, // technically you can do the following instead of accessing the key like above. // But it is not encouraged as this decoupling leads to had to track usage and decay over time: const message = intl.formatMessage({ // eslint-disable-next-line formatjs/enforce-default-message defaultMessage: data.type, // data.type === "Restaurant" | "Bar" | "Pool" }) ``` - **Do not use defaultMessage key as an alias.** The `defaultMessage` should be the actual message you want to display (and send to Lokalise). It must be written in American/US English. ❌ Do not do this: ```typescript const message = intl.formatMessage({ defaultMessage: "some.alias", }) ``` ✅ instead do this: ```typescript const message = intl.formatMessage({ defaultMessage: "The real message is here in US English", }) ``` - **Do not use conditionals when defining messages.** ❌ Do not do this: ```typescript const message = intl.formatMessage({ defaultMessage: someVariable ? "Variable is truthy" : "Variables i falsey", }) ``` ✅ instead do this: ```typescript const truthyMessage = intl.formatMessage({ defaultMessage: "Variable is truthy", }) const falseyMessage = intl.formatMessage({ defaultMessage: "Variable is falsey", }) const message = someVariable ? truthyMessage : falseyMessage ``` - **Avoid using ICU special words as placeholder names** Some words have meaning in ICU when dealing with placeholders and such. To avoid bugs and confusion do not use them as placehodler names. Avoid (list is not exhaustive): - `{number}` - `{integer}` - `{plural}` - `{date}` - `{time}` - `{select}` - `{choice}` - `{other}` Also do not use the current variable name in scope as the placeholder name (unless the variable is named following the below rules). It will confuse translators. ❌ Do not do this: ```typescript const number = getValueSomeWay() ... const message = intl.formatMessage( { defaultMessage: "The number is {number}", }, { number, } ) ``` nor this ```typescript const goodNameForVarButNotForPlaceholder = getValueSomeWay() ... const message = intl.formatMessage({ defaultMessage: "The number is {goodNameForVarButNotForPlaceholder}", }, { goodNameForVarButNotForPlaceholder }) ``` ✅ instead do this: Prefer a placeholder name that gives context to the message when reading it without the context of the code ```typescript const goodNameForVarButNotForPlaceholder = getValueSomeWay() ... const message = intl.formatMessage( { defaultMessage: "The number is {membershipNumber}", }, { membershipNumber: goodNameForVarButNotForPlaceholder, } ) ``` or if context is hard to give, use generic words like `value`, `count`, `amount`, etc. ```typescript const number = getValueSomeWay() ... const message = intl.formatMessage( { defaultMessage: "The number is {value}", }, { value: number, } ) ``` - Do not give id to messages. The eslint plugin will automatically fix this (removes the id on save/fix). ❌ Do not do this: ```typescript const message = intl.formatMessage({ id: "some-id", defaultMessage: "This is a message", }) ``` ✅ instead do this: ```typescript const message = intl.formatMessage({ defaultMessage: "This is a message", }) ```