Merged in chore/update-readmes (pull request #2751)

chore: Update README

* Update readme


Approved-by: Chuma Mcphoy (We Ahead)
Approved-by: Joakim Jäderberg
This commit is contained in:
Anton Gunnarsson
2025-09-03 08:54:50 +00:00
parent f7ef58eafa
commit ca408bbbb5
4 changed files with 475 additions and 449 deletions

View File

@@ -15,282 +15,3 @@ In order to not prop drill that all the way from a page we use React's `cache` i
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",
})
```