# Pdfstudio Impl I18next Solid

> 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.

- Skill: `impertio-studio/pdfstudio-impl-i18next-solid` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add impertio-studio/pdfstudio-impl-i18next-solid`
- Raw SKILL.md: https://api.skillmd.com/api/skills/impertio-studio/pdfstudio-impl-i18next-solid/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- License: MIT
- Author: Impertio-Studio (https://skillmd.com/u/impertio-studio)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/impertio-studio/pdfstudio-impl-i18next-solid

---


# 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

```javascript
// js/i18n/config.js — init options
{
  ns: ['common', 'ribbon', 'preferences', 'dialogs', 'appMenu', 'properties', 'context', 'statusbar'],
  defaultNS: 'common',
  fallbackLng: 'en',
  interpolation: { escapeValue: false },
  detection: {
    order: ['localStorage', 'navigator'],
    lookupLocalStorage: 'i18nextLng',
    caches: []           // No caching — detection runs fresh each time
  }
}
```

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

```javascript
// js/i18n/useTranslation.js
const [language, setLanguage] = createSignal(i18next.language || 'en');

i18next.on('languageChanged', (lng) => {
  setLanguage(lng);                                         // Triggers SolidJS reactivity
  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();       // CRITICAL: creates reactive dependency
    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`:
```javascript
export const RTL_LANGUAGES = ['ar', 'fa', 'he', 'ur'];
```

### How RTL is Applied

When language changes, `useTranslation.js` sets `dir` and `lang` attributes on `<html>`:
```javascript
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)

```jsx
import { useTranslation } from '../../i18n/useTranslation.js';

function MyComponent() {
  const { t } = useTranslation('ribbon');
  return <button>{t('save')}</button>;   // Reactive — re-renders on language change
}
```

### In SolidJS with Multiple Namespaces

```jsx
const { t } = useTranslation(['dialogs', 'common']);
// Primary namespace is dialogs, fallback uses i18next's ns resolution
```

### In Vanilla JS (No Reactivity)

```javascript
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

1. **Determine the namespace** — Which of the 8 namespaces does this key belong to?
2. **Add to English first** — Add the key to `js/i18n/locales/en/{namespace}.json`
3. **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.
4. **Use interpolation for dynamic values** — Use `{{variable}}` syntax: `"greeting": "Hello, {{name}}!"`
5. **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

1. **Create locale directory**: `js/i18n/locales/{code}/`
2. **Create all 8 namespace files** — Copy from `en/` as starting point
3. **Add 8 static imports** to `config.js` (one per namespace)
4. **Add resource entry** in the `i18next.init({ resources: { ... } })` block
5. **Add entry to `LANGUAGES` array** with `code`, `name` (native), `englishName`, and optionally `dir: 'rtl'`
6. **If RTL**: Add the language code to `RTL_LANGUAGES` array
7. **If non-Western digits**: Add digit conversion logic in `useTranslation.js`

### The `changeLanguage` Function

```javascript
export function changeLanguage(lang) {
  if (lang === 'auto') {
    // Re-detect from browser, validate against supported languages, fallback to 'en'
    const detected = i18next.services.languageDetector.detect();
    const baseLang = resolvedLang.split('-')[0];    // 'en-US' → 'en'
    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

1. **ALWAYS update ALL 37 language files** when adding a new translation key
2. **NEVER use `i18next.changeLanguage()` directly** — use the exported `changeLanguage()` from `useTranslation.js`
3. **ALWAYS read `language()` inside the `t()` function** to maintain SolidJS reactivity
4. **NEVER lazy-load languages** — the architecture requires static imports for all bundles
5. **ALWAYS use `localizeNumber()` for standalone numbers** in Farsi/Arabic contexts
6. **NEVER add nested keys** to translation JSON files — use flat structures only
7. **ALWAYS add `dir: 'rtl'` to the `LANGUAGES` entry** for any new RTL language
8. **ALWAYS add new RTL language codes to `RTL_LANGUAGES`** array in `config.js`

