ludic.i18n: the English path is out of L (26.9) - a plain string is drawn as it is in every language (a player named Settings stays Settings), a key, a key glued into text and a bracketed line are made as before. The pattern tables, the sentence and padding lookups, Ln, i18n_pattern_count and kr_words' English-argument lookup are gone; a language's .po is read for keys and plurals. Tests move to keys; README and a changeset say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-30 02:58:13 +03:00
parent e2aa928e95
commit 8882331421
18 changed files with 91 additions and 516 deletions

View file

@ -0,0 +1,9 @@
bump: minor
type: change
**ludic.i18n draws plain text as it is: the English path is gone (phase 26.9).** `L` makes a key, a
key glued into text, or a line bracketed inside another; anything else - a player's name, a chat
line, a number - is drawn as it is, in every language, so a player named "Settings" stays
"Settings". Removed with it: the lookup of English words (exact lines, patterns with holes, a
paragraph a sentence at a time, padding), `Ln` (use `trn(kn"...")`), `i18n_pattern_count`, and an
English argument's own lookup inside a key's hole. A language `.po` is read for its keys and
plurals only. A game still on English msgids draws them untranslated until they are keys.

View file

@ -26,7 +26,7 @@ section. The rules are in [ludic.base](ludic.base/README.md).
| [ludic.devlink](ludic.devlink/README.md) | an editor's live link into a running dev build: loopback UDP, one request a frame, verbs as ports | | [ludic.devlink](ludic.devlink/README.md) | an editor's live link into a running dev build: loopback UDP, one request a frame, verbs as ports |
| [ludic.fishing](ludic.fishing/README.md) | a rod at the water: cast, bite, the reel's fight, the landing; species as an open registry | | [ludic.fishing](ludic.fishing/README.md) | a rod at the water: cast, bite, the reel's fight, the landing; species as an open registry |
| [ludic.hints](ludic.hints/README.md) | a rail of what is true now, most urgent first, each a card that teaches it and can be muted by key | | [ludic.hints](ludic.hints/README.md) | a rail of what is true now, most urgent first, each a card that teaches it and can be muted by key |
| [ludic.i18n](ludic.i18n/README.md) | a game in any language: gettext `.po` files, patterns with holes, plurals, a mod folder, a font per language | | [ludic.i18n](ludic.i18n/README.md) | a game in any language by keys: gettext `.po` files (en.po the English), holes, plurals, a mod folder, a font per language |
| [ludic.inventory](ludic.inventory/README.md) | a pack: a count per kind, with room the game decides | | [ludic.inventory](ludic.inventory/README.md) | a pack: a count per kind, with room the game decides |
| [ludic.minimap](ludic.minimap/README.md) | a map's logic: the explored fog over a grid and its land share, the player's marks, and the view (world to map and back, zoom, pan, the camp, you, focus) | | [ludic.minimap](ludic.minimap/README.md) | a map's logic: the explored fog over a grid and its land share, the player's marks, and the view (world to map and back, zoom, pan, the camp, you, focus) |
| [ludic.needs](ludic.needs/README.md) | a body's warmth, food, water and energy, and the countdown to a collapse | | [ludic.needs](ludic.needs/README.md) | a body's warmth, food, water and energy, and the countdown to a collapse |

View file

