Class I18n
I18nString validates its cache against.
Shaped after Fonts and ControlSize, because a language change
is measurement-affecting in exactly the way a font change is: it notifies
listeners, every scene subscribes and re-lays-out,
and widgets re-read their text on the way through. An application switches
languages with one call and nothing else:
I18n.addBundle(PropertyBundle.family("/i18n/settings"));
I18n.addBundle(PropertyBundle.family("/i18n/editor"));
I18n.setLocale(Locale.forLanguageTag("pt-BR"));
Nothing is required. With no bundle registered every string resolves to the English it carries, which is what the toolkit did before this class existed.
A subtree can hold its own language
A widget that declares a locale gives its whole subtree one, resolved down the tree exactly asControlSize is. The mechanism
this class contributes is the scope: while the toolkit measures, lays out, paints or
dispatches an event to a widget, that widget's effective locale is
in scope, and locale() answers it. Everything that already
read I18n.locale() at the moment it resolved, formatted or broke text —
I18nString.get(), I18nString.format(java.lang.Object...), localizeDigits(java.lang.String), a chart format,
a line breaker — follows the subtree it is working inside without knowing subtrees
exist. That is the point: the default spelling is the correct one, and a widget written
naively cannot capture the wrong language.
Mutators are meant to be called on the UI thread (checked when a Ui
runtime is installed, so headless tests can drive them directly).
-
Method Summary
Modifier and TypeMethodDescriptionstatic voidaddBundle(StringBundle bundle) Registers a source of translations; the most recently added is consulted first, so an application's own bundle overrides one the toolkit installed.static List<StringBundle> bundles()The registered bundles, newest first.static Localelocalewith its language and region read as ICU reads them: a code that is another code for the same language, or a region that another region replaced, becomes the code ICU uses.static Collatorcollator()A collator for the text locale, for ordering what a user reads: list items, table rows, anything sorted for display.Every key declared so far, mapped to its English, in declaration order: the translation catalog as a runtime fact rather than a document that drifts.static longepoch()The counter everyI18nStringmemo is keyed on.static Localelocale()The language in effect here: the innermost open scope's locale, else the process locale.static StringlocalizeDigits(String text) textwith every ASCII digit rewritten in the active numbering system, and everything else untouched.static NumberingSystemThe digits a formatted number is written in: the own system of the locale in effect here, unlesssetNumberingSystem(limn.i18n.NumberingSystem)declared otherwise.static LocaleThe locale a number is written in here: the locale in effect, with the numbering system in effect named on it (-u-nu-) wherever the platform would otherwise write other digits, and its language read as ICU reads the code.static SubscriptionobserveChanges(Runnable listener) Subscribes to language changes.static SubscriptionobserveUnresolvedKeys(Consumer<UnresolvedKey> listener) Subscribes to keys that no registered bundle translates, which the toolkit otherwise answers silently with the English each declaration carries.static voidCloses the innermost scope by restoring whatpushScope(java.util.Locale)returned;null(the usual outermost case) restores "no scope", which is the process locale.static LocaleThe process-wide UI language, ignoring any open scope: whatsetLocale(java.util.Locale)set,Locale.getDefault()until something says otherwise, and the root of the widget resolution chain.static @Nullable LocaleOpens a resolution scope: until the matchingpopScope(java.util.Locale),locale()on this thread answerslocale.static voidreleaseLocale(Locale locale) Undoes oneretainLocale(java.util.Locale).static voidremoveBundle(StringBundle bundle) Removes a bundle.static StringThe first bundle with an answer for a key in a named locale, else the English.static voidretainLocale(Locale locale) Declares thatlocaleis being read somewhere — a widget or scene subtree resolves through it — so every bundle keeps (and any late-registered bundle gains) a prepared table for it, andsetLocale(java.util.Locale)moving the process away from it does not drop what that subtree is still reading.static voidSwitches the UI language.static voidsetNumberingSystem(@Nullable NumberingSystem system) Overrides the locale's numbering system;nullreturns to following the locale.static voidsetTextLocale(@Nullable Locale next) Declares the language of the content being ordered and case-mapped;nullreturns to following the UI locale.static LocaleThe language text is ordered and case-mapped in: the locale in effect here, unlesssetTextLocale(java.util.Locale)declared the content is in another language.static StringtoAsciiDigits(String text) textwith every digit of every known system folded back to ASCII: the parse-time half of digit substitution, and deliberately independent of the active system — a value pasted under one locale must survive being committed under another.static StringtoLowerCase(String text) The lower-case twin oftoUpperCase(java.lang.String), in the same locale.static StringtoUpperCase(String text) textupper-cased in the text locale: the case mapping for something a user reads.
-
Method Details
-
locale
The language in effect here: the innermost open scope's locale, else the process locale.Inside a widget's measure, layout, paint or event dispatch this is that widget's effective locale, because the toolkit opens the scope around each of those; everywhere else — application startup, a posted task, a worker thread — it is the process locale, exactly as before a subtree could declare one. Reading it at the moment text is resolved or formatted is what makes code follow the subtree it is working inside.
-
processLocale
The process-wide UI language, ignoring any open scope: whatsetLocale(java.util.Locale)set,Locale.getDefault()until something says otherwise, and the root of the widget resolution chain. Almost every reader wantslocale()instead. -
canonical
localewith its language and region read as ICU reads them: a code that is another code for the same language, or a region that another region replaced, becomes the code ICU uses.jpnisja,fra-CAisfr-CA,prs, Dari, isfa-AFandprs-IRisfa-IR,de-276isde-DE,uz-SUisuz-UZ. Where the replacement carries a script or a region, it fills in only what the locale leaves out; the locale's variants and extensions are kept.This is the reading the toolkit applies wherever it decides by language: translations, plural forms, digits, separators, order, case, calendars, line breaking and layout direction.
Localereplaces onlyiw,jiandinby itself, so code that comparesLocale.getLanguage()with"ja"treatsjpnas a language nobody listed. An application that chooses a font, a resource or a layout by language reads the locale through here first and gets the same answer the toolkit does.A locale ICU replaces nothing in, whether its codes are current or unknown, comes back as the same instance. Nothing is validated and nothing is added that the locale does not name:
jpnisja, notja-Jpan-JP. On a JVM started with-Djava.locale.useOldISOCodes=true,Localespells Hebrew, Yiddish and Indonesian with their old codes in every instance, including the ones answered here.Safe from any thread. The first call in a process reads the table of codes, which the toolkit has usually read already; after that a call costs two map lookups, and a replaced locale is built once and kept, so asking again answers the same instance.
- Parameters:
locale- the locale to read- Returns:
- the locale ICU reads, or
localeitself when ICU replaces nothing in it
-
setLocale
Switches the UI language. No-op when unchanged. Every registered bundle is prepared first (so a file-backed bundle reads from disk here rather than inside the next measure pass), then the epoch is bumped and listeners run, which is what re-lays-out every live scene. The outgoing language's tables are released, unless a retained subtree still reads them. -
pushScope
Opens a resolution scope: until the matchingpopScope(java.util.Locale),locale()on this thread answerslocale. This is how a widget's effective locale reaches everything that resolves or formats text while the toolkit is inside that widget —Widgetopens one around measure, layout, paint and event dispatch, andWidget.tooltip()around its own resolution. An application may open one too, to format something for a particular pane from outside a pass.Scopes nest: the returned value is the scope this call replaced, and handing it back to
popScope(java.util.Locale)restores it, so the idiom isLocale enclosing = I18n.pushScope(locale); try { ... } finally { I18n.popScope(enclosing); }- Returns:
- the scope in effect before this call, possibly
null: the valuepopScope(java.util.Locale)must be given back
-
popScope
Closes the innermost scope by restoring whatpushScope(java.util.Locale)returned;null(the usual outermost case) restores "no scope", which is the process locale. Always call it in afinally. -
retainLocale
Declares thatlocaleis being read somewhere — a widget or scene subtree resolves through it — so every bundle keeps (and any late-registered bundle gains) a prepared table for it, andsetLocale(java.util.Locale)moving the process away from it does not drop what that subtree is still reading. Counted:Widget.setLocaleandScene.setLocalecall this for a new declaration andreleaseLocale(java.util.Locale)for the one it replaced, and an application with its own reason to resolve a language outside the tree may do the same.Preparation happens here, on the first retain, for the reason
setLocale(java.util.Locale)prepares: a file-backed bundle must read from disk now, not inside the measure pass of the frame that first shows the subtree. -
releaseLocale
Undoes oneretainLocale(java.util.Locale). When the last retain forlocaleis released and it is not the process locale, every bundle is told to release its table for it. Releasing a locale that was never retained is a no-op, so a clear-before-declare cannot throw. -
epoch
public static long epoch()The counter everyI18nStringmemo is keyed on. Bumped by a locale change and by any bundle registration; both change what a key resolves to. -
numberingSystem
The digits a formatted number is written in: the own system of the locale in effect here, unlesssetNumberingSystem(limn.i18n.NumberingSystem)declared otherwise. There is no numbering-system axis: substitution happens at format time inside the widgets that render numbers they own, so application strings are never rewritten. What changed when a subtree could declare its own locale is only which locale the system is derived from — the effective one, so an Arabic subtree's spinner writes Arabic-Indic digits inside a Latin interface with no second mechanism. The declared override stays process-wide and wins everywhere, because it is a statement about the process ("this deployment writes Latin digits"), not about a subtree. -
numberLocale
The locale a number is written in here: the locale in effect, with the numbering system in effect named on it (-u-nu-) wherever the platform would otherwise write other digits, and its language read as ICU reads the code. It is the localeNumberFormatsformats under, published for a format of the application's own:String.format(I18n.numberLocale(), "%.1f MB", size)writes the digits and the separators a number the toolkit wrote beside it has.The bare locale is not that. The platform's idea of a language's default digits is not always the toolkit's: for a bare
arit writes١٢٣٤٫٥where the toolkit writes Latin digits, and a date or time it writes in ASCII digits whatever the language, where the toolkit writesar-EG's and Persian's own.DateFormatswrites those.The one case it cannot carry is a legacy locale form that takes no extension, whose formatter writes the platform's own digits; text formatted under it is put right by
localizeDigits(java.lang.String)(toAsciiDigits(java.lang.String)(text)), which is what the toolkit's own formats do.- Returns:
- the locale to format a number the user reads in
-
setNumberingSystem
Overrides the locale's numbering system;nullreturns to following the locale. Treated as a text change: the epoch bumps and every live scene re-lays-out, exactly as a locale switch does, because every formatted number on screen just changed. -
localizeDigits
textwith every ASCII digit rewritten in the active numbering system, and everything else untouched. Returns its argument underNumberingSystem.LATNor when there is nothing to rewrite, so the default locale pays an object comparison and a scan, not an allocation.This is the format-time half of digit substitution: call it on a string the widget itself rendered from a number, never on text an application authored.
-
toAsciiDigits
textwith every digit of every known system folded back to ASCII: the parse-time half of digit substitution, and deliberately independent of the active system — a value pasted under one locale must survive being committed under another. -
addBundle
Registers a source of translations; the most recently added is consulted first, so an application's own bundle overrides one the toolkit installed. Prepared for the process locale and every retained one before it can be seen, then treated as a text change.Registering the same instance twice is a no-op.
-
removeBundle
Removes a bundle. No-op when it was never registered. -
observeChanges
Subscribes to language changes.- Parameters:
listener- what to run when the locale or a bundle moves- Returns:
- a handle that unsubscribes; cancelling it twice is a no-op. UI thread
-
observeUnresolvedKeys
Subscribes to keys that no registered bundle translates, which the toolkit otherwise answers silently with the English each declaration carries. A test can collect them and fail; an application can send them to its own logging. Each key is reported once per locale, and again after a bundle is added or removed, since that can change the answer.A locale whose language is English reports nothing: there, the English in the declaration is the text. Neither does a plural form that is missing while the language's own
otherform is there, since that fallback is deliberate; when every form is missing, the.otherkey is reported.With no listener, a miss is logged once through
System.Logger: atINFOfor an application's keys, and atDEBUGfor the toolkit's own (limn.*), because several of those fall back on purpose, the colour picker's notations outside French and most theme names among them.A listener runs on the thread that resolved the key, which is usually the UI thread and is not always: an accessibility bridge resolves on the platform's thread. Keep it short and thread-safe. An exception it throws is logged and does not reach the text being drawn.
- Parameters:
listener- what to tell about each unresolved key- Returns:
- a handle that unsubscribes; cancelling it twice is a no-op
-
declaredKeys
Every key declared so far, mapped to its English, in declaration order: the translation catalog as a runtime fact rather than a document that drifts. A test asserts a bundle covers it; a tool dumps it as a starter.properties.It sees only what has been loaded: a component's strings are
static final, so they register when its class first initialises. Anything enumerating the catalog must touch the classes it cares about first. -
bundles
The registered bundles, newest first. -
textLocale
The language text is ordered and case-mapped in: the locale in effect here, unlesssetTextLocale(java.util.Locale)declared the content is in another language. The two are different facts — an English interface listing Swedish names must still put ä after z — but almost every application never needs to say so, and the default keeps order and case in the language the user is reading. Followinglocale()rather than the process locale is what makes a type-ahead fold, or a sort run inside a pass, answer for the subtree it is working inside, exactly as digits do; the declared override stays process-wide and wins everywhere, as the numbering-system override does. -
setTextLocale
Declares the language of the content being ordered and case-mapped;nullreturns to following the UI locale. Treated as a text change, exactly assetLocale(java.util.Locale)is: the epoch bumps and listeners run, because every order an application built throughcollator()is now stale and the application's own change listener is where it re-sorts. -
collator
A collator for the text locale, for ordering what a user reads: list items, table rows, anything sorted for display. A machine order — a key, a slug, a file format — keepscompareTo.Every call answers a fresh instance, because a
Collatorcarries mutable per-comparison state and must not be shared across threads. Fetch one per sort, not one per comparison:names.sort(I18n.collator()); items.sort(Comparator.comparing(Item::label, I18n.collator()));The language is read as ICU reads its code, as it is for case mapping:
sweorders as Swedish andfraas French, where the platform, which knows neither code, would order both as no language at all. -
toUpperCase
textupper-cased in the text locale: the case mapping for something a user reads. A machine format keepsLocale.ROOT— under Turkish this mapsitoİ, which is exactly right in a list of cities and exactly wrong in a hex color. Turkish namedturmaps it the same way. -
toLowerCase
The lower-case twin oftoUpperCase(java.lang.String), in the same locale. -
resolve
The first bundle with an answer for a key in a named locale, else the English.I18nString.get()is the ordinary way to read a string and resolves under the locale in effect on this thread. This is the other way, and it exists for one caller: an accessibility bridge answering a platform's question about a node, which must resolve under that node's locale and is very often not on the user-interface thread when it does.pushScope(java.util.Locale)cannot serve it — that is thread-confined ambient state, and a bridge reaching for it would be the silent data race this toolkit's threading rules exist to prevent.Safe from any thread, and read-only: the bundle list is copy-on-write and each bundle's tables are concurrent. What it cannot do is prepare a table, so a locale that no scene has retained resolves to the English rather than blocking or reading a file on a caller's thread.
When nothing translates the key, the
englishanswered is reported toobserveUnresolvedKeys(java.util.function.Consumer<limn.i18n.UnresolvedKey>). Anullenglishmakes the call a probe that reports nothing, which is how a plural looks for one form before settling on another.- Parameters:
key- the key to look upenglish- what to answer when nothing translates ittarget- the locale to answer in- Returns:
- the translation, or
english
-