Skip to content

Translations (i18n)

Fullfinity supports multi-language translations using an approach where the English source text serves as the translation key.

  • Backend-driven: All translations are managed on the backend and served to the frontend
  • English-text-as-key format: Translation files use {"English Text": "Translated Text"} format
  • AI-powered: Translations are generated using OpenAI or Anthropic APIs
  • Per-module: Each module has its own i18n/ folder with translation files

Translation files are stored in each module’s i18n/ directory:

modules/crm/
├── models/
├── views/
└── i18n/
├── es.json # Spanish
├── fr.json # French
└── de.json # German

Each file uses English text as the key:

{
"Customer": "Cliente",
"Expected Revenue": "Ingresos esperados",
"Mark as Won": "Marcar como ganado",
"Leads in this stage are considered won": "Los leads en esta etapa se consideran ganados"
}

The fullfinity-translate CLI extracts translatable strings and generates translations.

Terminal window
# Extract strings from a module (shows counts)
./fullfinity-translate -c config.yaml --extract fullfinity/modules/crm
# Translate a single module to Spanish
./fullfinity-translate -c config.yaml fullfinity/modules/crm es
# Translate all modules to multiple languages
./fullfinity-translate -c config.yaml fullfinity/modules es,fr,de
# Translate to all supported languages
./fullfinity-translate -c config.yaml --all fullfinity/modules/invoicing
# List supported languages
./fullfinity-translate --list-languages

React t() strings are extracted from app/src and stored with the core module (modules/core/i18n/<lang>.json), because all translations are served from the backend. They are translated automatically whenever core is within the path you pass, e.g.:

Terminal window
./fullfinity-translate -c config.yaml fullfinity/modules es # all modules + frontend
./fullfinity-translate -c config.yaml fullfinity/modules/core es # core + frontend

Generated i18n/*.json files are not read at runtime — they’re loaded into the Translation table. After generating, reload them:

  • UI: Languages → pick the language → Generate translations (generates + loads in one step), or Reload translations to just load existing files.
  • These map to Language.action_generate_translations / action_load_translations. Loading only happens for installed modules.
CodeLanguage
esSpanish
frFrench
deGerman
pt-BRPortuguese (Brazil)
zh-CNChinese (Simplified)
jaJapanese
arArabic
itItalian
nlDutch
ruRussian
koKorean
hiHindi
trTurkish
plPolish
viVietnamese

Add an AI API key to your config.yaml:

# Use either OpenAI or Anthropic
OPENAI_API_KEY: "sk-proj-..."
# or
ANTHROPIC_API_KEY: "sk-ant-..."

The CLI extracts:

  • Field descriptions: description="Customer Name"
  • Field hints: hint="Enter the customer's full name"
  • Error messages: raise ValidationError("Invalid email format")
class Contact(Model):
name = Char(
description="Contact Name", # Extracted
hint="Full name of the contact", # Extracted
max_length=255
)

The CLI extracts these keys from view definitions:

  • title - Section/tab titles
  • label - Field labels
  • description - Descriptions
  • placeholder - Input placeholders
  • hint - Help text
  • message - Messages
  • name - Only from UiMenu and WindowAction (display names)
{
"type": "field",
"name": "contact",
"properties": {
"label": "Customer",
"placeholder": "Select a customer",
"hint": "The customer for this order"
}
}

Strings wrapped in the t() function are extracted. The English text is the key — pass it as the first argument:

t("Save Changes") // key + fallback in one
t("Saved {count} records", { count }) // with interpolation params

Do not invent a separate identifier key, e.g. t("save_changes", "Save Changes"). The whole pipeline (extraction, i18n/*.json, the Translation.key/source columns, and the served lookup dict) is keyed by the English source text, so a synthetic key like "save_changes" is never present at runtime and the string silently falls back to English. Keying by the English text is also collision-free — the source string is already unique, whereas a lowercased/underscored identifier can collapse distinct strings together ("Sign up" and "Sign-up").

The translate_arch() function translates view architectures based on user language:

from fullfinity.engine.translation import translate_arch, get_translations_dict
# Get translations for Spanish
translations = await get_translations_dict(env, "es")
# Translate a view
translated_arch = translate_arch(view.arch, translations)

Use the useTranslation hook:

import { useTranslation } from '../contexts/TranslationContext';
function MyComponent() {
const { t } = useTranslation();
return (
<Button>{t("Save Changes")}</Button>
);
}
  1. In Python models, use description= and hint=:
name = Char(description="Product Name", hint="Enter product name")
  1. In JSON views, use translatable keys:
{
"type": "fieldset",
"title": "Customer Information",
"content": [...]
}
  1. In React components, wrap with t():
<Button>{t("Submit Order")}</Button>
  1. Run the translation CLI to generate translations:
Terminal window
./fullfinity-translate -c config.yaml fullfinity/modules/mymodule es

The CLI only translates new strings. Existing translations are preserved:

Terminal window
# First run: translates all 50 strings
./fullfinity-translate -c config.yaml fullfinity/modules/crm es
# Output: Translating 50 new strings...
# Add new fields, run again: only new strings translated
./fullfinity-translate -c config.yaml fullfinity/modules/crm es
# Output: Translating 3 new strings...
  1. Use descriptive English text - The English text becomes the key, so make it clear and unique

  2. Avoid programmatic strings - Don’t translate identifiers, field names, or code

  3. Review AI translations - AI translations are good but may need manual review for domain-specific terms

  4. Translate early and often - Run translations as part of your development workflow

  5. Keep translations in version control - The i18n/*.json files should be committed