Skip to content

i18n and Localization

blancas edited this page Jan 23, 2013 · 4 revisions

Kern includes an i18n scheme and a simple method for producing diagnostic messages, either for localization or just overrides of the default text. The installation of any new text takes place at runtime, under the control of the client code. The storage and retrieval of the new text is also up to the client code.

The default messages reside in a map with the following key-value pairs:

{ :unexpected   "unexpected %s"
  :expecting    "expecting %s"
  :comma        ", "
  :or           " or %s"
  :err-pos      "%sline %d column %d\n"
  :eof          "end of input"
  :letter       "letter"
  :lower        "lowercase letter"
  :upper        "uppercase letter"
  :whitespace   "whitespace"
  :space        "space"
  :new-line     "new line"
  :tab          "tab"
  :digit        "digit"
  :hex-digit    "hexadecimal digit"
  :oct-digit    "octal digit"
  :alpha-num    "letter or digit"
  :end-comment  "end of comment"
  :char-lit     "character literal"
  :end-char     "end of character literal"
  :esc-code-b   "escaped code: b, t, n, f, r, ', \\"
  :esc-code-c   "escaped code: b, t, n, f, r, ', \\, ?, a, v, 0, ooo, uhhhh, xhh"
  :esc-code-j   "escaped code: b, t, n, f, r, ', \\, ooo, hhhh"
  :esc-code-h   "escaped code: b, t, n, f, r, ', \\, ?, a, v, 0, nnn, onnn, xnnnn"
  :string-lit   "string literal"
  :end-string   "end of string literal"
  :end-of       "end of "
  :dec-lit      "decimal literal"
  :oct-lit      "octal literal"
  :hex-lit      "hex literal"
  :float-lit    "floating-point literal"
  :reserved     "%s is a reserved name" }

Client code overrides these values by creating a similar map with the new values and then calling i18n-merge. The new map may have fewer entries than the default one, in which case some values remain unchanged; or the new may may have additional entries for use by custom parsers in client code.

The functions described below are in the blancas.kern.i18n namespace.

(use 'blancas.kern.core
     'blancas.kern.i18n)

To illustrate the above, the following code uses capitalization in some messages, and adds three entries to the map:

(def custom
  { :unexpected   "Unexpected %s"
    :expecting    "Expecting %s"
    :dec-lit      "Decimal literal"
    :oct-lit      "Octal literal"
    :hex-lit      "Hex literal"
    :float-lit    "Floating-point literal"
    :range        "Out of range"
    :speed        "Too fast!"
    :over         "Game Over"})

(i18n-merge custom)

After this call, code that uses the above keys will show the new text. i18n-merge will merge the supplied map into the current text table, which is mutable and therefore calls to this function are cumulative. The argument can be any map. It returns the value of the merge map.

i18n works as a getter or setter for a specific key. If called with a key, it returns its associated string value or nil if the key is not found. If called with a key and a value, it will add them as a new entry.

(i18n :unexpected)  ;; "Unexpected %s"
(i18n :over)  ;; "Game Over"
(i18n :none)  ;; nil

(i18n :over "The End")  ;; { ... }
(i18n :none "None")  ;; { ... }
(i18n :over)  ;; "The End"
(i18n :none)  ;; "None"

ftm takes a key whose value contains a string format according to java.util.Formatter, and any number of additional values. It returns the resulting formatted string.

(fmt :unexpected "foo")  ;; "Unexpected foo"
(fmt :err-pos "helloworld.c " 130 5) ;; "helloworld.c line 130 column 5\n"

di18n works like i18n but its result is delayed and not immediately evaluated. This is necessary in parsers written as a def because this form evaluates the var's value at the moment of declaration, which is too soon. Some arguments may not be available until the parser runs; or you may want to have the choice of overriding the text later.

(di18n :unexpected)  ;; #<Delay@4ec1429c: :pending>

As a rule of thumb, if you define a parser with def use di18n; if you de fine a parser with defn use i18n. Note that di18n works as a getter but not as a setter.

(def letter-1
  "Reads a letter."
  (<?> (satisfy #(Character/isLetter %)) (di18n :letter)))

(i18n :good "a good letter")

(defn letter-x [x]
  "Expects letter x."
  (<?> (satisfy #(= x %)) (i18n :good))) ;; specify what's expected

(run letter-1 "1")
;; line 1 column 1
;; Unexpected \1
;; Expecting letter

(run (letter-x \x) "2")
;; line 1 column 1
;; Unexpected \2
;; Expecting a good letter

dfmt works like ftm but its result is delayed for the same reasons as di18n. Regarding when to use it, the same rules of thumb apply to this function as for di18n above.

(fmt :unexpected "foo")  ;; #<Delay@6f628b8d: :pending>