| name | pdfstudio-impl-i18next-solid |
| description | 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 |
Data Flow
i18next.init({ resources: { en: {...}, nl: {...}, ... } })
│
▼
i18next.on('languageChanged') ──► setLanguage(lng) ◄── SolidJS createSignal
│
▼
useTranslation(ns)
│
const lang = language() ◄── Creates reactive dependency
│
▼
i18next.t(key, { ns })
│
▼
convertDigits(result, lang) ◄── Farsi/Arabic numeral swap
The 37 Supported Languages
ALWAYS check this list when adding language support. The LANGUAGES array in config.js defines ALL supported languages:
ar, bn, bg, ca, zh, hr, cs, da, nl, en, fa, fi, fr, de, el, he, hi, hu, id, it, ja, ko, ms, nb, pl, pt, ro, ru, sr, sk, es, sw, sv, ta, th, tr, uk, ur, vi
The 8 Namespaces
EVERY language MUST have exactly these 8 JSON files in its locale directory:
| Namespace | File | Purpose |
|---|
common | common.json | Shared strings (save, cancel, open, errors) |
ribbon | ribbon.json | Ribbon toolbar labels |
preferences | preferences.json | Settings dialog strings |
dialogs | dialogs.json | Dialog box content |
appMenu | appMenu.json | Application menu items |
properties | properties.json | Properties panel labels |
context | context.json | Right-click context menu |
statusbar | statusbar.json | Status bar messages |
The default namespace is common. When calling useTranslation() without arguments, it uses common.
i18next Configuration Details
{
ns: ['common', 'ribbon', 'preferences', 'dialogs', 'appMenu', 'properties', 'context', 'statusbar'],
defaultNS: 'common',
fallbackLng: 'en',
interpolation: { escapeValue: false },
detection: {
order: ['localStorage', 'navigator'],
lookupLocalStorage: 'i18nextLng',
caches: []
}
}
Key settings:
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.
Hook Implementation
const [language, setLanguage] = createSignal(i18next.language || 'en');
i18next.on('languageChanged', (lng) => {
setLanguage(lng);
document.documentElement.setAttribute('dir', isRTL(lng) ? 'rtl' : 'ltr');
document.documentElement.setAttribute('lang', lng);
});
export function useTranslation(ns = 'common') {
const namespaces = Array.isArray(ns) ? ns : [ns];
const t = (key, options) => {
const lang = language();
const result = i18next.t(key, { ns: namespaces[0], ...options });
return convertDigits(result, lang);
};
return { t, i18n: i18next, language };
}
The language() Call is NOT Optional
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).
These are defined in config.js:
export const RTL_LANGUAGES = ['ar', 'fa', 'he', 'ur'];
How RTL is Applied
When language changes, useTranslation.js sets dir and lang attributes on <html>:
document.documentElement.setAttribute('dir', isRTL(lng) ? 'rtl' : 'ltr');
document.documentElement.setAttribute('lang', lng);
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';
function MyComponent() {
const { t } = useTranslation('ribbon');
return <button>{t('save')}</button>;
}
In SolidJS with Multiple Namespaces
const { t } = useTranslation(['dialogs', 'common']);
In Vanilla JS (No Reactivity)
import i18next from '../i18n/config.js';
showMessage(i18next.t('failedToLoadPdf', { error: error.message }));
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 changeLanguage Function
export function changeLanguage(lang) {
if (lang === 'auto') {
const detected = i18next.services.languageDetector.detect();
const baseLang = resolvedLang.split('-')[0];
const finalLang = supported.includes(baseLang) ? baseLang : 'en';
return i18next.changeLanguage(finalLang);
}
return i18next.changeLanguage(lang);
}
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