This application is built with Internationalization (I18n) / Localization (L10n) support to provide a consistent experience across different languages and regions.
- I18n - Internationalization, the process of designing software to support multiple languages
- L10n - Localization, the process of adapting software for specific languages and regions
- Translation Keys - Unique identifiers for translatable strings
- Locale - A specific language and region combination (e.g., en-US, es-ES)
The following types of data MUST always be localized when presented to the user (including accessibility texts that are not rendered):
- Texts: Use the translate method
- Date/time: Use DateUtils
- Numbers and amounts: Use NumberFormatUtils and LocaleDigitUtils
- Phone numbers: Use LocalPhoneNumber
In most cases, you will need to localize data used in a component. Use the useLocalize hook, which abstracts most of the logic you need (primarily subscribing to the NVP_PREFERRED_LOCALE Onyx key).
All translations are stored in language files in src/languages. Translations SHOULD be grouped by their pages/components for better organization.
A common rule of thumb is to move a common word/phrase to be shared when it's used in 3 or more places.
Always prefer to use arrow functions and/or HTML to produce rich text in translation files. For example, if you need to generate the text User has sent $20.00 to you on Oct 25th at 10:05am, add just one key to the translation file and use the arrow function version:
nameOfTheKey: ({amount, dateTime}) => `User has sent <strong>${amount}</strong> to you on <a>${datetime}</a>`,This is because the order of phrases might vary from one language to another, and LLMs will be able to produce better translations will the full context of the phrase. If rich formatting is needed, use HTML in the string and render it with react-native-render-html.
Always prefer whole phrases over string concatenation, even if the result is more verbose:
// BAD
{
receiptRequired: ({formattedLimit, category}: ViolationsReceiptRequiredParams) => {
let message = 'Receipt required';
if (formattedLimit ?? category) {
message += ' over';
if (formattedLimit) {
message += ` ${formattedLimit}`;
}
if (category) {
message += ' category limit';
}
}
return message;
},
addExpenseApprovalsTask: ({workspaceMoreFeaturesLink}) =>
`*Add expense approvals* to review your team's spend and keep it under control.\n` +
'\n' +
`Here's how:\n` +
'\n' +
'1. Go to *Workspaces*.\n' +
'2. Select your workspace.\n' +
'3. Click *More features*.\n' +
'4. Enable *Workflows*.\n' +
'5. Navigate to *Workflows* in the workspace editor.\n' +
'6. Enable *Add approvals*.\n' +
`7. You'll be set as the expense approver. You can change this to any admin once you invite your team.\n` +
'\n' +
`[Take me to more features](${workspaceMoreFeaturesLink}).`,
}
// GOOD
{
receiptRequired: ({formattedLimit, category}: ViolationsReceiptRequiredParams) => {
if (formattedLimit && category) {
return `Receipt required over ${formattedLimit} category limit`;
}
if (formattedLimit) {
return `Receipt required over ${formattedLimit}`;
}
if (category) {
return `Receipt required over category limit`;
}
return 'Receipt required';
},
addExpenseApprovalsTask: ({workspaceMoreFeaturesLink}) =>
dedent(`
*Add expense approvals* to review your team's spend and keep it under control.
Here's how:
1. Go to *Workspaces*.
2. Select your workspace.
3. Click *More features*.
4. Enable *Workflows*.
5. Navigate to *Workflows* in the workspace editor.
6. Enable *Add approvals*.
7. You'll be set as the expense approver. You can change this to any admin once you invite your team.
[Take me to more features](${workspaceMoreFeaturesLink}).
`),
},
}This provides our AI translation LLM with more context to translate the whole phrase as one string, producing higher quality results.
When working with translations that involve plural forms, it's important to handle different cases correctly:
- zero: Used when there are no items (optional)
- one: Used when there's exactly one item
- two: Used when there's two items (optional)
- few: Used for a small number of items (optional)
- many: Used for larger quantities (optional)
- other: A catch-all case for other counts or variations
Example implementation:
messages: () => ({
zero: 'No messages',
one: 'One message',
two: 'Two messages',
few: (count) => `${count} messages`,
many: (count) => `You have ${count} messages`,
other: (count) => `You have ${count} unread messages`,
})Usage in code:
translate('common.messages', {count: 1});src/languages/en.ts is the source of truth for static strings in the App. src/languages/es.ts is (for now) manually-curated. The remainder are AI-generated using scripts/generateTranslations.ts.
The script is run automatically in GH and a diff with the translations is posted as a comment. See example: #70702 (comment)
If you are unhappy with the results of an AI translation, there are two methods of recourse:
-
Context annotations: If you are adding a string that can have an ambiguous meaning without proper context, you can add a context annotation in
en.ts. This takes the form of a comment before your string starting with@context. -
Prompt adjustment: The base prompt(s) can be found in
prompts/translation, and can be adjusted if necessary.