Package limn.i18n

Class I18n

java.lang.Object
limn.i18n.I18n

public final class I18n extends Object
The UI language: the process locale, the locale in effect here, the registered bundles, and the epoch that every 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 as ControlSize 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 Details

    • locale

      public static Locale 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

      public static Locale processLocale()
      The process-wide UI language, ignoring any open scope: what setLocale(java.util.Locale) set, Locale.getDefault() until something says otherwise, and the root of the widget resolution chain. Almost every reader wants locale() instead.
    • canonical

      public static Locale canonical(Locale locale)
      locale with 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. jpn is ja, fra-CA is fr-CA, prs, Dari, is fa-AF and prs-IR is fa-IR, de-276 is de-DE, uz-SU is uz-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. Locale replaces only iw, ji and in by itself, so code that compares Locale.getLanguage() with "ja" treats jpn as 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: jpn is ja, not ja-Jpan-JP. On a JVM started with -Djava.locale.useOldISOCodes=true, Locale spells 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 locale itself when ICU replaces nothing in it
    • setLocale

      public static void setLocale(Locale next)
      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

      public static @Nullable Locale pushScope(Locale locale)
      Opens a resolution scope: until the matching popScope(java.util.Locale), locale() on this thread answers locale. This is how a widget's effective locale reaches everything that resolves or formats text while the toolkit is inside that widget — Widget opens one around measure, layout, paint and event dispatch, and Widget.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 is

      
       Locale enclosing = I18n.pushScope(locale);
       try {
           ...
       } finally {
           I18n.popScope(enclosing);
       }
       
      Returns:
      the scope in effect before this call, possibly null: the value popScope(java.util.Locale) must be given back
    • popScope

      public static void popScope(@Nullable Locale enclosing)
      Closes the innermost scope by restoring what pushScope(java.util.Locale) returned; null (the usual outermost case) restores "no scope", which is the process locale. Always call it in a finally.
    • retainLocale

      public static void retainLocale(Locale locale)
      Declares that locale is 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, and setLocale(java.util.Locale) moving the process away from it does not drop what that subtree is still reading. Counted: Widget.setLocale and Scene.setLocale call this for a new declaration and releaseLocale(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

      public static void releaseLocale(Locale locale)
      Undoes one retainLocale(java.util.Locale). When the last retain for locale is 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 every I18nString memo is keyed on. Bumped by a locale change and by any bundle registration; both change what a key resolves to.
    • numberingSystem

      public static NumberingSystem numberingSystem()
      The digits a formatted number is written in: the own system of the locale in effect here, unless setNumberingSystem(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

      public static Locale 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 locale NumberFormats formats 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 ar it writes ١٢٣٤٫٥ where the toolkit writes Latin digits, and a date or time it writes in ASCII digits whatever the language, where the toolkit writes ar-EG's and Persian's own. DateFormats writes 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

      public static void setNumberingSystem(@Nullable NumberingSystem system)
      Overrides the locale's numbering system; null returns 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

      public static String localizeDigits(String text)
      text with every ASCII digit rewritten in the active numbering system, and everything else untouched. Returns its argument under NumberingSystem.LATN or 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

      public static String toAsciiDigits(String text)
      text with 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

      public static void addBundle(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. 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

      public static void removeBundle(StringBundle bundle)
      Removes a bundle. No-op when it was never registered.
    • observeChanges

      public static Subscription observeChanges(Runnable listener)
      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

      public static Subscription observeUnresolvedKeys(Consumer<UnresolvedKey> listener)
      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 other form is there, since that fallback is deliberate; when every form is missing, the .other key is reported.

      With no listener, a miss is logged once through System.Logger: at INFO for an application's keys, and at DEBUG for 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

      public static Map<String,String> 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

      public static List<StringBundle> bundles()
      The registered bundles, newest first.
    • textLocale

      public static Locale textLocale()
      The language text is ordered and case-mapped in: the locale in effect here, unless setTextLocale(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. Following locale() 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

      public static void setTextLocale(@Nullable Locale next)
      Declares the language of the content being ordered and case-mapped; null returns to following the UI locale. Treated as a text change, exactly as setLocale(java.util.Locale) is: the epoch bumps and listeners run, because every order an application built through collator() is now stale and the application's own change listener is where it re-sorts.
    • collator

      public static Collator 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 — keeps compareTo.

      Every call answers a fresh instance, because a Collator carries 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: swe orders as Swedish and fra as French, where the platform, which knows neither code, would order both as no language at all.

    • toUpperCase

      public static String toUpperCase(String text)
      text upper-cased in the text locale: the case mapping for something a user reads. A machine format keeps Locale.ROOT — under Turkish this maps i to İ, which is exactly right in a list of cities and exactly wrong in a hex color. Turkish named tur maps it the same way.
    • toLowerCase

      public static String toLowerCase(String text)
      The lower-case twin of toUpperCase(java.lang.String), in the same locale.
    • resolve

      public static String resolve(String key, @Nullable String english, Locale target)
      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 english answered is reported to observeUnresolvedKeys(java.util.function.Consumer<limn.i18n.UnresolvedKey>). A null english makes 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 up
      english - what to answer when nothing translates it
      target - the locale to answer in
      Returns:
      the translation, or english