Skip to content

Internationalisation

Ed Mozley edited this page Jun 2, 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 (20)

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

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

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

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. ⏳ marks modules whose translations are being generated right now.

Module 🔧 Pages wired 🌍 Translations (19 locales)
🟡 Watchtower █████ 2 / 2 █████ 18 / 19 ⏳
🟡 Tickets ██░░░ 5 / 11 █████ 19 / 19
🟡 Tasks ████░ 5 / 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 ████░ 17 / 19 ⏳
🟡 Reporting █████ 5 / 5 █████ 18 / 19 ⏳
🟡 Software █████ 6 / 6 ░░░░░ 0 / 19 ⏳
🟡 Forms █████ 6 / 6 ░░░░░ 0 / 19 ⏳
Service Status █████ 3 / 3 █████ 19 / 19
🟡 LMS █████ 3 / 3 ███░░ 12 / 19 ⏳
🟡 Process Mapper ███░░ 2 / 3 █████ 19 / 19
CMDB █████ 4 / 4 █████ 19 / 19
🟡 Workflows ████░ 3 / 4 █████ 19 / 19
Contracts ░░░░░ 0 / 21 ░░░░░ 0 / 19
Network Mapper ░░░░░ 0 / 3 ░░░░░ 0 / 19
System ░░░░░ 0 / 11 ░░░░░ 0 / 19
Self-Service ░░░░░ 0 / 6 ░░░░░ 0 / 19

Legend — ✅ fully done · 🟡 in progress · ⬜ not started · ⏳ translations generating now. Pages wired counts full-document pages routed through t() (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. Live snapshot — 2026-06-02.

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