Skip to content

Internationalisation

Ed Mozley edited this page Aug 7, 2026 · 7 revisions

Internationalisation (i18n)

FreeITSM has native multi-language support built in — no framework, no build step, no external translation service. UI strings are looked up at render time through a tiny t('namespace.key') helper that works identically in PHP and JavaScript, with automatic per-key fallback to English.

The active language follows the logged-in analyst's preference (set in System → Preferences), so two analysts on the same install can use the product in different languages.

Supported locales (23)

en English af Afrikaans fr Français de Deutsch
es Español pt-BR Português (BR) nl Nederlands it Italiano
pl Polski ru Русский uk Українська id Bahasa Indonesia
nb Norsk bokmål nn Norsk nynorsk hi हिन्दी bn বাংলা
ta தமிழ் te తెలుగు mr मराठी kn ಕನ್ನಡ
ml മലയാളം gu ગુજરાતી pa ਪੰਜਾਬੀ

Norwegian ships as both written standards. Bokmål and Nynorsk are separate locales rather than one "Norwegian", and they are listed under their native names because a Nynorsk reader offered only "Norsk" cannot tell which one they will get.

Adding a language is a code change (add it to I18n::SUPPORTED_LOCALES and create the lang/<code>/ folder) — deliberate, so the locale parameter can never be used for path traversal. Locale codes follow BCP 47 (the same form used in the HTML lang attribute).

Doing it in practice: see Adding a Language — developer guide for the fan-out workflow that translates all 23 modules, and the four verification checks that catch what the silent per-key fallback hides.

How it works

  • Lang files live at lang/<locale>/<module>.php and return a nested PHP array. The first dot-segment of a t() key is the filename; the rest walks the array. t('cmdb.nav.browse')lang/<locale>/cmdb.php['nav']['browse'].
  • PHP calls t('cmdb.nav.browse') (or htmlspecialchars(t(...)) for HTML output). Defined in includes/i18n.php.
  • JavaScript calls window.t('cmdb.nav.browse'). The host page injects window.translations from I18n::exportForJs([...]) with the English fallback already merged in per key; the JS bridge (assets/js/i18n.js) just walks the dotted path.
  • Per-key fallback. If a locale file is missing a key (or doesn't exist at all), that single string falls back to English — everything else in the locale still applies. A completely unfilled key surfaces as the literal key, so gaps are visible during development rather than rendering blank.
  • Interpolation. t('common.welcome', ['name' => 'Ed']) substitutes {name}. Unknown placeholders are left intact.
  • Locale resolution (priority order): analyst's interface_language preference → browser Accept-Language (best supported match, primary-subtag fallback) → en. Changing it in Preferences persists to user_preferences and reloads the page.
  • Security. Namespace identifiers are regex-validated; locale codes are checked against the supported list before being used as a path; the JS bridge JSON is emitted with JSON_HEX_* flags to prevent script-tag injection from a translation value.

Module coverage

🇧🇷 Português (Brasil) — 100% complete (2026-06-28). pt-BR has been brought to full key parity with English across all 23 modules: the six modules that had no pt-BR file (System, Contracts, Network Mapper, Self-Service, System Wiki, Setup) were translated, and every later-added key in the already-translated modules (notably the Tickets/Tasks/Workflows/Process-Mapper in-app help & SLA documentation) was filled in. Verified lint-clean with zero missing keys and zero placeholder mismatches. The per-locale tracker below still reflects the broader multi-locale rollout.

Internationalising a module happens in two phases: first its pages are wired (strings extracted to keys, the t() / window.t() bridge added), then the 19 locale files are generated. The tracker below shows both at a glance.

Module 🔧 Pages wired 🌍 Translations (19 locales)
Watchtower █████ 2 / 2 █████ 19 / 19
Tickets █████ 11 / 11 █████ 19 / 19
Tasks █████ 6 / 6 █████ 19 / 19
Assets █████ 7 / 7 █████ 19 / 19
Knowledge █████ 4 / 4 █████ 19 / 19
Change Management █████ 6 / 6 █████ 19 / 19
Calendar █████ 4 / 4 █████ 19 / 19
Morning Checks █████ 3 / 3 █████ 19 / 19
Reporting █████ 5 / 5 █████ 19 / 19
🟡 Software █████ 6 / 6 █████ 18 / 19
🟡 Forms █████ 6 / 6 ████░ 17 / 19
Service Status █████ 3 / 3 █████ 19 / 19
LMS █████ 3 / 3 █████ 19 / 19
Process Mapper █████ 3 / 3 █████ 19 / 19
CMDB █████ 4 / 4 █████ 19 / 19
Workflows █████ 4 / 4 █████ 19 / 19
🟡 System █████ 11 / 11 ░░░░░ 0 / 19
🟡 Network Mapper █████ 3 / 3 ░░░░░ 0 / 19
🟡 Self-Service █████ 6 / 6 ░░░░░ 0 / 19
🟡 Contracts █████ 21 / 21 ░░░░░ 0 / 19
🟡 System Wiki █████ 7 / 7 ░░░░░ 0 / 19
🟡 Setup █████ 1 / 1 ░░░░░ 0 / 19

Legend — ✅ fully done · 🟡 in progress · ⬜ not started. Pages wired counts full-document pages initialised for i18n (data pages such as uploaded course/SCORM content are excluded); Translations counts the non-English lang/<locale>/<module>.php files present, out of 19. Per-key English fallback means any gap simply shows English, so nothing ever renders blank. Updated 2026-06-03 — English page-wiring is now complete for every module. Contracts, System Wiki and the Setup installer were the final three to convert. The only remaining i18n work is translations: System, Network Mapper, Self-Service, Contracts, System Wiki and Setup still need their 19 locale files, and Software/Forms have 1–2 Indic locales left.

What is not translated, by design: user-authored and configurable data — CMDB CI values and user-defined class/property names, knowledge-article content, supplier/contract records, incident update text, etc. These are data, not UI chrome, so they always display as entered.

Converting a module

The pattern is mechanical and mirrors any already-converted module (Tasks is the canonical reference):

  1. Extract every user-facing string into lang/en/<module>.php as a grouped nested array (nav, list, settings, toast, help, …).
  2. Wire each full-HTML page: require_once 'includes/i18n.php'; I18n::initFromSession();, set $translationNamespaces = ['common', '<module>'], set <html lang="<?php echo htmlspecialchars(I18n::getLocale()); ?>">, and emit the window.translations + i18n.js block in <head>.
  3. Replace literals — PHP <?php echo htmlspecialchars(t('<module>.key')); ?>, JS window.t('<module>.key', {param}).
  4. Add the locale files lang/<locale>/<module>.php for the other 19 languages. Missing keys fall back to English, so a partial file is always valid — you can ship incrementally.

Reuse the shared common.* namespace for cross-cutting strings (buttons, calendar month/weekday names) rather than duplicating them per module.

Files

Path Purpose
includes/i18n.php I18n class + global t(); locale resolution, fallback, interpolation, exportForJs()
assets/js/i18n.js Client-side window.t() mirroring the PHP contract
lang/<locale>/<module>.php Per-locale, per-module translation arrays
lang/<locale>/common.php Shared strings (buttons, calendar primitives, module names, account menu)

FreeITSM

Getting Started

Modules

Multi-tenancy (planned)

Blue sky thinking

Bugs resolved

Links

Clone this wiki locally