Internationalization (i18n)
NRP Invenio repositories support full internationalization (i18n) across Python backend, Jinja templates, and React front-end. This guide explains how translations are created, extracted, organized, and maintained.
Internationalization in NRP-based repositories is built on:
- Flask-Babel / invenio-i18n for Python & Jinja.
- react-i18next for React applications.
- invenio-cli translation tooling for extracting and updating
.pofiles.
Translations ultimately live in standard .po files stored in your repository.
Overview
NRP Invenio repositories support multilingual UI by combining:
- Python/Jinja message extraction via
./run.sh cli translations - Manual message collection for templates or components not automatically detected
- Unified
.pofiles where all translation keys end up
Initial Translation Setup
When first working with translations in your repository, follow these steps:
-
Extract translatable strings from your codebase:
./run.sh cli translations extract -
Initialize the catalog for your desired language(s):
./run.sh cli translations init -l cs ./run.sh cli translations init -l deThis will initialize translation catalogs for Czech (
cs), German (de) or other languages. -
Configure the supported languages in your
invenio.cfg:# Set default language I18N_LANGUAGES = [ ("en", "English"), ("cs", "Čeština"), ("de", "Deutsch"), ] -
Edit the
.pofiles with POEdit or your preferred translation editor to add translations for the extracted strings. -
Compile the translations to generate
.mofiles:./run.sh cli translations compile
New languages will then be available in the language selector on your repository’s UI.
Day-to-Day Workflow
Write translatable strings
Use _() or lazy_gettext() in Python, {% trans %} in Jinja to mark any strings for translation.
Extract translations
Run ./run.sh cli translations extract to find any newly added strings and include them in the translations catalog.
Update language .po files
Run ./run.sh cli translations update to merge newly extracted strings into existing .po files without losing existing translations. This is safe to run repeatedly as you add new translatable strings.
Edit translations
Use POEdit or another .po editor to provide translations for extracted messages.
Compile translations
Build .mo files using ./run.sh cli translations compile.
Python: Creating localisable strings
In Python code (views, resources, models, UI resources), import the gettext utilities:
from flask_babel import gettext as _
# or
from flask_babel import lazy_gettext as _Use them to wrap user-visible strings:
title = _("Datasets")
description = _("Create, publish, and manage datasets.")gettext for immediate translation and lazy_gettext for delayed (lazy) translation (especially for use in configuration or class attributes).Jinja Templates: Localisable strings
Two options exist for Jinja:
- Inline
_()calls
<h1>{{ _("Search results") }}</h1>{% trans %}block Recommended for longer, more complex text blocks:
{% trans %}
This dataset is part of the national metadata directory.
{% endtrans %}Extracting & Managing Translations
NRP repositories use the Invenio CLI tooling for extraction and compilation.
Update or extract new strings
./run.sh cli translations extractor
./run.sh cli translations updateThis:
- extracts Python strings via Babel
- extracts Jinja strings
- extracts JinjaX strings
- merges everything into .po files under:
translations/<lang>/LC_MESSAGES/messages.po
Add a new language
To add support for a new language after initial setup:
./run.sh cli translations init -l skThis will initialize translation catalog for additional Slovak (sk) locale.
Editing translations (.po files)
Editing .po files is done using a tool like POEdit, which provides:
- side-by-side source & translation
- validation of syntax
- search & filter
- plural forms
After updating translations, run:
./run.sh cli translations compileThis generates .mo translation files used at runtime.
Strings that cannot be automatically extracted
Some contexts—especially JinjaX components or certain dynamic template fragments cannot be detected automatically by Babel extractors.
For that purpose, place a jinjax_messages.jinja file in any registered templates/ folder (e.g., ui/mymodule/templates/). All registered templates are scanned during extraction, so the file can be located anywhere in your templates directory structure.
Inside this file, place any strings you want to get extracted for translation:
{{ _("Dataset") }}
{{ _("Add new item") }}
{{ _("Advanced search") }}These strings will be picked up by the extractor during:
./run.sh cli translations extractThink of this file as a “translation collector” for stray strings that cannot get extracted automatically.
Best Practices
- Wrap all user-facing texts in
_()or{% trans %}. - Keep translation keys stable to avoid unnecessary retranslation.
- Use sentence-level translation keys rather than fragments.
- Avoid concatenating translatable strings.
- Add unextractable strings into
jinjax_messages.jinja.
React: Localization with react-i18next
React components use the react-i18next library for translations, matching the architecture used in InvenioRDM frontends.
React translations have a separate workflow from Python/Jinja translations and must be configured independently. There is currently no automated tooling to create this structure. When adding React translations to a new module, you must manually set up the translation package directory, configuration files, and scripts, following the pattern from invenio-app-rdm .
Translation Package Structure
Each UI module that needs React translations follows this directory pattern:
- translations.json
- translations.json
- i18next-scanner.config.js
- i18next.js
- package.json
- webpack.py
The key files are:
| File | Purpose |
|---|---|
messages/{{lng}}/translations.json | Language-specific translation resources for React |
i18next.js | Initializes i18next with configuration |
i18next-scanner.config.js | Configures extraction of translation keys from JSX/JS |
package.json | Defines scripts for extracting and compiling translations |
Webpack Alias Configuration
In your webpack.py, add an alias pointing to the translation package root:
from invenio_assets.webpack import WebpackThemeBundle
theme = WebpackThemeBundle(
__name__,
".",
default="semantic-ui",
themes={
"semantic-ui": dict(
dependencies={},
devDependencies={},
aliases={
"@translations/my_module": "translations/my_module"
},
)
},
)This alias allows React components to import and use the i18next instance:
import i18next from "@translations/my_module/i18next";i18next Configuration
The i18next.js file configures the i18next instance:
import i18n from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import { translations } from "./messages";
import { initReactI18next } from "react-i18next";
const options = {
fallbackLng: "en",
returnEmptyString: false,
debug: process.env.NODE_ENV === "development",
resources: translations,
keySeparator: false,
nsSeparator: false,
detection: {
order: ["htmlTag"],
caches: [],
},
react: {
transKeepBasicHtmlNodesFor: [],
},
};
const i18next = i18n.createInstance();
i18next.use(LanguageDetector).use(initReactI18next).init(options);
export { i18next };i18next Scanner Configuration
The i18next-scanner.config.js file configures how translation keys are extracted from your React code. This is based on the invenio-app-rdm configuration :
// Read languages from package.json config
const { languages } = require("./package.json").config;
const funcList = ["i18next.t"];
const extensions = [".js", ".jsx"];
module.exports = {
options: {
debug: true,
removeUnusedKeys: true,
browserLanguageDetection: true,
func: {
list: funcList,
extensions: extensions,
},
// Using Trans component
trans: {
component: "Trans",
extensions: extensions,
fallbackKey: function (ns, value) {
return value;
},
},
lngs: languages,
ns: ["translations"],
defaultLng: "en",
defaultNs: "translations",
defaultValue: function (lng, ns, key) {
if (lng === "en") {
return key; // Return key as default for English
}
return "";
},
resource: {
loadPath: "messages/{{lng}}/{{ns}}.json",
savePath: "messages/{{lng}}/{{ns}}.json",
jsonIndent: 2,
lineEnding: "\n",
},
nsSeparator: false,
keySeparator: false,
},
};Using Translations in React Components
Mark strings for translation by importing i18next and using the t() function:
import i18next from "@translations/my_module/i18next";
export function MyComponent() {
return (
<div>
<h1>{i18next.t("welcome_message")}</h1>
<p>{i18next.t("description")}</p>
</div>
);
}Or use the Trans component for complex translations with HTML:
import { Trans } from "react-i18next";
import i18next from "@translations/my_module/i18next";
export function MyComponent() {
return (
<Trans i18n={i18next}>
Learn more about <a href="/docs">our documentation</a>.
</Trans>
);
}Extracting and Updating React Translations
The translation package’s package.json defines NPM scripts for managing translations (as seen in invenio-app-rdm ):
{
"name": "my-module-ui",
"config": {
"languages": ["en", "cs"]
},
"devDependencies": {
"i18next-conv": "^10.2.0",
"i18next-scanner": "^3.0.0",
"react-i18next": "^11.11.3"
},
"scripts": {
"extract_messages": "i18next-scanner --config i18next-scanner.config.js '../../js/**/**/*.{js,jsx}' '!../../js/**/node_modules/**'",
"postextract_messages": "node ./scripts/postExtractMessages.js",
"compile_catalog": "node ./scripts/compileCatalog.js",
"init_catalog": "node ./scripts/initCatalog"
}
}| Script | Purpose |
|---|---|
extract_messages | Scans JS/JSX files for translation keys using i18next-scanner |
postextract_messages | Post-processes extracted messages (merges with existing translations) |
compile_catalog | Compiles translation catalog for runtime use |
init_catalog | Initializes a new language catalog |
To extract new translation keys from your React code:
cd ui/mymodule/translations/my_module
npm run extract_messagesThis updates the messages/{{lng}}/translations.json files with any new keys found in your components. The post-extraction script runs automatically to merge new keys with existing translations.
To initialize a new language catalog:
npm run init_catalog