feat(SW-2333): I18n for multiple apps and packages * Set upp i18n in partner-sas * Adapt lokalise workflow to monorepo * Fix layout props Approved-by: Linus Flood
297 lines
8.4 KiB
Markdown
297 lines
8.4 KiB
Markdown
# 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 (
|
|
<p>{intl.formatMessage(messages.restaurant)}</p> // 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",
|
|
})
|
|
```
|