Skip to the content
Documentation
Help Start
Reading in:
  • German: 9,059 of 9,059 pages
About the help in other languages
All help pages

The interface language list: every language, the most needed first

Since 6.0.15 (#516). Extends the interface language choice of #506.

Download as PDF

Address: https://doc.exponential.earth/exphelp/viewer/features/6.0/ui-switch-languages.md

On this page

The interface language list: every language, the most needed first

Since 6.0.15 (#516). Extends the interface language choice of #506.

The Interface language choice in the right toolbar (admin4, admin4l, the classic admin design) and in the AdminUI side bar offers every language the kernel has a locale for, not only the few that have a translation. It is one list box with headings:

  1. English: the English locales administrators use most.
  2. Major world languages: the 19 languages most needed by administrators worldwide, ranked (see below).
  3. Regional variants: 10 regional forms of those languages with their own formats.
  4. All languages (A–Z): every language of share/locale (about 130), in alphabetical order.

Each entry names the language in itself and in the language the interface is shown in now, and says how complete the interface translation is:

Deutsch (Deutschland) — German (Germany) (99%)
Français (France) — French (France) (46%)
Kiswahili (Kenya) — Swahili (Kenya) (no translation yet)

A language without a translation is offered honestly: its texts stay English, but dates, times and numbers follow its locale (share/locale/<locale>.ini). Choosing one is also the first step towards translating the interface: the formats are right from the start, and every message translated later appears at once.

All of it is set in settings/uiswitch.ini [UISwitch_language]; the settings are described in UI switch settings.

The list box

A native <select> with an <optgroup> per group and an Apply button, in a POST form to uiswitch/set (template design/standard/templates/uiswitch/grouped.tpl, used by every administration design, which all fall back to the standard design; providers without groups keep their list of buttons).

  • Type-ahead: with the list box focused, typing the first letters of a name ("Kis") selects the first entry that starts with them, as in every native list box. Arrow keys, Home, End, Page Up and Page Down move through it.
  • Nothing happens until Apply: moving through the list never reloads the page (WCAG 3.2.2, on input).
  • Screen readers announce the label (Interface language), the entry and its group.
  • The entry in effect is selected where it first appears (a language of a group is also in the A–Z list).
  • No script is needed, and none is added. The styles are the shared component classes (a4-input, a4-btn, a4-stack, a4-meta) with the design's tokens and their dark values; admin4.css adds only that the list box is never wider than its column.

The A–Z order and the names

  • The own name comes from the locale file (LanguageName). Where a file only repeats the English name ("Indonesian", "Croatian (Hrvatski)", "Serbian (Српски)": the first word is that of InternationalLanguageName), PHP's Intl extension gives the own name instead ("Indonesia (Indonesia)", "Hrvatski (Hrvatska)").
  • The second name is the language's name in the reader's current interface language (Intl, Locale::getDisplayName()), else the English name of the locale file. It is left out when both are the same.
  • Two locales with the same names get their codes added ([ser-SR], [srp-RS]).
  • The order is the collation of the reader's interface language (Intl's Collator): Č after C in Czech, Ä with A in English. Without Intl the fallback folds accents (Unicode decomposition when Normalizer exists) and case, and sorts other scripts after Latin, in code point order.

Translation coverage

expUISwitchLanguageCatalogue counts, for each locale with share/translations/<locale>/translation.ts, the source messages of share/translations/untranslated/translation.ts (context, source and comment) that the file translates: a translation that is empty, unfinished or obsolete does not count, nor does a message the kernel no longer has. The percentage is rounded down, so only a complete translation says 100%. English (eng-*) is the language of the source texts and always 100%. Only the kernel's translation is counted, not the extensions' own.

Reading the 25 translation files (1 to 3 MB each) takes 2 to 3 seconds, so the result is cached: in memory for the process (Velocity workers) and in <cache directory>/uiswitch/language-catalogue.json (and the names per reader language in language-names-<code>.json). Both are keyed by a signature of the names, sizes and times of every locale and translation file, so a new or changed file builds the catalogue again on its next use; nothing needs clearing. With the cache, the list costs about 2 ms per page.

Each translation file is kept as a mask of the source messages it translates, per file, in <cache directory>/uiswitch/translation-masks.json, keyed by the file's size, time and inode and by those of the source catalogue. When a translation changes, only that file is read again, not all of them. A well-formed file is read by string search, about three times faster than building an XML node for each message. A file the scan cannot read exactly as the XML parser does (broken, with CDATA, or in another encoding than UTF-8) is streamed with XMLReader as before. The cache files get the installation's file mode (site.ini [FileSettings] StorageFilePermissions), so Velocity (root) and PHP-FPM (the site user) can both replace them. exp:velocity deploy builds them in its "interface language caches" step.

Building every mask from nothing is the expensive part: with 156 translations it takes about 30 seconds. The language choice in the administration's toolbar is on every page, so it never waits for all of them: its catalogue has a time budget of 2 seconds per request (expUISwitchLanguageProvider::COVERAGE_TIME_BUDGET, expUISwitchLanguageCatalogue::setTimeBudget()). The masks not built within it are left for the next requests; until then those languages are listed without a percentage (their entries carry pending), and the complete catalogue is cached as soon as every mask is there, usually after about ten pages. Setup > Interface languages and the console build everything at once, as before. Before this, the first administration page after the masks were gone built them all and could run into max_execution_time (Redmine #16709).

A regional variant without a translation of its own takes the one i18n.ini [TranslationSettings] FallbackLanguages[] names: ger-AT and ger-CH show the ger-DE texts, the French-speaking countries fre-FR, the Spanish-speaking ones esl-ES, the Arabic ones ara-SA once it exists. The translator looks this up for the kernel and for each extension on its own, so esl-MX, which has a kernel translation, still gets esl-ES for an extension without an esl-MX one. The coverage of such a locale is that of the translation it falls back to. testShippedFallbacksGiveEveryCountryOfATranslatedLanguageItsTranslation fails when a country of a translated language has neither a translation nor a fallback.

The fallbacks come from expI18nFallbackResolver, the class the translator uses: a locale counts with the first translation of its whole chain (esl-AR -> esl-MX -> esl-ES), the rules of extensions (FallbackRules[]) and the automatic same-language fallback included, and with FallbackMode=merge with every translation of the chain together. Each entry has fallback (the translation counted, '' for its own) and chain. The resolver's signature is part of the cache's (format 5). Setup > Interface languages and ./console exp:i18n fallbacks list the same per locale, with the extensions' coverage: see Translation fallbacks.

Untranslated languages: listed by default

ListUntranslated=enabled is shipped, so every language is offered, the untranslated ones marked as such. The reasons: the owner's aim is that the list is useful to administrators worldwide, the formats of a locale are useful on their own, and the mark makes the state clear before anyone chooses. ListUntranslated=disabled offers English and the translated languages only (and is what applies when the setting is missing, as in #506).

Security

  • The allow-list is the siteaccess's own Locale, the groups' locales, AllowedValues[] and, with AllLanguages=enabled, every share/locale/<code>.ini whose name is a locale code (/^[a-z]{3}-[A-Z]{2}(@[a-z0-9]{1,16})?$/), without the format variants (eng-GB@euro) unless one is named. With ListUntranslated=disabled a locale also needs a translation file (or to be English).
  • A value from the request is compared with that list as it is (in_array(..., true)): nothing from the request is normalised, and nothing reaches a path. Paths are built only from codes read from the directory listing or the settings, after the pattern check.
  • The cache files hold only data (JSON), no code, and a file whose signature does not match is ignored.

Why the default ranking is what it is

The groups rank languages by how many administrators of a CMS are likely to need them: the number of people who speak the language (first or second language) and how much of the web's content and business is in it. Neither alone is enough: Hindi and Bengali have hundreds of millions of speakers but little web content, and many of their administrators work in English; Dutch, Polish and Italian have far fewer speakers but a large share of websites.

LanguageSpeakers, millions (L1+L2)Websites in itPlace
English1,49349.5%English group
Spanish5616.1%1
Chinese (Mandarin)1,1831.3%2
French3344.5%3
German1335.9%4
Portuguese2694.1%5
Japanese1264.9%6
Russian2103.4%7
Arabic (Standard)335 (L2)0.6%8
Hindi611< 0.1%9
Italian662.7%10
Indonesian2551.3%11
Turkish941.6%12
Korean820.9%13
Dutch—2.1%14
Polish—1.8%15
Vietnamese970.9%16
Ukrainian—0.6%17
Persian821.0%18
Hebrew100.4%19

Speakers: Ethnologue, as summarised on Wikipedia's List of languages by total number of speakers (2026 edition; "—" where the summary has no figure). Websites: W3Techs, Usage Statistics and Market Share of Content Languages for Websites, October 2026. Figures change every year; the order matters, not the digits.

  • English gets its own group because it is the source language (always complete) and the one most administrators use: eng-US first (the most common default), eng-GB (the kernel's own default), then eng-AU, eng-CA and eng-IN, the largest other English-speaking administrations with their own date formats.
  • Major world languages: the top of both lists, Spanish to Russian first (large on both), then Arabic and Hindi (very many speakers), then the languages with a large share of websites and business software (Italian, Turkish, Korean, Dutch, Polish) and the large markets of Indonesian, Vietnamese, Ukrainian and Persian, then Hebrew (0.4% of websites, more than Thai, and the right-to-left language that has an interface translation and a right-to-left administration layout since #521). Bengali, Urdu, Thai and Swahili are in the A–Z list: many speakers, little web content (Thai 0.3%, the others under 0.1%).
  • Regional variants: the regional forms with the most users and their own formats: Mexican and US Spanish, Canadian and Swiss French, Austrian and Swiss German, European Portuguese, Traditional Chinese of Taiwan and Hong Kong, and Egyptian Arabic, the largest Arabic-speaking country.

In all 34 entries, inside the 14 to 35 the list should hold so that the groups stay quick to scan. An installation with other needs ranks its own: see UI switch settings.

Sources:

For developers

  • expUISwitchLanguageProvider::groups(): the groups, each identifier, label and values. displayLabel( $value ) is the text of an entry, coverage( $value ) the percentage or null, nativeName() and readerName() the two names. label() stays the own name (the flat list, uiswitch/state).
  • expUISwitch::toolbar() adds groups to a provider's entry when the provider has groups() and displayLabel(); each group's values are hashes value, label, current, lang. uiswitch/state returns them too. A provider of an extension gets the same list box by implementing both methods.
  • expUISwitchLanguageCatalogue: entries(), entry( $code ), readerNames( $httpCode ), localeCodes(), coveragePercent(), sortValuesByLabel( $labels, $httpCode, $useIntl ).
  • expUISwitchLanguageCatalogue( $root, $cacheDirectory, $fallbacks, $resolver ): $fallbacks (a FallbackLanguages[] map without other rules) or $resolver (an expI18nFallbackResolver) replace the installation's settings, for tests; resolver() is the one in use, fallbacks() the configured map that points to a translation.
  • Tests: tests/tests/kernel/classes/uiswitch/expUISwitchLanguageProviderTest.php (grouping, the A–Z order with and without Intl, the allow-list, untranslated languages, coverage and its cache), without a database.

Writing a fallback rule

An extension that knows better where a locale should fall back (a regional language whose speakers read another, a script variant, a customer's own locale) adds a rule: a class implementing expI18nFallbackRuleInterface with one method.

<?php
// extension/myext/classes/mylocalefallbackrule.php (in the extension's autoload)
class myLocaleFallbackRule implements expI18nFallbackRuleInterface
{
    public function fallbackFor( $locale, expI18nFallbackResolver $resolver )
    {
        // Catalan of Andorra and Valencia read the Catalan translation, if there is one
        if ( in_array( $locale, array( 'cat-AD', 'cat-VA' ), true ) && $resolver->hasTranslation( 'cat-ES' ) )
            return 'cat-ES';
        return null; // no say: the next rule is asked
    }
}
# extension/myext/settings/i18n.ini.append.php
[TranslationSettings]
FallbackRules[]=myLocaleFallbackRule
  • fallbackFor() returns a locale code, 'untranslated' (or 'eng-GB') for the English source texts, or null when the rule has no say. It is asked for each step of a chain, so returning cat-ES for cat-AD and something else for cat-ES makes a longer chain; the resolver stops cycles and long chains, and exp:i18n check reports them.
  • The order is fixed: FallbackLanguages[] first (what is configured always wins), then the rules in the order of FallbackRules[], then the automatic same-language fallback. A rule cannot override a configured line; an administrator can always correct a rule with one.
  • The resolver gives the rule hasTranslation( $locale ) (a kernel translation exists), translations(), localeExists( $locale ) (a share/locale file), configured() and option( $name ). The rule is created once per resolver (once per process while the settings stay the same) without arguments, so it can keep what it computed; keep it cheap, the translator asks once per locale and request.
  • A rule that throws is logged and skipped; a FallbackRules[] entry that is no such class is reported by exp:i18n check and on Setup > Interface languages, and otherwise ignored.
  • Test it the way tests/tests/lib/ezi18n/expI18nFallbackResolverTest.php does: new expI18nFallbackResolver( $map, array( 'myLocaleFallbackRule' ), array( 'repository' => ..., 'locale_directory' => ... ) ) and chain() / explain(), without a database.

Become an Exponential Partner

Partners share their information, provide funding, contribute back and develop the products with us, and that is how Exponential grows. A partner website and dashboard are planned for mid-2027: please become a partner today to speed up our growth rate and land new features faster.

Something wrong on this page? Report an error

Documentation license

This documentation is free documentation, licensed under the GNU Free Documentation License 1.3 or later.

Exponential Copyright © 1998-2026 Exponential Foundation and 7x