Use when modifying translations or language handling in open-pdf-studio. Prevents the common mistake of adding translations without updating all 37 language files or breaking the custom useTranslation SolidJS hook. Covers i18next configuration, 8 namespaces, RTL support, Farsi/Arabic digit conversion, and the SolidJS signal bridge for reactive translations. Keywords: i18next, SolidJS, useTranslation, translations, RTL, namespaces, language detection, Farsi digits, localization, i18n, add new language, translation not showing, language switch, multi-language.
Use when modifying translations or language handling in open-pdf-studio. Prevents the common mistake of adding translations without updating all 37 language files or breaking the custom useTranslation SolidJS hook. Covers i18next configuration, 8 namespaces, RTL support, Farsi/Arabic digit conversion, and the SolidJS signal bridge for reactive translations. Keywords: i18next, SolidJS, useTranslation, translations, RTL, namespaces, language detection, Farsi digits, localization, i18n, add new language, translation not showing, language switch, multi-language.
license
MIT
compatibility
Designed for Claude Code. Specific to open-pdf-studio (i18next 25.x, SolidJS 1.9).
metadata
{"author":"OpenAEC-Foundation","version":"1.0"}
i18next + SolidJS Integration in Open PDF Studio
Architecture Overview
Open PDF Studio uses i18next 25.x for internationalization, bridged to SolidJS 1.9 reactivity through a custom useTranslation hook. All 37 languages are statically imported and bundled at build time — there is NO lazy loading.
Key Files
File
Role
js/i18n/config.js
i18next initialization, all 37 language bundles, LANGUAGES array, RTL_LANGUAGES, isRTL()
js/i18n/useTranslation.js
SolidJS signal bridge: useTranslation(), changeLanguage(), localizeNumber(), digit conversion
js/i18n/locales/{lang}/*.json
Translation files — 8 JSON files per language, ~296 files total
fallbackLng: 'en' — English is the fallback for ALL missing translations
escapeValue: false — No HTML escaping (safe because SolidJS handles escaping)
Detection order: localStorage first, then browser navigator language
caches: [] — Language detection result is NOT cached by i18next-browser-languagedetector
The SolidJS Signal Bridge
Why a Custom Hook Exists
i18next is imperative — it has no built-in SolidJS integration. The useTranslation hook bridges this gap by using a SolidJS createSignal to track the current language. When language() is read inside a SolidJS component's JSX, it creates a reactive dependency that triggers re-rendering on language change.
The language() call inside t() looks unused (its return value feeds into convertDigits, but even without digit conversion it would be needed). Reading language() is what creates the SolidJS reactive subscription. Without it, components would NOT re-render when the language changes.
RTL Support
RTL Languages
4 languages require right-to-left layout: ar (Arabic), fa (Farsi), he (Hebrew), ur (Urdu).
CSS throughout the app MUST use logical properties (margin-inline-start instead of margin-left) to support RTL correctly.
Farsi and Arabic Digit Conversion
Mechanism
Western digits (0-9) in translation strings are automatically converted to locale-specific numerals for Farsi and Arabic:
Language
Digits
Example
Farsi (fa)
['۰','۱','۲','۳','۴','۵','۶','۷','۸','۹']
Page 3 → صفحه ۳
Arabic (ar)
['٠','١','٢','٣','٤','٥','٦','٧','٨','٩']
Page 3 → صفحة ٣
Exported Helper
localizeNumber(num) is exported for use outside the t() function — ALWAYS use this when displaying numbers in the UI that are not part of a translation string.
Usage Patterns
In SolidJS Components (Correct)
import { useTranslation } from'../../i18n/useTranslation.js';
functionMyComponent() {
const { t } = useTranslation('ribbon');
return<button>{t('save')}</button>; // Reactive — re-renders on language change
}
In SolidJS with Multiple Namespaces
const { t } = useTranslation(['dialogs', 'common']);
// Primary namespace is dialogs, fallback uses i18next's ns resolution
Vanilla JS code imports i18next directly from config.js. This does NOT create reactive subscriptions — the translation is resolved once at call time. This is correct for imperative code (error messages, logging).
Adding a New Translation Key
Step-by-Step Procedure
Determine the namespace — Which of the 8 namespaces does this key belong to?
Add to English first — Add the key to js/i18n/locales/en/{namespace}.json
Add to ALL 37 languages — ALWAYS add the key to every language file, even if the value is the English fallback. While i18next falls back to English for missing keys, incomplete files cause confusion for translators.
Use interpolation for dynamic values — Use {{variable}} syntax: "greeting": "Hello, {{name}}!"
NEVER nest keys — All translation files in this project use flat key structures
Naming Conventions for Keys
Use camelCase: saveAsDialog, pageNotFound
Be descriptive: confirmDeleteAnnotation not confirm1
Prefix with context when ambiguous: ribbonSave vs dialogSave
Adding a New Language
Step-by-Step Procedure
Create locale directory: js/i18n/locales/{code}/
Create all 8 namespace files — Copy from en/ as starting point
Add 8 static imports to config.js (one per namespace)
Add resource entry in the i18next.init({ resources: { ... } }) block
Add entry to LANGUAGES array with code, name (native), englishName, and optionally dir: 'rtl'
If RTL: Add the language code to RTL_LANGUAGES array
If non-Western digits: Add digit conversion logic in useTranslation.js
The 'auto' option strips region codes (en-US → en) and validates against actually bundled languages. ALWAYS use this function instead of calling i18next.changeLanguage() directly.
Critical Rules
ALWAYS update ALL 37 language files when adding a new translation key
NEVER use i18next.changeLanguage() directly — use the exported changeLanguage() from useTranslation.js
ALWAYS read language() inside the t() function to maintain SolidJS reactivity
NEVER lazy-load languages — the architecture requires static imports for all bundles
ALWAYS use localizeNumber() for standalone numbers in Farsi/Arabic contexts
NEVER add nested keys to translation JSON files — use flat structures only
ALWAYS add dir: 'rtl' to the LANGUAGES entry for any new RTL language
ALWAYS add new RTL language codes to RTL_LANGUAGES array in config.js