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 (ADR 035). 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: after ADR 006's own argument, 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 ADR 035. 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.
    • 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 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(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 and ADR 033's Decision 1 still stands: substitution happens at format time inside the widgets that render numbers they own, so application strings are never rewritten. What changed with ADR 035 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.
    • setNumberingSystem

      public static void setNumberingSystem(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 ADR 033: 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 ADR 033, 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.
    • addChangeListener

      public static void addChangeListener(Runnable listener)
      Subscribes to language changes (idempotent per instance).
    • removeChangeListener

      public static void removeChangeListener(Runnable listener)
      Unsubscribes; no-op when it was never registered.
    • 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 (ADR 035), exactly as digits do; the declared override stays process-wide and wins everywhere, for ADR 034's reason.
    • setTextLocale

      public static void setTextLocale(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()));
       
    • 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.
    • toLowerCase

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