Localization for Developers
Hello! Here are some things to keep in mind when writing content that includes copy that will be localized.
This includes (but is not limited to):
- Button prompts
- Menu items
- Error messages
- Notifications
- Any other text that users will see
This is NOT:
- User submitted content (e.g. chat messages, stream titles, etc)
Quick start
Section titled “Quick start”- Most of the time, you’ll use the useTranslation hook from
react-i18nextto wrap any text that will be localized:
import { useTranslation } from 'react-i18next';
function MyComponent() { // Use default namespace (common) const { t } = useTranslation();
return ( <div> {/* Simple translation from common namespace */} <button>{t('save')}</button> <button>{t('cancel')}</button>
{/* With variables */} <p>{t('welcome-user', { username: 'Alice' })}</p>
{/* With count (triggers pluralization) */} <span>{t('notification-count', { count: 5 })}</span> </div> );}
function SettingsComponent() { // Specify a namespace for settings-specific translations const { t } = useTranslation('settings');
return ( <div> <h1>{t('settings-title')}</h1> <p>{t('language-selection-description')}</p> </div> );}- For complex React components, use the
Transfunction fromreact-i18nextto wrap any text that will be localized. As long you have no React/HTML nodes integrated into a cohesive sentence (text formatting like strong, em, link components, maybe others), you won’t need it. Most of the times you will be using the abovetfunction. You should probably import it asTto save some typing:
import { Trans as T } from "react-i18next";import { Text, Linking } from "react-native";
function HelpText() { const handleEmailPress = () => { Linking.openURL("mailto:support@stream.place"); };
const handleDocsPress = () => { Linking.openURL("/docs"); // Or use your navigation method };
return ( <T i18nKey="help-message" components={{ supportLink: ( <Text style={{ color: "#007AFF", textDecorationLine: "underline" }} onPress={handleEmailPress} /> ), docsLink: ( <Text style={{ color: "#007AFF", textDecorationLine: "underline" }} onPress={handleDocsPress} /> ), bold: <Text style={{ fontWeight: "bold" }} />, }} values={{ username: "Alice", supportEmail: "support@stream.place", }} > Hi <bold>{{ username }}</bold>! Need help? Contact{" "} <supportLink>{{ supportEmail }}</supportLink> or check our <docsLink>documentation</docsLink>. </T> );}Workflow
Section titled “Workflow”- Add translation keys in your code as shown above.
// ❌ Don't hardcode text<Text>Settings</Text><Text>You have 5 new messages</Text>
// ✅ Use translation keys with appropriate namespace// Common UI elements use default namespace<Button>{t('save')}</Button><Button>{t('cancel')}</Button>
// Settings-specific use settings namespaceconst { t } = useTranslation('settings');<Text>{t('settings-title')}</Text>- Add translations to the appropriate .ftl files in
js/components/locales/:
For common UI elements (buttons, labels, etc.), use common.ftl:
save = Savecancel = Cancelloading = Loading...error = ErrorFor feature-specific translations, use the appropriate namespace file:
settings-title = Settingslanguage-selection = Languagelanguage-selection-description = Choose your preferred language- Compile the translations to JSON:
cd js/componentspnpm i18n:compileThis reads the .ftl files and outputs compiled JSON to
js/components/public/locales/{locale}/{namespace}.json (e.g., common.json,
settings.json).
- For web: the compiled files in
public/locales/are served as static assets and loaded on demand.
For native: the compiled files are bundled with the app via static require()
calls in the components package.
Project structure
Section titled “Project structure”The i18n system is centralized in @streamplace/components:
js/components/├── locales/ # Source .ftl files (organized by namespace)│ ├── en-US/│ │ ├── common.ftl # Common UI elements (buttons, labels, etc.)│ │ └── settings.ftl # Settings-specific translations│ ├── pt-BR/│ ├── es-ES/│ ├── zh-Hant/│ └── fr-FR/├── src/i18n/│ ├── manifest.json # Supported locales and metadata│ ├── i18next-config.ts # Bootstrap configuration with namespace setup│ ├── provider.tsx # React provider components│ └── index.ts # Public exports├── public/locales/ # Compiled JSON output (by locale and namespace)│ ├── en-US/│ │ ├── common.json│ │ └── settings.json│ └── ...└── scripts/ ├── compile-translations.js # Uses @fluent/syntax to parse .ftl files └── extract-i18n.jsThe app imports i18n from @streamplace/components:
import { i18next, useTranslation } from "@streamplace/components";Namespaces
Section titled “Namespaces”Translations are organized into namespaces to keep related translations together and improve code organization:
-
common(default): General UI elements used across the app- Buttons: save, cancel, delete, edit, etc.
- Status messages: loading, error, success, warning
- Common actions: yes, no, continue, back, next
-
settings: Settings page translations- App version and update messages
- Language selection
- Custom node configuration
- Debug recording settings
When adding new feature areas, create a new namespace .ftl file:
- Create
locales/{locale}/feature.ftlfor each locale - Add the namespace to
I18NEXT_CONFIG.nsarray ini18next-config.ts - Add static
require()entries for React Native in the translation map - Use
useTranslation('feature')in your components
Available scripts
Section titled “Available scripts”In js/components:
pnpm i18n:compile- Compile .ftl files to JSON and copy to apppnpm i18n:watch- Watch .ftl files, recompile and copy on changes (recommended for development)pnpm i18n:extract- Extract translation keys from source code and add them to .ftl files
Development workflow
Section titled “Development workflow”When actively working on translations:
-
In one terminal, run the app dev server:
Terminal window cd js/apppnpm start # or pnpm web, pnpm ios, etc. -
In another terminal, run the translation watcher:
Terminal window cd js/componentspnpm i18n:watch -
Edit
.ftlfiles injs/components/locales/ -
Save - the watcher will automatically:
- Compile your changes to JSON
- Copy the compiled files to
js/app/public/locales/ - Your dev server will pick up the changes and hot reload
This provides a fast feedback loop for translation work!
Keep in mind…
Section titled “Keep in mind…”Keys are important
Section titled “Keys are important”Choose meaningful keys that describe the content, not the presentation. They should be descriptive and scoped.
user-welcome-message = Welcome back, { $username }!settings-account-title = Account Settingserror-network-connection = Connection failedbutton-save-changes = Save Changesform-validation-email-invalid = Please enter a valid email addressPlatform differences
Section titled “Platform differences”The system handles both web and React Native:
- Web: loads translations via HTTP from
/locales/{locale}/messages.json - React Native: bundles translations via static
require()calls
The bootstrap code in @streamplace/components/i18n automatically detects the
platform and uses the appropriate loading strategy.
Adding new locales
Section titled “Adding new locales”- Add the locale to
js/components/src/i18n/manifest.json:
{ "supportedLocales": [ "en-US", "pt-BR", "es-ES", "zh-Hant", "fr-FR", "new-LOCALE" ], "languages": { "new-LOCALE": { "code": "new-LOCALE", "name": "Language Name", "nativeName": "Native Name", "flag": "🏁" } }}- Create the locale directory and copy .ftl files from another locale:
cd js/components/localesmkdir new-LOCALEcp en-US/*.ftl new-LOCALE/-
Translate the .ftl files in
js/components/locales/new-LOCALE/ -
Add static
require()entries for all namespaces injs/components/src/i18n/i18next-config.ts:
const translationMap: Record<string, any> = { // ... existing entries "new-LOCALE/common": require("../../public/locales/new-LOCALE/common.json"), "new-LOCALE/settings": require("../../public/locales/new-LOCALE/settings.json"), // Add any other namespaces you've created};- Run
pnpm i18n:compileto generate the JSON files
You can also view the official Fluent docs and the official syntax guide to learn how to write better Fluent messages.