-
Notifications
You must be signed in to change notification settings - Fork 15
Internationalisation
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.
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).
-
Lang files live at
lang/<locale>/<module>.phpand return a nested PHP array. The first dot-segment of at()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')(orhtmlspecialchars(t(...))for HTML output). Defined inincludes/i18n.php. -
JavaScript calls
window.t('cmdb.nav.browse'). The host page injectswindow.translationsfromI18n::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_languagepreference → browserAccept-Language(best supported match, primary-subtag fallback) →en. Changing it in Preferences persists touser_preferencesand 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.
Fully translated across all 20 locales (every user-facing string in PHP and JavaScript):
Tickets · Tasks · Process Mapper · Workflows · Knowledge · Change Management · Asset Management · Calendar · Service Status · CMDB
The remaining modules (Contracts, Morning Checks, Watchtower, Reporting, Software, Forms, LMS, Network Mapper, Self-Service, System) render in English today and are converted module-by-module. Last updated: 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.
The pattern is mechanical and mirrors any already-converted module (Tasks is the canonical reference):
-
Extract every user-facing string into
lang/en/<module>.phpas a grouped nested array (nav,list,settings,toast,help, …). -
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 thewindow.translations+i18n.jsblock in<head>. -
Replace literals — PHP
<?php echo htmlspecialchars(t('<module>.key')); ?>, JSwindow.t('<module>.key', {param}). -
Add the locale files
lang/<locale>/<module>.phpfor 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.
| 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 — an open-source IT Service Management platform · github.com/edmozley/freeitsm · MIT licence
- Installation
- ⏰ Scheduled tasks (cron jobs)
- Architecture
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- Theming & Dark Mode
- ⌨️ Command palette (⌘K)
- 🔍 Searching inside tickets
- 📄 Attached documents
- Mobile‑Friendly
-
Security
- Layer 1 — which modules you can enter
- ↳ 🧩 Module Access Control
- ↳ 🛠️ Module Access — Developer Guide
- Layer 2 — what you can administer
- ↳ 🎭 Roles & Permissions
- ↳ 🛠️ Roles — Developer Guide
- ↳ 🔤 Why capabilities are constants
- Layer 3 — the System module
- ↳ 🔑 Admin Access Control
- Hardening
- ↳ 📄 Security review response 2026-08
- ↳ 🛡️ Security hardening 2026-08
- ↳ 🛠️ Security hardening 2026-08 — Developer Guide
- ↳ 🛡️ Round three — plain English
- ↳ 🛠️ Round three — Developer Guide
- Single Sign-On (SSO)
- 🗂️ LDAP & Active Directory
- Browser Extension
- API Reference
-
🔌 REST API — how it works
- ↳ 🎫 REST API: Tickets
- ↳ 💻 REST API: Assets
- ↳ 🔴 REST API: Problems
- ↳ 🟠 REST API: Changes
- ↳ 📚 REST API: Knowledge
- ↳ ✅ REST API: Tasks
- ↳ 🗄️ REST API: CMDB
- ↳ 📜 REST API: Contracts
- ↳ 🗓️ REST API: Calendar
- ↳ 💿 REST API: Software
- ↳ 🚦 REST API: Service Status
- ↳ ☀️ REST API: Morning Checks
- ↳ 📝 REST API: Forms
- ↳ ⚙️ REST API: Workflow
- ↳ 🗺️ REST API: Network Mapper
- ↳ 🧭 Using the API docs page
- ↳ 📐 OpenAPI specification
- ↳ ✅ OpenAPI: kept correct
- ↳ 🛠️ Maintaining the catalogue
- Watchtower
-
Tickets
- ↳ Mailbox Authentication
- ↳ 📤 Email send log
- ↳ Basic IMAP mailboxes
- ↳ Email rendering & images
- ↳ SLA Management
- ↳ WhatsApp channel
- ↳ 💬 Web chat channel
- ↳ 🟣 Slack channel
- ↳ 🔗 Linking tickets
- ↳ 🗒️ Canned responses
- ↳ ✉️ Limiting replies to particular senders
- ↳ ✍️ Email signatures
- ↳ 🌐 The public web address
- ↳ 🙋 Raising a ticket for someone else
- ↳ 🔀 Merging tickets
- ↳ ⑂ Splitting tickets
- ↳ ✅ Selecting several tickets
- ↳ 🛠️ Snoozing tickets — Developer Guide
- ↳ 👥 Collision detection
- ↳ ⏱️ Time tracking
- Problem Management
- Tasks
- Assets
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
- Forms
- Contracts
- Service Status
- 🔔 Notifications
- 🚨 War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- System
-
Overview
- ↳ 📊 Progress tracker
- ↳ Concepts & vocabulary
- ↳ Email routing & mailboxes
- ↳ Settings: global vs per-company
- ↳ Users & self-service
- ↳ Staff cross-company access
- ↳ Worked examples
- ↳ Pitfalls & gotchas
- ↳ Scope: what it's for
- ↳ 🛠️ Developer Guide (make a module multi-company)
- ↳ 🗄️ Case study: CMDB (a linked graph)
- ↳ 🧪 Test harness (prove it's isolated)