Package limn.i18n

Class I18nString

java.lang.Object
limn.i18n.I18nString

public final class I18nString extends Object
A piece of user-visible text, carried as a value instead of resolved into a String at the point of use.

A component stores one of these where it would otherwise store a String, and reads get() when it measures or paints. Holding the value is what makes keeping it correct: a lookup call whose result must never be kept in a field fails silently, leaving one widget in yesterday's language.


 private static final I18nString PLACEHOLDER =
         new I18nString("limn.searchField.placeholder", "Search…");
 ...
 canvas.drawText(PLACEHOLDER.get(), x, baseline, font, colour);
 

English is not a translation. The second argument is both the default text and the fallback for every key a bundle does not answer, so an application that registers nothing behaves exactly as it did before this type existed, and a missing translation degrades to English rather than to a key name on screen.

The cache lives here. get() memoises its resolution against I18n.epoch(), so nothing subscribes and nothing leaks: the re-resolution rides the relayout a language change already causes.

Declare them static final, so the cache is one slot per key for the whole process rather than one per widget; a list showing five hundred rows of the same label resolves once per language change, not five hundred times.

Immutable in every respect a caller can observe; the memo is mutated on the UI thread like the rest of the toolkit's state.

  • Field Summary

    Fields
    Modifier and Type
    Field
    Description
    static final I18nString
    The empty literal: a text field's placeholder before anyone sets one.
  • Constructor Summary

    Constructors
    Constructor
    Description
    I18nString(String key, String english)
    Declares a localizable string.
  • Method Summary

    Modifier and Type
    Method
    Description
    The default-language text: also the fallback, and never null.
    boolean
    equals(@Nullable Object o)
    Value equality on key and English: two literals of the same text are the same string, and a component's "did this actually change" check keeps working when its field stops being a String.
    format(@Nullable Object @Nullable ... args)
    The text with {0}-style arguments substituted, formatted by MessageFormat under the current locale.
    get()
    The text in the locale in effect here, or the English when nothing translates it.
    int
     
    boolean
    Whether this is a literal, which no bundle can ever change.
    @Nullable String
    key()
    The lookup key, or null for a literal.
    static I18nString
    Text that is not localizable and resolves to itself: what setText(String) wraps a caller's literal in, so a component can hold one field instead of two and never has to decide which of them wins.
    The resolved text, so logging and debugging read as the user sees it.
    withArguments(@Nullable Object @Nullable ... args)
    This text with {0}-style arguments, as a text that follows the language: where format(java.lang.Object...) answers a String in the language of the moment, which a label given it keeps after the language changes, this answers an I18nString that formats again in the language in effect wherever and whenever it is read.
    This text as words, as the toolkit shows it with its access letter's mark taken out: for a string a menu or a button shows with its letter, quoted in a sentence, a status line or a log.

    Methods inherited from class java.lang.Object

    clone, finalize, getClass, notify, notifyAll, wait, wait, wait
  • Field Details

    • EMPTY

      public static final I18nString EMPTY
      The empty literal: a text field's placeholder before anyone sets one.
  • Constructor Details

    • I18nString

      public I18nString(String key, String english)
      Declares a localizable string.
      Parameters:
      key - the lookup key, dotted and globally unique; prefix it with the screen or component that owns it (settings.title, not title), because bundles share one namespace
      english - the text in the default language, used whenever no registered bundle answers the key
      Throws:
      IllegalStateException - if the key was already declared with different English: one key with two meanings is a bug that would otherwise surface as a mistranslation
  • Method Details

    • literal

      public static I18nString literal(String text)
      Text that is not localizable and resolves to itself: what setText(String) wraps a caller's literal in, so a component can hold one field instead of two and never has to decide which of them wins.
    • get

      public String get()
      The text in the locale in effect here, or the English when nothing translates it. Inside a widget's measure, layout, paint or event dispatch that locale is the widget's own effective one, so the same declaration reads correctly from every subtree without the caller doing anything.
    • withoutAccessLetter

      public I18nString withoutAccessLetter()
      This text as words, as the toolkit shows it with its access letter's mark taken out: for a string a menu or a button shows with its letter, quoted in a sentence, a status line or a log. "&Open" reads "Open", "R&&D" reads "R&D", an & before anything but a letter or a digit is kept ("Terms & conditions"), and the parenthesised letter Japanese, Korean and Chinese strings carry goes whole, on every system, with an ellipsis after it kept: "名前(&N)" reads "名前" and "プロパティ(&P)…" reads "プロパティ…". The reading is the controls' own, so the words are the ones a control shows.

      Resolved at get(), like this string: it follows a change of language, and the mark is taken out of whichever translation is in effect, each of which picks its own letter. format(java.lang.Object...) substitutes its arguments into the words, and an argument's own ampersands are left alone. Called on its own result, it returns that result.

      Returns:
      the words; this when it is already words
    • format

      public String format(@Nullable Object @Nullable ... args)
      The text with {0}-style arguments substituted, formatted by MessageFormat under the current locale. Never cached: the arguments vary per call, and caching them would be caching the wrong thing.

      Only parameterized keys pay MessageFormat's rules, which is why get() does not route through it: an apostrophe in an ordinary string needs no escaping. In a parameterized one it does (double it), and so does a stray brace; a pattern a translator got wrong falls back to the English rather than throwing in the middle of a paint.

      A number argument is written the way NumberFormats writes one: in the digits the toolkit shows for the language, with the separators, signs and grouping that go with those digits, and so are the digits of a {0,date} or {0,time} argument. The platform's own idea of a language's digits is not always the toolkit's: it writes Arabic-Indic digits for a bare ar, where every other number on the screen is Latin, and a sentence formatted under the bare locale carried its count in the digits nothing beside it used. Everything else the locale decides, the words of a date and a currency's symbol among them, is the language's as before. Pass Integer.toString(n) where a number is a spelling rather than a quantity and must stay in ASCII digits.

    • withArguments

      public I18nString withArguments(@Nullable Object @Nullable ... args)
      This text with {0}-style arguments, as a text that follows the language: where format(java.lang.Object...) answers a String in the language of the moment, which a label given it keeps after the language changes, this answers an I18nString that formats again in the language in effect wherever and whenever it is read. Give it to anything that takes an I18nString (Label.setText, a button, an accessible name) and a change of language reaches it as it reaches a plain declared string.

      An argument that is itself an I18nString is read in that language too; numbers are written as format(java.lang.Object...) writes them. Two texts made from the same string and equal arguments are equal, so setting the same sentence again is no change.

      The arguments are values, taken at the call. What follows is the language, not the arguments: the sentence is formatted once in each language it is read in and kept, as a declared string's translation is, so a count or a name that changes afterwards does not reach a text already made. Make a new one with the new value and set it again, as a label showing a count does each time the count moves. The array is copied; an object in it that changes after the call is read again only when the language does, and by equals(java.lang.Object) when two texts are compared, so pass what it holds (builder.toString(), a copy of a date) rather than the object.

      Parameters:
      args - the arguments, in their positions
      Returns:
      the text with its arguments
    • key

      public @Nullable String key()
      The lookup key, or null for a literal.
    • english

      public String english()
      The default-language text: also the fallback, and never null. For withoutAccessLetter(), its words.
    • isLiteral

      public boolean isLiteral()
      Whether this is a literal, which no bundle can ever change.
    • toString

      public String toString()
      The resolved text, so logging and debugging read as the user sees it.
      Overrides:
      toString in class Object
    • equals

      public boolean equals(@Nullable Object o)
      Value equality on key and English: two literals of the same text are the same string, and a component's "did this actually change" check keeps working when its field stops being a String. A string without its access letter equals only another such of the same key and English, since a translation can mark a letter the English does not.
      Overrides:
      equals in class Object
    • hashCode

      public int hashCode()
      Overrides:
      hashCode in class Object