Feature Wiki
Tabs
PO-Files for improving language handling
Page Overview
[Hide]- 1 Initial Problem
- 2 Conceptual Summary
- 3 User Interface Modifications
- 4 Additional Information
- 4.1 Involved Authorities
- 4.2 Technical Aspects
- 4.3 Privacy
- 4.4 Security
- 4.5 Contact
- 4.6 Funding
- 5 Discussion
- 6 Implementation
- 6.1 Description and Screenshots
- 6.2 Test Cases
- 6.3 Privacy
- 6.4 Approval
1 Initial Problem
The language handling mechanism of ILIAS – i.e. the management of all translated interface texts – relies on a very outdated scheme: language variables are maintained in proprietary .lang files and imported into the database at deployment time, from where the application reads them at runtime. This approach is maintenance-intensive, is hardly compatible with common translation tooling, and provides no structured solution for texts that differ depending on quantity (singular/plural) or for language variants such as dialects.
Modern approaches now exist to solve exactly this problem. gettext, a well-established industry standard built around template, translation and compiled files (POT, PO, MO), offers a considerably more future-proof and maintainable foundation for the language service.
2 Conceptual Summary
PO files are to be introduced for the language handling scheme, with their contents read at runtime via PHP's gettext() function family. The database is fully removed as the storage and runtime source for language variables; today's .lang files are replaced by .po/.mo files. The ILIAS GUI for editing language variables remains available and unchanged in its functionality, but internally operates on the new files instead of the database.
2.1 Advantages over the current scheme
- gettext is a widely adopted industry standard with broad tool support (e.g. Poedit, Weblate, Crowdin) that translators can work with comfortably. This noticeably reduces maintenance effort compared to today's .lang/database workflow.
- It solves the problem of wording that differs depending on quantity (singular/plural) in a structured, automatic way – see 2.2.
- An additional context allows identically worded but semantically different source strings to be cleanly distinguished, instead of relying on module prefixes within the key as done today.
- Language variants – such as British vs. American English or an "easy language" version – can be set up as a separate translation file based on the same template, without touching the core. In general, this allows any new language to be created and set up.
- Translations are compiled into a binary, very fast-to-read form for production use, so that language strings are loaded considerably more efficiently at runtime than via database queries.
- The clear separation between template, translation and runtime artifact considerably simplifies versioning in Git and code review of translation changes – something that is not possible for database content today.
2.2 Handling of singular and plural forms
In the current .lang/database approach there is no structured distinction between singular and plural forms; plural handling has to be solved individually in PHP code. gettext solves this via ngettext(): for each language, a plural formula is stored that defines how many plural forms the language has and by which rule a given number is mapped to one of those forms (German/English: 2 forms; languages such as Polish or Arabic: 3–6 forms). For every quantity-dependent string there is a singular and a plural reference, together with one translation per plural form of the target language. At call time, only the quantity is passed; the correct form is then selected automatically based on the plural formula of the respective language – the calling code itself does not need to know the plural rules of that language.
Example (German translation, 2 plural forms):Example (German translation, 2 plural forms):
# ----------------------------# Variante A (mit msgctxt)# ----------------------------# legacy: mail#:#unread_messages#:#Du hast %d ungelesene Nachricht(en)msgctxt "mail"msgid "unread_messages"msgid_plural "unread_messages_plural"msgstr[0] "Du hast %d ungelesene Nachricht"msgstr[1] "Du hast %d ungelesene Nachrichten"# ----------------------------# Variante B (ohne msgctxt, msgid = modul.identifier)# ----------------------------# legacy: mail#:#unread_messages#:#Du hast %d ungelesene Nachricht(en)msgid "mail.unread_messages"msgid_plural "mail.unread_messages_plural"msgstr[0] "Du hast %d ungelesene Nachricht"msgstr[1] "Du hast %d ungelesene Nachrichten"
Every entry consists of the original key (msgid), optionally a context (msgctxt) to distinguish identical keys originating from different modules, and – for quantity-dependent strings – a singular and plural variant (msgid_plural) with one translation per form (msgstr[0], msgstr[1], …). At runtime, gettext automatically selects the matching translation based on the given number. The same mechanism allows dialect languages, language variants, or an "easy language" version to be set up and maintained as a complete, self-contained language.
2.3 Migration of existing language data
Migrating today's .lang files and the language variables maintained in the database into the new PO/MO scheme is an integral part of this FR:
- A one-off migration tool generates a template per language from all currently known language keys and, from it, populates an initial translation file per language with the existing translations (1:1 takeover of the current database/.lang content).
- Existing keys with a module prefix are mapped to a context and key according to the chosen key variant (see 2.2).
- Quantity-dependent legacy strings, so far solved as individual texts with a manual case distinction in code, are identified during migration and converted into genuine singular/plural pairs; the respective call sites are switched to ngettext().
- Once migration has been completed and verified (test cases, see 6.2), the database-based read/write path for language variables is switched off; from this point on, the GUI for editing language variables writes to the PO files, including subsequent compilation.
- For the transition period, a rollback path is defined (backup of the database language tables as well as the previous .lang files) in case deviations are found after the switch-over.
The concrete technical design of the migration tool – timing, execution as a database update step vs. a standalone tool, handling of individual customer customizations to language texts – is detailed in a separate implementation concept.
2.4 Uniqueness of language variables
In the new scheme, language keys must be unique across all components. Through the combination of context (or module-qualified key) and identifier, every key is unambiguously assigned to a module; collisions between modules that use the same identifier are thereby structurally excluded.
- The previous "last loaded module wins" behaviour for cross-module collisions is thereby eliminated entirely.
- Remaining ambiguities within a single module – e.g. an identifier assigned twice – are detected and reported as a validation error when compiling the translation files, instead of silently overwriting one of them.
Settling on the concrete key variant (see 2.2/2.3) is a prerequisite for implementing this uniqueness concept.
2.5 Consequences for developers
The txt() function is switched to gettext() (or ngettext() for quantity-dependent strings) within the Language component and remains available as a thin wrapper. This results in the following requirements:
- Existing callers of txt() in other components require no code changes and keep working unchanged; they only benefit from the new capabilities (plural handling, contexts) once they make use of the corresponding wrapper parameters.
- New or quantity-dependent language usages make use of the new wrapper functions (the counterpart to ngettext()) going forward, in order to benefit from automatic plural handling. Migrating existing code that currently resolves plural cases manually is not part of this FR.
- Creating new language variables changes in the background: instead of writing to the database, new entries are stored in the new translation files – either directly or, as before, comfortably via the ILIAS GUI, which writes to the PO files behind the scenes. Direct file editing requires an additional compilation step, which is integrated into the existing build/deploy process.
- Developers adding new components are given binding documentation on how to provide a template for their own module and how modular translation files are integrated.
2.6 Documentation (language.md)
The existing language.md still fully describes the old .lang/database structure and is revised as part of this FR. Going forward, it is to describe, among other things:
- The structure and storage location of the new translation files per component.
- A step-by-step guide for correctly creating a new language entry under the new scheme, including the choice of singular/plural form and context.
- Migration notes for developers whose code or documentation still refers to the old scheme.
- The conventions for key assignment and uniqueness as described in 2.4.
3 User Interface Modifications
3.1 List of Affected Views
No views affected.
3.2 User Interface Details
No UI changes required.
3.3 New User Interface Concepts
No new UI concepts available.
3.4 Accessibility Implications
No changes regarding accessibility.
4 Additional Information
4.1 Involved Authorities
- Authority to Sign off on Conceptual Changes: Kunkel, Matthias [mkunkel]
- Authority to Sign off Code Changes: Kunkel, Matthias [mkunkel], Großkopf, Katrin [katrin.grosskopf], Potter, Chris [ChrisPotter], Clausen, Keven [keven.clausen], Knof, Christian [cknof]
4.2 Technical Aspects
The following are brief explanations of the terms gettext, POT file, PO file and MO file:
- gettext: The gettext functions implement an NLS (Native Language Support) API that can be used to internationalize PHP applications.
- POT file (.pot): Stands for Portable Object Template. It is the blank master template. A program scans the source code for text strings and collects them in this file. It contains the original text but no translations yet.
- PO file (.po): Stands for Portable Object. It is created by copying and filling out the POT file for a specific language. It is a simple text file containing pairs of original text (msgid) and translation (msgstr). It is easy for humans to read and edit.
- MO file (.mo): Stands for Machine Object. It is the binary version of the PO file. The finished program reads this file because computers can process binary data much faster than plain text.
As a result, the central function for retrieving language texts (txt()) is switched to gettext() within the Language component (see 2.5).
4.3 Privacy
No changes.
4.4 Security
The following security aspect must be taken into account:
- No scripts may be integrated via the GUI.
4.5 Contact
Person to be contacted in case of questions about the feature or for funding offers: Auerbach, Jeanine [jeanine.auerbach]
4.6 Funding
Funding status and funding parties are listed in the block 'Status of Feature' in the right column of this page.
If you are interested to give funding for this feature, please get into contact with the person mentioned above as 'Contact'.
5 Discussion
6 Implementation
Feature has been implemented by {Please add related profile link of this person}
6.1 Description and Screenshots
{ Description of the final implementation and screenshots if possible. }
6.2 Test Cases
Test cases completed at {date} by {user}
- {Test case number linked to Testrail} : {test case title}
6.3 Privacy
Information in privacy.md of component: updated at {date} by {user} | no change required
6.4 Approval
Approved at {date} by {user}.
Last edited: 11. Sep 2026, 08:26, Großkopf, Katrin [katrin.grosskopf]