Internationalization (i18n)
Relevant source files
The following files were used as context for generating this wiki page:The internationalization (i18n) system in Umami is built upon the react-intl library, a part of the FormatJS suite. It enables the application to be translated into multiple languages by managing message strings within the source code and providing a structured workflow for translation and compilation. The system relies on a combination of message definitions in TypeScript, JSON language files, and a series of Node.js scripts to process, merge, and compile translations for use in the React front-end.
The core of the i18n architecture involves defining messages with unique IDs and default English strings directly in the code. These messages are then extracted and merged into locale-specific JSON files for translators. A build process compiles these JSON files into a highly efficient format that react-intl consumes at runtime. In components, a custom hook useMessages provides a simple interface for accessing and formatting these translated strings.
Core Concepts & Architecture
The i18n system is centered around react-intl and a set of conventions for defining, storing, and building translation assets.
Message Definitions
All UI text strings (labels, messages, etc.) are defined centrally using the defineMessages function from react-intl. This function creates a map of message descriptors, each containing a unique id and a defaultMessage.
labels: For short, reusable UI text like button labels and field names.messages: For longer, more complex sentences, including those with placeholders.
These definitions are located in a single file for easy management and extraction.
Example of message definitions:
// src/components/messages.ts:3
import { defineMessages } from 'react-intl';
export const labels = defineMessages({
ok: { id: 'label.ok', defaultMessage: 'OK' },
save: { id: 'label.save', defaultMessage: 'Save' },
cancel: { id: 'label.cancel', defaultMessage: 'Cancel' },
// ...
});
export const messages = defineMessages({
error: { id: 'message.error', defaultMessage: 'Something went wrong.' },
saved: { id: 'message.saved', defaultMessage: 'Saved successfully.' },
activeUsers: {
id: 'message.active-users',
defaultMessage: '{x} current {x, plural, one {visitor} other {visitors}}',
},
// ...
});
Sources: src/components/messages.ts:3-585
Language File Structure
The project uses two primary directories for language files, each serving a distinct purpose in the translation workflow.
| Directory | Purpose | Format | Example File |
|---|---|---|---|
src/lang/ | Source files for translators. Contains simple key-value pairs. | Plain JSON | en-US.json |
public/intl/messages/ | Compiled files used by the application at runtime. | ICU Message Format | en-US.json |
Sources: src/lang/en-US.json, public/intl/messages/en-US.json
src/lang/*.json: These files are human-readable and intended for translators. Each file represents a locale (e.g.,en-US.json,en-GB.json) and contains a flat JSON object mapping message IDs to their translated strings.public/intl/messages/*.json: These are the compiled output of the build process. They are structured in the ICU (International Components for Unicode) Message Format, whichreact-intluses to handle complex translations, including pluralization and variable interpolation. These files are served to the client.
Configuration
The default locale for the application is set to en-US. This constant is used as a fallback and for initial setup.
// src/lib/constants.ts:24
export const DEFAULT_LOCALE = 'en-US';
Sources: src/lib/constants.ts:24
The user's selected locale is stored in localStorage under the key umami.locale.
// src/lib/constants.ts:4
export const LOCALE_CONFIG = 'umami.locale';
Sources: src/lib/constants.ts:4
Build & Translation Workflow
The process of managing translations from source code to the final application is automated through a series of npm scripts defined in package.json. The main script, build-lang, orchestrates the entire workflow.
This diagram illustrates the flow of data through the various scripts to produce the final compiled language files.
Key Scripts
The i18n workflow is managed by several scripts:
| Script | Description |
|---|---|
extract-messages | Uses @formatjs/cli to scan src/components/messages.ts and extract all message descriptors into build/extracted-messages.json. |
merge-messages | A custom script (scripts/merge-messages.js) that reads build/extracted-messages.json and merges any new message keys into the existing src/lang/*.json files. It uses the defaultMessage for the new keys. |
format-lang | A custom script (scripts/format-lang.js) that transforms the simple key-value files in src/lang into the structured format required by FormatJS, outputting them to build/messages. |
compile-lang | Uses @formatjs/cli to compile the structured JSON files from build/messages into the final, optimized ICU message format in public/intl/messages. |
download-language-names | A script that downloads language name translations from a public repository to supplement the i18n assets. |
Sources: package.json:26-34, scripts/merge-messages.js, scripts/format-lang.js, scripts/download-language-names.js
Step-by-Step Workflow
- Extraction: The
extract-messagesscript is run to find alldefineMessagescalls and create a JSON file of all message IDs and their default messages. - Merging: The
merge-messagesscript adds any new keys found during extraction to eachsrc/lang/*.jsonfile, ensuring translators can see and translate new text. Foren-US.json, it ensures the value matches thedefaultMessage. - Formatting: The
format-langscript converts the simplesrc/langfiles into a verbose, object-based format that the compiler expects. - Compilation: The
compile-langscript takes the formatted files and produces the final, compact ICU-formatted files inpublic/intl/messages. These are the files loaded by the application.
Usage in Components
To use the translated messages in React components, the application provides a custom hook, useMessages.
The useMessages Hook
This hook is a wrapper around react-intl's useIntl hook. It provides a convenient API for formatting messages, accessing the predefined labels and messages objects, and handling API error messages.
The following diagram shows how a component uses the hook to render a translated message.
Sources: src/components/hooks/useMessages.ts
The hook returns an object containing:
formatMessage: A function to format a message descriptor into a string.messages: The importedmessagesobject fromsrc/components/messages.ts.labels: The importedlabelsobject fromsrc/components/messages.ts.getErrorMessage: A utility to translate API error codes into user-friendly messages.
Implementation of useMessages hook:
// src/components/hooks/useMessages.ts:16
export function useMessages(): UseMessages {
const intl = useIntl();
// ...
const formatMessage = (
descriptor: MessageDescriptor,
values?: Record<string, string | number | boolean | null | undefined>,
opts?: any,
) => {
return descriptor ? intl.formatMessage(descriptor, values, opts) : null;
};
return { formatMessage, messages, labels, getMessage, getErrorMessage, FormattedMessage };
}