@ -1,19 +1,39 @@
# ludic.i18n # ludic.i18n
A game in any language, from gettext files anyone can add. English is the source and stays in the A game in any language, from gettext files anyone can add. The code names text by KEYS, never by its
code: nothing here, and nothing a translator does, rewrites a string in the game. A language is a English: a key says what a text is FOR (`pause.resume`), `en.po` says what it says in English, and
`.po` keyed by the English exactly as the game draws it, and text is translated at the moment it is every other language is a `.po` keyed the same way. Text is made at the moment it is drawn, so a
drawn, so a string that reaches the screen is covered wherever it was built. Uses nothing - not line that reaches the screen is covered wherever it was built. Uses nothing - not even
even `ludic.base`; the font atlas and the mod folder are the game's, through a port. `ludic.base`; the font atlas and the mod folder are the game's, through a port.
```ludic ```ludic
import "ludic.i18n" import "ludic.i18n"
i18n_use("tr") # from settings; the atlas follows i18n_use("tr") # from settings; the atlas follows
ov_text(x, y, 16, L("Settings"), ...) ov_text(x, y, 16, L(tr(k"pause.resume")), ...) # "Resume"; "Devam" in Turkish
ov_text(x, y, 16, L(`Day {day}`), ...) # "Day {1}" in the file matches "Day 12" ov_text(x, y, 16, L(trf(k"hud.day", `{day}`, "", "", "")), ...)
ov_text(x, y, 16, Ln("{1} trout", "{1} trout", n), ...) ov_text(x, y, 16, L(trn(kn"pack.items", n, "", "", "")), ...)
``` ```
## Keys
A key travels as a string with a marker byte in front (the compiler's `k"pause.resume"` is exactly
that; `kn"..."` a plural key), so code that makes text never takes the language's state: the text is
made where it is drawn, by `L`.
- A key's text is the language in use's, else en.po's (read from the languages' directory the first
time a key is asked for), else the key itself - `[[key]]` in a developer's build (`i18n_loud`),
said once.
- Holes `{1}` .. `{4}` take `trf`'s arguments in the language's own order, and a translation may
reorder or drop them; an argument that is a key is made into its text first. A plural's count is
`{1}`, and its form is the language's `Plural-Forms` rule's.
- A key's line inside another's hole survives: an argument's own separators are escaped outside
brackets, and a line bracketed with bytes 29 .. 30 (a game's nested line) rides raw and is made
first, outermost in.
- **Anything without a marker is drawn as it is**, in every language: a player's name, a chat line,
a number, a path. There is no lookup by English words, so a player named "Settings" stays
"Settings". A key glued into such text (`"Photo: " + tr(k"...")`) is still made where it sits,
an interim help for code on its way to a key of its own.
## Where languages come from ## Where languages come from
Later ones replace earlier ones with the same code: Later ones replace earlier ones with the same code:
@ -22,40 +42,8 @@ Later ones replace earlier ones with the same code:
- every `*.po` in the mod folder the port names - a player drops a file in and picks it. No build. - every `*.po` in the mod folder the port names - a player drops a file in and picks it. No build.
`X-Language-Name` in the header is what a menu shows; `X-Font` names a font atlas directory beside `X-Language-Name` in the header is what a menu shows; `X-Font` names a font atlas directory beside
the `.po`, for a script the default atlas does not carry; `Plural-Forms` picks the plural rule. the `.po`, for a script the default atlas does not carry; `Plural-Forms` picks the plural rule. A
language that lacks a key draws its English, so a partial file is always playable.
## What a msgid can be
- `"Hold to break off deadwood"` - an exact string.
- `"Day {1}"` -> `"{1}. Gün"` - a pattern: the English around the holes must match, the holes are
whatever the game put there, and a translation may reorder or drop them. A hole's own text is
translated too when the file knows it - an item name, or "a deer" with its English article taken
off. Of several patterns that match, the one pinning down the most English wins.
- A paragraph the game assembled from sentences is tried whole, then a sentence at a time; the
padding a layout put round a word (" owned") is taken off and put back.
- `msgid_plural` / `msgstr[k]` - `Ln(one, many, n)`, by the file's `Plural-Forms`.
Whatever has no translation draws in English, so a partial file is always playable.
## Keys (phase 26: the form that replaces English in the code)
A key names what a text is FOR (`pause.resume`), and `assets/lang/en.po` says what it says in English,
like any other language. A key travels as a string with a marker byte in front (the compiler's
`k"pause.resume"` is exactly that; `kn"..."` a plural key), so code that makes text never takes the
language's state: the text is made where it is drawn, by `L`.
```ludic
L(tr(k"pause.resume")) # "Resume"; "Devam" in Turkish
L(trf(k"catch.saw", tr(k"species.elk.a_name"), "5:30", "", "")) # "You saw an elk at 5:30."
L(trn(kn"pack.items", n, "", "", "")) # "{1} items" by the language's plural rule
```
- A key's text is the language in use's, else en.po's (read from the languages' directory the first
time a key is asked for), else the key itself - `[[key]]` in a developer's build (`i18n_loud`),
said once.
- Holes `{1}` .. `{4}` take `trf`'s arguments in the language's own order; an argument that is a key
is made into its text first. A plural's count is `{1}`.
- A string with no marker takes the English path below, so a game moves over a file at a time.
## Config and port ## Config and port
@ -73,8 +61,7 @@ port I18nWorld {
| | | | | |
| --- | --- | | --- | --- |
| `L(s) -> string` | what the game draws, in the language in use (cached) | | `L(s) -> string` | what the game draws, in the language in use (made once per distinct line, kept) |
| `Ln(one, many, n) -> string` | a count's form, with `{1}` the count; English is one for 1, many otherwise (the old form) |
| `tr(k: Key)`, `trf(k: Key, a, b, c, d)`, `trn(k: Key, n, a, b, c) -> string` | a key (`k"..."`), a key with its holes' arguments, a plural key (`kn"..."`) by `n`: made into text by `L` | | `tr(k: Key)`, `trf(k: Key, a, b, c, d)`, `trn(k: Key, n, a, b, c) -> string` | a key (`k"..."`), a key with its holes' arguments, a plural key (`kn"..."`) by `n`: made into text by `L` |
| `i18n_key(name)`, `i18n_key_plural(name)`, `key_text(k)`, `i18n_is_key(s)` | a key named at run time, its name without the marker, whether a string is one | | `i18n_key(name)`, `i18n_key_plural(name)`, `key_text(k)`, `i18n_is_key(s)` | a key named at run time, its name without the marker, whether a string is one |
| `i18n_base_text(text)`, `i18n_loud(on)` | the base (en.po) from text, for a test; a developer's build's loud missing key | | `i18n_base_text(text)`, `i18n_loud(on)` | the base (en.po) from text, for a test; a developer's build's loud missing key |
@ -82,21 +69,16 @@ port I18nWorld {
| `i18n_use(code)`, `i18n_off()`, `i18n_on()`, `i18n_current()` | choosing one; whether anything is translating | | `i18n_use(code)`, `i18n_off()`, `i18n_on()`, `i18n_current()` | choosing one; whether anything is translating |
| `i18n_font_base(dir)`, `i18n_font_now()` | the atlas the overlay was opened with, and the one it has now | | `i18n_font_base(dir)`, `i18n_font_now()` | the atlas the overlay was opened with, and the one it has now |
| `i18n_load_text(text)`, `i18n_parse(text) -> []PoEntry`, `i18n_header(text, field)` | a `.po` from text, outright | | `i18n_load_text(text)`, `i18n_parse(text) -> []PoEntry`, `i18n_header(text, field)` | a `.po` from text, outright |
| `i18n_exact_count()`, `i18n_pattern_count()`, `i18n_plural_rule()`, `i18n_plural_form(rule, n)`, `PL_*` | what the file gave | | `i18n_exact_count()`, `i18n_plural_rule()`, `i18n_plural_form(rule, n)`, `PL_*` | what the file gave |
| `i18n_find`, `i18n_starts`, `i18n_ends` | the byte helpers, for a game that wants them | | `i18n_find`, `i18n_starts`, `i18n_ends` | the byte helpers, for a game that wants them |
## Keeping an extractor working
A game's extractor finds the English by reading its own source for literals (Maroon Lake's
`tools/i18n/extract.py`); nothing here changes what it reads. Build sentences a translator can
find: one template literal with holes, not fragments glued with `+`.
## Tests ## Tests
```bash ```bash
ludic test packages/ludic.i18n ludic test packages/ludic.i18n
``` ```
A shipped list and a mod folder in the temp directory: exact lines, patterns with translated holes `keys_test`: keys, holes, plurals, a key glued into text, plain text as it is. `nest_test`: lines
and articles, paragraphs, padding, plurals (a Russian-shaped rule), the font and its fallback, a mod inside lines, bare, bracketed and escaped. `i18n_test`: a shipped list and a mod folder in the temp
replacing a shipped code, a forced code, and English untouched. directory, a language's keys, plurals (a Russian-shaped rule), the font and its fallback, a mod
replacing a shipped code, a forced code.

View file

@ -8,8 +8,6 @@ import "bytes.ludic"
import "po.ludic" import "po.ludic"
import "langs.ludic" import "langs.ludic"
import "use.ludic" import "use.ludic"
import "patterns.ludic"
import "match.ludic"
import "translate.ludic" import "translate.ludic"
import "keys.ludic" import "keys.ludic"
import "keyed.ludic" import "keyed.ludic"

View file

@ -21,13 +21,6 @@ function kr_unescape(s: string) -> string {
return text_of(buf, n) return text_of(buf, n)
} }
# an argument with letters in it (not a number, a time, a sign): while the game still hands English
# into a key's holes (until its code and data are keys, phase 26.9), it gets the old English lookup
function kr_words(s: string) -> bool {
for i in 0 .. len(s) { if (s[i] >= 65 and s[i] <= 90) or (s[i] >= 97 and s[i] <= 122) { return true } }
return false
}
# a count written as decimal digits (a leading - allowed); anything else is 0 # a count written as decimal digits (a leading - allowed); anything else is 0
function kr_int(s: string) -> int { function kr_int(s: string) -> int {
var v = 0 var v = 0

View file

@ -60,7 +60,7 @@ function kr_fill(i18n_st: mut I18nState, t: string, args: []string) -> string {
if h >= 1 { if h >= 1 {
var v = "" var v = ""
if h < len(args) { v = args[h] } if h < len(args) { v = args[h] }
if i18n_is_key(v) or kx_mixed(v) or kx_nested(v) { v = L(i18n_st, v) } else if i18n_st.on and kr_words(v) { v = L(i18n_st, v) } if i18n_is_key(v) or kx_mixed(v) or kx_nested(v) { v = L(i18n_st, v) }
r = r + t[seg..i] + v r = r + t[seg..i] + v
i = hole_end(t, i) i = hole_end(t, i)
seg = i seg = i

View file

@ -1,96 +0,0 @@
# match.ludic - which pattern produces a string, the best one being the one that pins down the most
# English, and the text that filled each hole translated in turn
# does pattern p produce s? The holes land in caps, in hole order
function try_pattern(i18n_st: mut I18nState, p: int, s: string) -> bool {
let n = len(s)
let l0 = i18n_st.pat_l0[p]
let nl = i18n_st.pat_nl[p]
let first = i18n_st.lits[l0]
let f_n = len(first)
let last = i18n_st.lits[l0 + nl - 1]
let l_n = len(last)
if nl - 1 > CAPS { return false }
if n < f_n + l_n + nl - 1 { return false } # every hole takes at least a byte
for k in 0 .. f_n { if s[k] != first[k] { return false } }
for k in 0 .. l_n { if s[n - l_n + k] != last[k] { return false } }
let lim = n - l_n
var pos = f_n
for g in 0 .. nl - 1 {
var stop = lim
var adv = 0
if g < nl - 2 {
let mid = i18n_st.lits[l0 + g + 1]
stop = i18n_find(s, lim, pos + 1, mid, len(mid))
if stop < 0 { return false }
adv = len(mid)
}
if stop <= pos { return false }
i18n_st.caps[g] = s[pos..stop]
pos = stop + adv
}
return true
}
# the best of a chain that produces s, or `best` as it was
function best_of(i18n_st: mut I18nState, head: int, next: words, s: string, best: int) -> int {
var b = best
var c = head
while c > 0 {
let p = c - 1
var sc = -1
if b >= 0 { sc = i18n_st.pat_sc[b] }
if i18n_st.pat_sc[p] > sc and try_pattern(i18n_st, p, s) { b = p }
c = next[p]
}
return b
}
@alloc_ok("a translation cache miss (L): once per distinct text")
function pattern(i18n_st: mut I18nState, s: string, depth: int) -> string {
i18n_st.hit = false
if i18n_st.pat_n == 0 { return s }
let n = len(s)
var best = -1
if n >= 4 {
best = best_of(i18n_st, Dict.get_or(i18n_st.pfx, s[0..4], 0), i18n_st.nx_p, s, best)
best = best_of(i18n_st, Dict.get_or(i18n_st.sfx, s[n - 4..n], 0), i18n_st.nx_s, s, best)
}
if n < 80 { best = best_of(i18n_st, i18n_st.gen_head, i18n_st.nx_g, s, best) }
if best < 0 { return s }
try_pattern(i18n_st, best, s)
let l0 = i18n_st.pat_l0[best]
let nl = i18n_st.pat_nl[best]
# copied out first: translating a hole may match another pattern and reuse caps
let raw = new []string
for g in 0 .. nl - 1 { push(raw, i18n_st.caps[g]) }
let cs = new []string
for g in 0 .. nl - 1 { push(cs, piece(i18n_st, raw[g], depth + 1)) }
let r = fill_holes(i18n_st, i18n_st.pat_out[best], l0, nl, cs)
# a pattern that gives the English straight back has translated nothing, and must not stop
# the sentence-by-sentence pass
if r == s { return s }
i18n_st.hit = true
return r
}
# the text that filled a hole: known as it stands, known without the English article the game
# glued on ("a deer", "the Dark King"), or itself a pattern
function piece(i18n_st: mut I18nState, c: string, depth: int) -> string {
let e = Dict.get_or(i18n_st.exact, c, -1)
if e >= 0 { return i18n_st.out[e] }
let n = len(c)
var bare = -1
if n > 4 and (i18n_starts(c, "the ") or i18n_starts(c, "The ")) { bare = 4 }
else if n > 3 and (i18n_starts(c, "an ") or i18n_starts(c, "An ")) { bare = 3 }
else if n > 2 and (i18n_starts(c, "a ") or i18n_starts(c, "A ")) { bare = 2 }
if bare > 0 {
let e2 = Dict.get_or(i18n_st.exact, c[bare..n], -1)
if e2 >= 0 { return i18n_st.out[e2] }
}
if depth < 3 {
let r = pattern(i18n_st, c, depth)
if i18n_st.hit { return r }
}
return c
}

View file

@ -1,5 +1,5 @@
# mixed.ludic - a key's text glued into an English line ("A {1}, {2} cm." given a name that is a key, or a # mixed.ludic - a key's text glued into an English line ("A {1}, {2} cm." given a name that is a key, or a
# name + "\n" + its blurb): each marked name made in the language in use, then the line as English. Interim, # name + "\n" + its blurb): each marked name made in the language in use; the words round it drawn as they are. Interim,
# until every such line is a key of its own (26.9); a mark with arguments cannot be told from the line after it # until every such line is a key of its own (26.9); a mark with arguments cannot be told from the line after it
# a key's name: a-z, 0-9, _ and . (every key the compiler derives or a game writes), a full stop at its # a key's name: a-z, 0-9, _ and . (every key the compiler derives or a game writes), a full stop at its
@ -30,8 +30,7 @@ function kx_line(i18n_st: mut I18nState, s: string) -> string {
if i18n_st.kc == null { kc_clear(i18n_st) } if i18n_st.kc == null { kc_clear(i18n_st) }
let ci = Dict.get_or(i18n_st.kc, s, -1) let ci = Dict.get_or(i18n_st.kc, s, -1)
if ci >= 0 { return i18n_st.kc_out[ci] } if ci >= 0 { return i18n_st.kc_out[ci] }
var r = kx_unmix(i18n_st, s) let r = kx_unmix(i18n_st, s)
if i18n_st.on { r = whole(i18n_st, r) }
if Dict.size(i18n_st.kc) >= CACHE_MAX { kc_clear(i18n_st) } if Dict.size(i18n_st.kc) >= CACHE_MAX { kc_clear(i18n_st) }
@alloc_ok("a line with a key glued in, made into text: kept, up to CACHE_MAX") @alloc_ok("a line with a key glued in, made into text: kept, up to CACHE_MAX")
Dict.set(i18n_st.kc, s, len(i18n_st.kc_out)) Dict.set(i18n_st.kc, s, len(i18n_st.kc_out))

View file

@ -1,83 +0,0 @@
# patterns.ludic - a msgid with holes ("Day {1}"): its literal pieces, how much English they pin
# down, and a chain by its first (else last) four bytes so a lookup tries only its own kind
function holes(s: string) -> int {
let n = len(s)
var h = 0
var i = 0
while i < n {
if hole_at(s, i) >= 0 {
h += 1
i = hole_end(s, i)
continue
}
i += 1
}
return h
}
function add_pattern(i18n_st: mut I18nState, id: string, tr: string) -> void {
let p = i18n_st.pat_n
i18n_st.pat_n += 1
let l0 = len(i18n_st.lits)
let n = len(id)
var seg = 0
var i = 0
var nl = 0
var score = 0
while i < n {
let num = hole_at(id, i)
if num >= 0 {
push(i18n_st.lits, id[seg..i])
score += i - seg
i18n_st.pat_ph[l0 + nl] = num
nl += 1
i = hole_end(id, i)
seg = i
continue
}
i += 1
}
push(i18n_st.lits, id[seg..n])
score += n - seg
nl += 1
i18n_st.pat_l0[p] = l0
i18n_st.pat_nl[p] = nl
i18n_st.pat_sc[p] = score
push(i18n_st.pat_out, tr)
chain(i18n_st, p, i18n_st.lits[l0], i18n_st.lits[l0 + nl - 1])
}
function chain(i18n_st: mut I18nState, p: int, first: string, last: string) -> void {
if len(first) >= 4 {
let key: string = first[0..4]
i18n_st.nx_p[p] = Dict.get_or(i18n_st.pfx, key, 0)
Dict.set(i18n_st.pfx, key, p + 1)
} else if len(last) >= 4 {
let key: string = last[len(last) - 4..len(last)]
i18n_st.nx_s[p] = Dict.get_or(i18n_st.sfx, key, 0)
Dict.set(i18n_st.sfx, key, p + 1)
} else {
i18n_st.nx_g[p] = i18n_st.gen_head
i18n_st.gen_head = p + 1
}
}
# a translation with its holes filled from caps, by number: a translation may reorder or drop them
function fill_holes(i18n_st: I18nState, t: string, l0: int, nl: int, cs: []string) -> string {
let n = len(t)
var r = ""
var seg = 0
var i = 0
while i < n {
let num = hole_at(t, i)
if num >= 0 {
r = r + t[seg..i]
for g in 0 .. nl - 1 { if i18n_st.pat_ph[l0 + g] == num { r = r + cs[g] } }
i = hole_end(t, i)
seg = i
continue
}
i += 1
}
return r + t[seg..n]
}

View file

@ -41,36 +41,3 @@ function plural_add(i18n_st: mut I18nState, e: PoEntry) -> void {
if k < len(e.forms) { push(i18n_st.forms, e.forms[k]) } else { push(i18n_st.forms, e.forms[len(e.forms) - 1]) } if k < len(e.forms) { push(i18n_st.forms, e.forms[k]) } else { push(i18n_st.forms, e.forms[len(e.forms) - 1]) }
} }
} }
# `one` or `many` by the count, in the language in use, with every {1} the count: Ln("{1} trout",
# "{1} trout", 3). English (or a line the file lacks) is one for 1 and many otherwise
export function Ln(i18n_st: I18nState, one: string, many: string, n: int) -> string {
var t = many
if n == 1 { t = one }
if i18n_st.on {
let k = Dict.get_or(i18n_st.plural, one, -1)
if k >= 0 {
let f = i18n_st.forms[k * 3 + i18n_plural_form(i18n_st.plural_rule, n)]
if not blank(f) { t = f }
}
}
return count_in(t, n)
}
@alloc_ok("a counted line (Ln) made when the game asks for one, outside a frame; its caller keeps it")
function count_in(t: string, n: int) -> string {
let m = len(t)
var r = ""
var seg = 0
var i = 0
while i < m {
if hole_at(t, i) == 1 {
r = r + t[seg..i] + `{n}`
i = hole_end(t, i)
seg = i
continue
}
i += 1
}
return r + t[seg..m]
}

View file

@ -24,23 +24,6 @@ export state I18nState {
font_now: string = "" font_now: string = ""
exact: Dict = null # msgid -> index into out exact: Dict = null # msgid -> index into out
out: []string = null out: []string = null
lits: []string = null # every pattern's literal pieces, one run per pattern
pat_out: []string = null
pat_l0: words = null # where a pattern's pieces start in lits
pat_nl: words = null # how many pieces (holes + 1)
pat_sc: words = null # bytes of English it pins down: more is a better match
pat_ph: words = null # by piece: the hole number that follows it
pat_n: int = 0
pfx: Dict = null # a pattern's first four bytes -> its chain head + 1
sfx: Dict = null # else its last four
gen_head: int = 0 # else the short list, tried on short strings only
nx_p: words = null
nx_s: words = null
nx_g: words = null
cache: Dict = null # English as drawn -> index into cache_out
cache_out: []string = null
caps: []string = null
hit: bool = false
plural: Dict = null # a plural msgid (the singular) -> index into forms / 3 plural: Dict = null # a plural msgid (the singular) -> index into forms / 3
forms: []string = null # three forms per entry, "" where the file has fewer forms: []string = null # three forms per entry, "" where the file has fewer
plural_rule: int = 0 # PL_* plural_rule: int = 0 # PL_*

View file

@ -3,7 +3,6 @@
const CACHE_MAX: int = 4000 const CACHE_MAX: int = 4000
const CAPS: int = 16
export function i18n_on(i18n_st: I18nState) -> bool { return i18n_st.on } export function i18n_on(i18n_st: I18nState) -> bool { return i18n_st.on }
export function i18n_current(i18n_st: I18nState) -> string { return i18n_st.cur } export function i18n_current(i18n_st: I18nState) -> string { return i18n_st.cur }
@ -12,7 +11,6 @@ export function i18n_exact_count(i18n_st: I18nState) -> int {
if i18n_st.out == null { return 0 } if i18n_st.out == null { return 0 }
return len(i18n_st.out) return len(i18n_st.out)
} }
export function i18n_pattern_count(i18n_st: I18nState) -> int { return i18n_st.pat_n }
# the overlay's own font, as the overlay was opened with it # the overlay's own font, as the overlay was opened with it
export function i18n_font_base(i18n_st: mut I18nState, path: string) -> void { i18n_st.font_now = path } export function i18n_font_base(i18n_st: mut I18nState, path: string) -> void { i18n_st.font_now = path }

View file

@ -3,22 +3,6 @@
property I18nTables { property I18nTables {
exact: Dict = null exact: Dict = null
out: []string = null out: []string = null
lits: []string = null
pat_out: []string = null
pat_l0: words = null
pat_nl: words = null
pat_sc: words = null
pat_ph: words = null
pat_n: int = 0
pfx: Dict = null
sfx: Dict = null
gen_head: int = 0
nx_p: words = null
nx_s: words = null
nx_g: words = null
cache: Dict = null
cache_out: []string = null
caps: []string = null
plural: Dict = null plural: Dict = null
forms: []string = null forms: []string = null
plural_rule: int = 0 plural_rule: int = 0
@ -29,22 +13,6 @@ function tables_keep(i18n_st: I18nState) -> I18nTables {
let t = new I18nTables let t = new I18nTables
t.exact = i18n_st.exact t.exact = i18n_st.exact
t.out = i18n_st.out t.out = i18n_st.out
t.lits = i18n_st.lits
t.pat_out = i18n_st.pat_out
t.pat_l0 = i18n_st.pat_l0
t.pat_nl = i18n_st.pat_nl
t.pat_sc = i18n_st.pat_sc
t.pat_ph = i18n_st.pat_ph
t.pat_n = i18n_st.pat_n
t.pfx = i18n_st.pfx
t.sfx = i18n_st.sfx
t.gen_head = i18n_st.gen_head
t.nx_p = i18n_st.nx_p
t.nx_s = i18n_st.nx_s
t.nx_g = i18n_st.nx_g
t.cache = i18n_st.cache
t.cache_out = i18n_st.cache_out
t.caps = i18n_st.caps
t.plural = i18n_st.plural t.plural = i18n_st.plural
t.forms = i18n_st.forms t.forms = i18n_st.forms
t.plural_rule = i18n_st.plural_rule t.plural_rule = i18n_st.plural_rule
@ -54,22 +22,6 @@ function tables_keep(i18n_st: I18nState) -> I18nTables {
function tables_put(i18n_st: mut I18nState, t: I18nTables) -> void { function tables_put(i18n_st: mut I18nState, t: I18nTables) -> void {
i18n_st.exact = t.exact i18n_st.exact = t.exact
i18n_st.out = t.out i18n_st.out = t.out
i18n_st.lits = t.lits
i18n_st.pat_out = t.pat_out
i18n_st.pat_l0 = t.pat_l0
i18n_st.pat_nl = t.pat_nl
i18n_st.pat_sc = t.pat_sc
i18n_st.pat_ph = t.pat_ph
i18n_st.pat_n = t.pat_n
i18n_st.pfx = t.pfx
i18n_st.sfx = t.sfx
i18n_st.gen_head = t.gen_head
i18n_st.nx_p = t.nx_p
i18n_st.nx_s = t.nx_s
i18n_st.nx_g = t.nx_g
i18n_st.cache = t.cache
i18n_st.cache_out = t.cache_out
i18n_st.caps = t.caps
i18n_st.plural = t.plural i18n_st.plural = t.plural
i18n_st.forms = t.forms i18n_st.forms = t.forms
i18n_st.plural_rule = t.plural_rule i18n_st.plural_rule = t.plural_rule

View file

@ -1,6 +1,6 @@
# i18n_test.ludic - languages from a shipped list and a mod folder, exact lines, patterns with their # i18n_test.ludic - languages from a shipped list and a mod folder, a language's keys and their holes,
# holes translated, a paragraph a sentence at a time, padding, plurals, the font a language names, # plurals, the font a language names, and plain text drawn as it is; every file written to the
# and English untouched throughout; every file written to the temp directory # temp directory
import "ludic.i18n" import "ludic.i18n"
program I18nTest { program I18nTest {
numbers float numbers float
@ -30,11 +30,10 @@ program I18nTest {
# a shipped "tt" with a font of its own, and a Russian-shaped plural # a shipped "tt" with a font of its own, and a Russian-shaped plural
function tt() -> string { function tt() -> string {
var s = header("Test Tongue", "\"X-Font: font_tt\\n\"\n\"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : 2);\\n\"") var s = header("Test Tongue", "\"X-Font: font_tt\\n\"\n\"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : 2);\\n\"")
s = s + po_line("Settings", "Zettings") + po_line("Plant fiber", "Lif") + po_line("a deer", "") + po_line("deer", "geyik") s = s + po_line("menu.settings", "Zettings") + po_line("items.fiber.name", "Lif") + po_line("hud.day", "{1}. Gun")
s = s + po_line("Day {1}", "{1}. Gun") + po_line("+{1} {2}", "{2} +{1}") + po_line("You saw {1}.", "{1} gordun.") s = s + po_line("pack.gain", "{2} +{1}") + po_line("empty.one", "")
s = s + po_line("You need to eat.", "Yemek lazim.") + po_line("owned", "senin") s = s + "msgid \"catch.count\"\nmsgid_plural \"catch.count\"\nmsgstr[0] \"{1} alabalik\"\nmsgstr[1] \"{1} alabaliga\"\nmsgstr[2] \"{1} alabaliklar\"\n\n"
s = s + "msgid \"{1} trout\"\nmsgid_plural \"{1} trout\"\nmsgstr[0] \"{1} alabalik\"\nmsgstr[1] \"{1} alabaliga\"\nmsgstr[2] \"{1} alabaliklar\"\n\n" s = s + "msgid \"multi.line\"\nmsgstr \"Cok\"\n\"satir\\n\"\n"
s = s + "msgid \"Multi\"\n\"line\"\nmsgstr \"Cok\"\n\"satir\\n\"\n"
return s return s
} }
@ -62,39 +61,25 @@ program I18nTest {
expect(L(i18n_st, "Settings") == "Settings") expect(L(i18n_st, "Settings") == "Settings")
} }
test "exact lines, patterns with translated holes, and English where unknown" (i18n_st: mut I18nState, i18n_test_st: mut I18nTestState) { test "a language's keys, their holes, and plain text drawn as it is" (i18n_st: mut I18nState, i18n_test_st: mut I18nTestState) {
fresh(i18n_st, i18n_test_st) fresh(i18n_st, i18n_test_st)
i18n_use(i18n_st, "tt") i18n_use(i18n_st, "tt")
expect(i18n_on(i18n_st)) expect(i18n_on(i18n_st))
expect(i18n_current(i18n_st) == "tt") expect(i18n_current(i18n_st) == "tt")
expect(L(i18n_st, "Settings") == "Zettings") expect(L(i18n_st, tr(k"menu.settings")) == "Zettings")
expect(L(i18n_st, "Day 12") == "12. Gun") expect(L(i18n_st, trf(k"hud.day", "12", "", "", "")) == "12. Gun")
expect(L(i18n_st, "+2 Plant fiber") == "Lif +2") expect(L(i18n_st, trf(k"pack.gain", "2", tr(k"items.fiber.name"), "", "")) == "Lif +2")
expect(L(i18n_st, "You saw a deer.") == "geyik gordun.") expect(L(i18n_st, tr(k"multi.line")) == "Coksatir\n")
expect(L(i18n_st, "zzqx words nobody wrote") == "zzqx words nobody wrote") expect(L(i18n_st, "Settings") == "Settings")
expect(L(i18n_st, "Multiline") == "Coksatir\n") expect_eq(i18n_exact_count(i18n_st), 5)
expect_eq(i18n_exact_count(i18n_st), 6)
expect_eq(i18n_pattern_count(i18n_st), 3)
} }
test "a paragraph a sentence at a time, and a layout's padding kept" (i18n_st: mut I18nState, i18n_test_st: mut I18nTestState) { test "plurals by the file's rule" (i18n_st: mut I18nState, i18n_test_st: mut I18nTestState) {
fresh(i18n_st, i18n_test_st) fresh(i18n_st, i18n_test_st)
i18n_use(i18n_st, "tt") i18n_use(i18n_st, "tt")
expect(L(i18n_st, "You need to eat. zzqx words nobody wrote.") == "Yemek lazim. zzqx words nobody wrote.")
expect(L(i18n_st, "zzqx. zzqy.") == "zzqx. zzqy.")
expect(L(i18n_st, " owned") == " senin")
expect(L(i18n_st, " zzqx") == " zzqx")
}
test "plurals by the file's rule, and English by one and many" (i18n_st: mut I18nState, i18n_test_st: mut I18nTestState) {
fresh(i18n_st, i18n_test_st)
expect(Ln(i18n_st, "{1} trout", "{1} trouts", 1) == "1 trout")
expect(Ln(i18n_st, "{1} trout", "{1} trouts", 3) == "3 trouts")
i18n_use(i18n_st, "tt")
expect_eq(i18n_plural_rule(i18n_st), PL_SLAVIC) expect_eq(i18n_plural_rule(i18n_st), PL_SLAVIC)
expect(Ln(i18n_st, "{1} trout", "{1} trouts", 21) == "21 alabalik") expect(L(i18n_st, trn(kn"catch.count", 21, "", "", "")) == "21 alabalik")
expect(Ln(i18n_st, "{1} trout", "{1} trouts", 5) == "5 alabaliklar") expect(L(i18n_st, trn(kn"catch.count", 5, "", "", "")) == "5 alabaliklar")
expect(Ln(i18n_st, "{1} fish", "{1} fishes", 2) == "2 fishes")
expect_eq(i18n_plural_form(PL_NOT_ONE, 0), 1) expect_eq(i18n_plural_form(PL_NOT_ONE, 0), 1)
expect_eq(i18n_plural_form(PL_ABOVE_ONE, 0), 0) expect_eq(i18n_plural_form(PL_ABOVE_ONE, 0), 0)
expect_eq(i18n_plural_form(PL_SLAVIC, 3), 1) expect_eq(i18n_plural_form(PL_SLAVIC, 3), 1)
@ -117,28 +102,28 @@ program I18nTest {
test "a language switched back to is its kept tables, not its file read again; a new probe reads it" (i18n_st: mut I18nState, i18n_test_st: mut I18nTestState) { test "a language switched back to is its kept tables, not its file read again; a new probe reads it" (i18n_st: mut I18nState, i18n_test_st: mut I18nTestState) {
fresh(i18n_st, i18n_test_st) fresh(i18n_st, i18n_test_st)
i18n_use(i18n_st, "tt") i18n_use(i18n_st, "tt")
let first = L(i18n_st, "Settings") let first = L(i18n_st, tr(k"menu.settings"))
Fs.write_text(dir() + "/tt.po", header("Tongue", "") + po_line("Settings", "Changed")) Fs.write_text(dir() + "/tt.po", header("Tongue", "") + po_line("menu.settings", "Changed"))
i18n_use(i18n_st, "en") i18n_use(i18n_st, "en")
i18n_use(i18n_st, "tt") i18n_use(i18n_st, "tt")
expect(L(i18n_st, "Settings") == first) expect(L(i18n_st, tr(k"menu.settings")) == first)
i18n_init(i18n_st) i18n_init(i18n_st)
i18n_use(i18n_st, "tt") i18n_use(i18n_st, "tt")
expect(L(i18n_st, "Settings") == "Changed") expect(L(i18n_st, tr(k"menu.settings")) == "Changed")
} }
test "a mod in the player's folder is found, replaces a shipped code, and goes when removed" (i18n_st: mut I18nState, i18n_test_st: mut I18nTestState) { test "a mod in the player's folder is found, replaces a shipped code, and goes when removed" (i18n_st: mut I18nState, i18n_test_st: mut I18nTestState) {
fresh(i18n_st, i18n_test_st) fresh(i18n_st, i18n_test_st)
Fs.write_text(mods() + "/zz.po", header("Mod Tongue", "") + po_line("Settings", "Mettings")) Fs.write_text(mods() + "/zz.po", header("Mod Tongue", "") + po_line("menu.settings", "Mettings"))
Fs.write_text(mods() + "/tt.po", header("Tongue Two", "") + po_line("Settings", "Z2")) Fs.write_text(mods() + "/tt.po", header("Tongue Two", "") + po_line("menu.settings", "Z2"))
i18n_init(i18n_st) i18n_init(i18n_st)
expect_eq(i18n_count(i18n_st), 3) expect_eq(i18n_count(i18n_st), 3)
expect(i18n_name_at(i18n_st, i18n_index(i18n_st, "zz")) == "Mod Tongue") expect(i18n_name_at(i18n_st, i18n_index(i18n_st, "zz")) == "Mod Tongue")
i18n_use(i18n_st, "zz") i18n_use(i18n_st, "zz")
expect(L(i18n_st, "Settings") == "Mettings") expect(L(i18n_st, tr(k"menu.settings")) == "Mettings")
expect(L(i18n_st, "Day 3") == "Day 3") expect(L(i18n_st, "Day 3") == "Day 3")
i18n_use(i18n_st, "tt") i18n_use(i18n_st, "tt")
expect(L(i18n_st, "Settings") == "Z2") expect(L(i18n_st, tr(k"menu.settings")) == "Z2")
Fs.remove(mods() + "/zz.po") Fs.remove(mods() + "/zz.po")
Fs.remove(mods() + "/tt.po") Fs.remove(mods() + "/tt.po")
i18n_init(i18n_st) i18n_init(i18n_st)
@ -158,9 +143,9 @@ program I18nTest {
test "a header field, and a .po loaded from text" (i18n_st: mut I18nState) { test "a header field, and a .po loaded from text" (i18n_st: mut I18nState) {
expect(i18n_header(tt(), "X-Language-Name") == "Test Tongue") expect(i18n_header(tt(), "X-Language-Name") == "Test Tongue")
expect(i18n_header(tt(), "X-Nothing") == "") expect(i18n_header(tt(), "X-Nothing") == "")
i18n_load_text(i18n_st, po_line("Quit", "Cik")) i18n_load_text(i18n_st, po_line("pause.quit", "Cik"))
expect(L(i18n_st, "Quit") == "Cik") expect(L(i18n_st, tr(k"pause.quit")) == "Cik")
i18n_off(i18n_st) i18n_off(i18n_st)
expect(L(i18n_st, "Quit") == "Quit") expect(not (L(i18n_st, tr(k"pause.quit")) == "Cik")) # back to English, whatever en.po is found
} }
} }

View file

@ -1,6 +1,6 @@
# keys_test.ludic - keys where English was: en.po as the base under every language, a key's text from the # keys_test.ludic - keys where English was: en.po as the base under every language, a key's text from the
# language in use else English else the key ([[key]] in a developer's build), holes filled by arguments # language in use else English else the key ([[key]] in a developer's build), holes filled by arguments
# that may be keys themselves, plural forms by each language's rule, and the old English form beside them # that may be keys themselves, plural forms by each language's rule, and plain text drawn as it is
import "ludic.i18n" import "ludic.i18n"
program I18nKeysTest { program I18nKeysTest {
numbers float numbers float
@ -68,24 +68,14 @@ program I18nKeysTest {
expect(L(i18n_st, trn(kn"catch.count", 2, "", "", "")) == "2 trout") # the language lacks it expect(L(i18n_st, trn(kn"catch.count", 2, "", "", "")) == "2 trout") # the language lacks it
} }
test "the old English form still works beside keys, so a game can move over a file at a time" (i18n_st: mut I18nState) { test "a plain string is drawn as it is in every language: a player named Settings stays Settings" (i18n_st: mut I18nState) {
start (i18n_st) start (i18n_st)
expect(L(i18n_st, "Settings") == "Settings")
i18n_load_text(i18n_st, tt()) i18n_load_text(i18n_st, tt())
expect(L(i18n_st, "Settings") == "Ayarlar") expect(L(i18n_st, "Settings") == "Settings" and L(i18n_st, tr(k"pause.resume")) == "Devam")
expect(L(i18n_st, tr(k"pause.resume")) == "Devam") expect(L(i18n_st, trf(k"catch.saw", "Settings", "5:30", "", "")) == "5:30 saatinde Settings gordun.") # a name in a hole too
expect(not i18n_is_key("Settings") and i18n_is_key(i18n_key("x")) and i18n_is_key(i18n_key_plural("x"))) expect(not i18n_is_key("Settings") and i18n_is_key(i18n_key("x")) and i18n_is_key(i18n_key_plural("x")))
} }
test "an English value handed into a key's hole still gets its old English line, until it is a key itself" (i18n_st: mut I18nState) {
start (i18n_st)
expect(L(i18n_st, trf(k"catch.saw", "Settings", "5:30", "", "")) == "You saw Settings at 5:30.")
i18n_load_text(i18n_st, tt())
expect(L(i18n_st, trf(k"catch.saw", "Settings", "5:30", "", "")) == "5:30 saatinde Ayarlar gordun.") # exact
expect(L(i18n_st, trf(k"catch.saw", "Day 3", "5:30", "", "")) == "5:30 saatinde 3. gun gordun.") # a pattern
expect(L(i18n_st, trf(k"catch.saw", "Nowhere known", "7", "", "")) == "7 saatinde Nowhere known gordun.") # no line: as it is
}
test "a key's text glued into an English line is made where it sits, until the line is a key of its own" (i18n_st: mut I18nState) { test "a key's text glued into an English line is made where it sits, until the line is a key of its own" (i18n_st: mut I18nState) {
start (i18n_st) start (i18n_st)
let elk = tr(k"species.elk.a_name") let elk = tr(k"species.elk.a_name")
@ -94,7 +84,7 @@ program I18nKeysTest {
expect(L(i18n_st, "Not owned. " + tr(k"pause.quit")) == "Not owned. Quit" and L(i18n_st, elk + "Quit") == "an elkQuit") expect(L(i18n_st, "Not owned. " + tr(k"pause.quit")) == "Not owned. Quit" and L(i18n_st, elk + "Quit") == "an elkQuit")
expect(L(i18n_st, trf(k"catch.saw", "Seen: " + elk, "5:30", "", "")) == "You saw Seen: an elk at 5:30.") expect(L(i18n_st, trf(k"catch.saw", "Seen: " + elk, "5:30", "", "")) == "You saw Seen: an elk at 5:30.")
i18n_load_text(i18n_st, tt()) i18n_load_text(i18n_st, tt())
expect(L(i18n_st, "Day " + tr(k"pause.resume")) == "Devam. gun") # the name made, then the line's own pattern expect(L(i18n_st, "Day " + tr(k"pause.resume")) == "Day Devam") # the name made; the words round it as they are
expect(L(i18n_st, trf(k"catch.saw", elk, "5:30", "", "")) == "5:30 saatinde bir geyik gordun.") # a key whole is untouched expect(L(i18n_st, trf(k"catch.saw", elk, "5:30", "", "")) == "5:30 saatinde bir geyik gordun.") # a key whole is untouched
} }
} }

View file

@ -28,8 +28,8 @@ program I18nNestTest {
expect(L(i18n_st, trf(k"shop.ada", day, "", "", "")) == "Ada: Day 3") expect(L(i18n_st, trf(k"shop.ada", day, "", "", "")) == "Ada: Day 3")
expect(L(i18n_st, b(trf(k"shop.ada", day, "", "", ""))) == "Ada: Day 3") expect(L(i18n_st, b(trf(k"shop.ada", day, "", "", ""))) == "Ada: Day 3")
i18n_load_text(i18n_st, tt()) i18n_load_text(i18n_st, tt())
expect(L(i18n_st, "Settings " + day) == "Settings 3. Gun") # the rest is English, looked up as it was expect(L(i18n_st, "Settings " + day) == "Settings 3. Gun") # the words round it drawn as they are
expect(L(i18n_st, b("Settings")) == "Ayarlar") expect(L(i18n_st, b("Settings")) == "Settings") # a name in a bracket too
} }
test "a bracket with no partner draws nothing of itself" (i18n_st: mut I18nState) { test "a bracket with no partner draws nothing of itself" (i18n_st: mut I18nState) {

View file

@ -1,83 +1,10 @@
# translate.ludic - what the game draws, in the language in use: a key (keyed_render.ludic); else, the old # translate.ludic - what the game draws, in the language in use: a key (keyed_render.ludic), a line with
# English form, exact, else a pattern, else padding taken off, else a paragraph a sentence at a time # a key glued into it (mixed.ludic) or bracketed inside it (nest.ludic); anything else is drawn as it is -
# a player's name, a chat line, a number. English lives in en.po under keys, never looked up by its words
export function L(i18n_st: mut I18nState, s: string) -> string { export function L(i18n_st: mut I18nState, s: string) -> string {
let mixed = kx_mixed(s) let mixed = kx_mixed(s)
if i18n_is_key(s) and not mixed { return keyed(i18n_st, s) } # a key whole: its arguments are made in it if i18n_is_key(s) and not mixed { return keyed(i18n_st, s) } # a key whole: its arguments are made in it
if kx_nested(s) { return kx_nest_line(i18n_st, s) } if kx_nested(s) { return kx_nest_line(i18n_st, s) }
if mixed { return kx_line(i18n_st, s) } if mixed { return kx_line(i18n_st, s) }
if not i18n_st.on { return s } return s
let ci = Dict.get_or(i18n_st.cache, s, -1)
if ci >= 0 { return i18n_st.cache_out[ci] }
let r = whole(i18n_st, s)
if Dict.size(i18n_st.cache) >= CACHE_MAX {
Dict.clear(i18n_st.cache)
List.clear(i18n_st.cache_out)
}
@alloc_ok("a translation cache miss: the entry is kept, up to CACHE_MAX")
Dict.set(i18n_st.cache, s, len(i18n_st.cache_out))
@alloc_ok("a translation cache miss: as above")
push(i18n_st.cache_out, r)
return r
}
@alloc_ok("a translation cache miss (L): once per distinct text, bounded by CACHE_MAX")
function whole(i18n_st: mut I18nState, s: string) -> string {
let n = len(s)
if n == 0 { return s }
let e = Dict.get_or(i18n_st.exact, s, -1)
if e >= 0 { return i18n_st.out[e] }
let r = pattern(i18n_st, s, 0)
if i18n_st.hit { return r }
# the padding a layout put round a word: " owned"
var a = 0
var b = n
while a < n and s[a] == 32 { a += 1 }
while b > a and s[b - 1] == 32 { b -= 1 }
if a > 0 or b < n {
if b <= a { return s }
let core: string = s[a..b]
let t = whole(i18n_st, core)
if t == core { return s }
return s[0..a] + t + s[b..n]
}
return sentences(i18n_st, s)
}
# a paragraph built from sentences, one at a time; English stays where a sentence is unknown
@alloc_ok("a translation cache miss (L): once per distinct text")
function sentences(i18n_st: mut I18nState, s: string) -> string {
let n = len(s)
var r = ""
var start = 0
var any = false
var pieces = 0
var i = 0
while i < n - 1 {
let c = s[i]
if (c == 46 or c == 33 or c == 63) and s[i + 1] == 32 {
let t = sentence(i18n_st, s[start..i + 1])
if i18n_st.hit { any = true }
r = r + t + " "
start = i + 2
pieces += 1
i += 2
continue
}
i += 1
}
if pieces == 0 { return s }
let t = sentence(i18n_st, s[start..n])
if i18n_st.hit { any = true }
if not any { return s }
return r + t
}
function sentence(i18n_st: mut I18nState, p: string) -> string {
i18n_st.hit = false
let e = Dict.get_or(i18n_st.exact, p, -1)
if e >= 0 {
i18n_st.hit = true
return i18n_st.out[e]
}
return pattern(i18n_st, p, 0)
} }

View file

@ -40,45 +40,16 @@ export function i18n_load_text(i18n_st: mut I18nState, text: string) -> void {
kc_clear(i18n_st) kc_clear(i18n_st)
i18n_st.exact = Dict.new() i18n_st.exact = Dict.new()
i18n_st.out = new []string i18n_st.out = new []string
i18n_st.cache = Dict.new()
i18n_st.cache_out = new []string
i18n_st.caps = new []string
for c in 0 .. CAPS { push(i18n_st.caps, "") }
i18n_st.plural = Dict.new() i18n_st.plural = Dict.new()
i18n_st.forms = new []string i18n_st.forms = new []string
i18n_st.plural_rule = plural_rule_of(i18n_header(text, "Plural-Forms")) i18n_st.plural_rule = plural_rule_of(i18n_header(text, "Plural-Forms"))
var np = 0
var nlit = 0
for i in 0 .. len(es) {
if blank(es[i].id) or blank(es[i].str) { continue }
let h = holes(es[i].id)
if h > 0 {
np += 1
nlit += h + 1
}
}
i18n_st.pat_out = new []string
i18n_st.lits = new []string
i18n_st.pat_l0 = words(np + 1)
i18n_st.pat_nl = words(np + 1)
i18n_st.pat_sc = words(np + 1)
i18n_st.pat_ph = words(nlit + 1)
i18n_st.nx_p = words(np + 1)
i18n_st.nx_s = words(np + 1)
i18n_st.nx_g = words(np + 1)
i18n_st.pfx = Dict.new()
i18n_st.sfx = Dict.new()
i18n_st.gen_head = 0
i18n_st.pat_n = 0
for i in 0 .. len(es) { for i in 0 .. len(es) {
let e = es[i] let e = es[i]
if len(e.plural) > 0 { plural_add(i18n_st, e) } else if not (blank(e.id) or blank(e.str)) { if len(e.plural) > 0 { plural_add(i18n_st, e) } else if not (blank(e.id) or blank(e.str)) {
if holes(e.id) > 0 { add_pattern(i18n_st, e.id, e.str) } else {
Dict.set(i18n_st.exact, e.id, len(i18n_st.out)) Dict.set(i18n_st.exact, e.id, len(i18n_st.out))
push(i18n_st.out, e.str) push(i18n_st.out, e.str)
} }
} }
}
i18n_st.on = true i18n_st.on = true
} }