diff --git a/changes/i18n-keys-only.md b/changes/i18n-keys-only.md new file mode 100644 index 00000000..546b09c8 --- /dev/null +++ b/changes/i18n-keys-only.md @@ -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. diff --git a/packages/README.md b/packages/README.md index 5fb9e962..44292ba3 100644 --- a/packages/README.md +++ b/packages/README.md @@ -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.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.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.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 | diff --git a/packages/ludic.i18n/README.md b/packages/ludic.i18n/README.md index 24f4cdc6..b930f76d 100644 --- a/packages/ludic.i18n/README.md +++ b/packages/ludic.i18n/README.md @@ -1,19 +1,39 @@ # ludic.i18n -A game in any language, from gettext files anyone can add. English is the source and stays in the -code: nothing here, and nothing a translator does, rewrites a string in the game. A language is a -`.po` keyed by the English exactly as the game draws it, and text is translated at the moment it is -drawn, so a string that reaches the screen is covered wherever it was built. Uses nothing - not -even `ludic.base`; the font atlas and the mod folder are the game's, through a port. +A game in any language, from gettext files anyone can add. The code names text by KEYS, never by its +English: a key says what a text is FOR (`pause.resume`), `en.po` says what it says in English, and +every other language is a `.po` keyed the same way. Text is made at the moment it is drawn, so a +line that reaches the screen is covered wherever it was built. Uses nothing - not even +`ludic.base`; the font atlas and the mod folder are the game's, through a port. ```ludic import "ludic.i18n" -i18n_use("tr") # from settings; the atlas follows -ov_text(x, y, 16, L("Settings"), ...) -ov_text(x, y, 16, L(`Day {day}`), ...) # "Day {1}" in the file matches "Day 12" -ov_text(x, y, 16, Ln("{1} trout", "{1} trout", n), ...) +i18n_use("tr") # from settings; the atlas follows +ov_text(x, y, 16, L(tr(k"pause.resume")), ...) # "Resume"; "Devam" in Turkish +ov_text(x, y, 16, L(trf(k"hud.day", `{day}`, "", "", "")), ...) +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 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. `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. - -## 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. +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. ## Config and port @@ -73,8 +61,7 @@ port I18nWorld { | | | | --- | --- | -| `L(s) -> string` | what the game draws, in the language in use (cached) | -| `Ln(one, many, n) -> string` | a count's form, with `{1}` the count; English is one for 1, many otherwise (the old form) | +| `L(s) -> string` | what the game draws, in the language in use (made once per distinct line, kept) | | `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_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_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_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 | -## 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 ```bash ludic test packages/ludic.i18n ``` -A shipped list and a mod folder in the temp directory: exact lines, patterns with translated holes -and articles, paragraphs, padding, plurals (a Russian-shaped rule), the font and its fallback, a mod -replacing a shipped code, a forced code, and English untouched. +`keys_test`: keys, holes, plurals, a key glued into text, plain text as it is. `nest_test`: lines +inside lines, bare, bracketed and escaped. `i18n_test`: a shipped list and a mod folder in the temp +directory, a language's keys, plurals (a Russian-shaped rule), the font and its fallback, a mod +replacing a shipped code, a forced code. diff --git a/packages/ludic.i18n/index.ludic b/packages/ludic.i18n/index.ludic index dda50feb..50268021 100644 --- a/packages/ludic.i18n/index.ludic +++ b/packages/ludic.i18n/index.ludic @@ -8,8 +8,6 @@ import "bytes.ludic" import "po.ludic" import "langs.ludic" import "use.ludic" -import "patterns.ludic" -import "match.ludic" import "translate.ludic" import "keys.ludic" import "keyed.ludic" diff --git a/packages/ludic.i18n/keyed_args.ludic b/packages/ludic.i18n/keyed_args.ludic index e3668e1d..d71cc745 100644 --- a/packages/ludic.i18n/keyed_args.ludic +++ b/packages/ludic.i18n/keyed_args.ludic @@ -21,13 +21,6 @@ function kr_unescape(s: string) -> string { 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 function kr_int(s: string) -> int { var v = 0 diff --git a/packages/ludic.i18n/keyed_render.ludic b/packages/ludic.i18n/keyed_render.ludic index d62131bf..c0e6e0a5 100644 --- a/packages/ludic.i18n/keyed_render.ludic +++ b/packages/ludic.i18n/keyed_render.ludic @@ -60,7 +60,7 @@ function kr_fill(i18n_st: mut I18nState, t: string, args: []string) -> string { if h >= 1 { var v = "" 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 i = hole_end(t, i) seg = i diff --git a/packages/ludic.i18n/match.ludic b/packages/ludic.i18n/match.ludic deleted file mode 100644 index 38ff7244..00000000 --- a/packages/ludic.i18n/match.ludic +++ /dev/null @@ -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 -} diff --git a/packages/ludic.i18n/mixed.ludic b/packages/ludic.i18n/mixed.ludic index a1576e37..4c30f9c3 100644 --- a/packages/ludic.i18n/mixed.ludic +++ b/packages/ludic.i18n/mixed.ludic @@ -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 -# 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 # 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) } let ci = Dict.get_or(i18n_st.kc, s, -1) if ci >= 0 { return i18n_st.kc_out[ci] } - var r = kx_unmix(i18n_st, s) - if i18n_st.on { r = whole(i18n_st, r) } + let r = kx_unmix(i18n_st, s) 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") Dict.set(i18n_st.kc, s, len(i18n_st.kc_out)) diff --git a/packages/ludic.i18n/patterns.ludic b/packages/ludic.i18n/patterns.ludic deleted file mode 100644 index c12ebfd1..00000000 --- a/packages/ludic.i18n/patterns.ludic +++ /dev/null @@ -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] -} diff --git a/packages/ludic.i18n/plural.ludic b/packages/ludic.i18n/plural.ludic index 165551fb..20893de1 100644 --- a/packages/ludic.i18n/plural.ludic +++ b/packages/ludic.i18n/plural.ludic @@ -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]) } } } - -# `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] -} diff --git a/packages/ludic.i18n/ports.ludic b/packages/ludic.i18n/ports.ludic index f2f8cc50..70342c08 100644 --- a/packages/ludic.i18n/ports.ludic +++ b/packages/ludic.i18n/ports.ludic @@ -24,23 +24,6 @@ export state I18nState { font_now: string = "" exact: Dict = null # msgid -> index into out 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 forms: []string = null # three forms per entry, "" where the file has fewer plural_rule: int = 0 # PL_* diff --git a/packages/ludic.i18n/state.ludic b/packages/ludic.i18n/state.ludic index b59e3318..469a5035 100644 --- a/packages/ludic.i18n/state.ludic +++ b/packages/ludic.i18n/state.ludic @@ -3,7 +3,6 @@ const CACHE_MAX: int = 4000 -const CAPS: int = 16 export function i18n_on(i18n_st: I18nState) -> bool { return i18n_st.on } 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 } 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 export function i18n_font_base(i18n_st: mut I18nState, path: string) -> void { i18n_st.font_now = path } diff --git a/packages/ludic.i18n/tables.ludic b/packages/ludic.i18n/tables.ludic index fadd8403..3e60a05f 100644 --- a/packages/ludic.i18n/tables.ludic +++ b/packages/ludic.i18n/tables.ludic @@ -3,22 +3,6 @@ property I18nTables { exact: Dict = 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 forms: []string = null plural_rule: int = 0 @@ -29,22 +13,6 @@ function tables_keep(i18n_st: I18nState) -> I18nTables { let t = new I18nTables t.exact = i18n_st.exact 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.forms = i18n_st.forms 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 { i18n_st.exact = t.exact 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.forms = t.forms i18n_st.plural_rule = t.plural_rule diff --git a/packages/ludic.i18n/tests/i18n_test.ludic b/packages/ludic.i18n/tests/i18n_test.ludic index fd206573..7ac0a4da 100644 --- a/packages/ludic.i18n/tests/i18n_test.ludic +++ b/packages/ludic.i18n/tests/i18n_test.ludic @@ -1,6 +1,6 @@ -# i18n_test.ludic - languages from a shipped list and a mod folder, exact lines, patterns with their -# holes translated, a paragraph a sentence at a time, padding, plurals, the font a language names, -# and English untouched throughout; every file written to the temp directory +# i18n_test.ludic - languages from a shipped list and a mod folder, a language's keys and their holes, +# plurals, the font a language names, and plain text drawn as it is; every file written to the +# temp directory import "ludic.i18n" program I18nTest { numbers float @@ -30,11 +30,10 @@ program I18nTest { # a shipped "tt" with a font of its own, and a Russian-shaped plural 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\"") - s = s + po_line("Settings", "Zettings") + po_line("Plant fiber", "Lif") + po_line("a deer", "") + po_line("deer", "geyik") - 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("You need to eat.", "Yemek lazim.") + po_line("owned", "senin") - 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\"\n\"line\"\nmsgstr \"Cok\"\n\"satir\\n\"\n" + s = s + po_line("menu.settings", "Zettings") + po_line("items.fiber.name", "Lif") + po_line("hud.day", "{1}. Gun") + s = s + po_line("pack.gain", "{2} +{1}") + po_line("empty.one", "") + 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 \"multi.line\"\nmsgstr \"Cok\"\n\"satir\\n\"\n" return s } @@ -62,39 +61,25 @@ program I18nTest { 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) i18n_use(i18n_st, "tt") expect(i18n_on(i18n_st)) expect(i18n_current(i18n_st) == "tt") - expect(L(i18n_st, "Settings") == "Zettings") - expect(L(i18n_st, "Day 12") == "12. Gun") - expect(L(i18n_st, "+2 Plant fiber") == "Lif +2") - expect(L(i18n_st, "You saw a deer.") == "geyik gordun.") - expect(L(i18n_st, "zzqx words nobody wrote") == "zzqx words nobody wrote") - expect(L(i18n_st, "Multiline") == "Coksatir\n") - expect_eq(i18n_exact_count(i18n_st), 6) - expect_eq(i18n_pattern_count(i18n_st), 3) + expect(L(i18n_st, tr(k"menu.settings")) == "Zettings") + expect(L(i18n_st, trf(k"hud.day", "12", "", "", "")) == "12. Gun") + expect(L(i18n_st, trf(k"pack.gain", "2", tr(k"items.fiber.name"), "", "")) == "Lif +2") + expect(L(i18n_st, tr(k"multi.line")) == "Coksatir\n") + expect(L(i18n_st, "Settings") == "Settings") + expect_eq(i18n_exact_count(i18n_st), 5) } - 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) 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(Ln(i18n_st, "{1} trout", "{1} trouts", 21) == "21 alabalik") - expect(Ln(i18n_st, "{1} trout", "{1} trouts", 5) == "5 alabaliklar") - expect(Ln(i18n_st, "{1} fish", "{1} fishes", 2) == "2 fishes") + expect(L(i18n_st, trn(kn"catch.count", 21, "", "", "")) == "21 alabalik") + expect(L(i18n_st, trn(kn"catch.count", 5, "", "", "")) == "5 alabaliklar") 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_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) { fresh(i18n_st, i18n_test_st) i18n_use(i18n_st, "tt") - let first = L(i18n_st, "Settings") - Fs.write_text(dir() + "/tt.po", header("Tongue", "") + po_line("Settings", "Changed")) + let first = L(i18n_st, tr(k"menu.settings")) + Fs.write_text(dir() + "/tt.po", header("Tongue", "") + po_line("menu.settings", "Changed")) i18n_use(i18n_st, "en") 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_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) { fresh(i18n_st, i18n_test_st) - Fs.write_text(mods() + "/zz.po", header("Mod Tongue", "") + po_line("Settings", "Mettings")) - Fs.write_text(mods() + "/tt.po", header("Tongue Two", "") + po_line("Settings", "Z2")) + 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("menu.settings", "Z2")) i18n_init(i18n_st) expect_eq(i18n_count(i18n_st), 3) expect(i18n_name_at(i18n_st, i18n_index(i18n_st, "zz")) == "Mod Tongue") 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") 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() + "/tt.po") i18n_init(i18n_st) @@ -158,9 +143,9 @@ program I18nTest { 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-Nothing") == "") - i18n_load_text(i18n_st, po_line("Quit", "Cik")) - expect(L(i18n_st, "Quit") == "Cik") + i18n_load_text(i18n_st, po_line("pause.quit", "Cik")) + expect(L(i18n_st, tr(k"pause.quit")) == "Cik") 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 } } diff --git a/packages/ludic.i18n/tests/keys_test.ludic b/packages/ludic.i18n/tests/keys_test.ludic index 045692eb..62119ffe 100644 --- a/packages/ludic.i18n/tests/keys_test.ludic +++ b/packages/ludic.i18n/tests/keys_test.ludic @@ -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 # 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" program I18nKeysTest { numbers float @@ -68,24 +68,14 @@ program I18nKeysTest { 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) - expect(L(i18n_st, "Settings") == "Settings") i18n_load_text(i18n_st, tt()) - expect(L(i18n_st, "Settings") == "Ayarlar") - expect(L(i18n_st, tr(k"pause.resume")) == "Devam") + expect(L(i18n_st, "Settings") == "Settings" and 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"))) } - 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) { start (i18n_st) 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, trf(k"catch.saw", "Seen: " + elk, "5:30", "", "")) == "You saw Seen: an elk at 5:30.") 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 } } diff --git a/packages/ludic.i18n/tests/nest_test.ludic b/packages/ludic.i18n/tests/nest_test.ludic index 1bf5eaab..d3f10f8c 100644 --- a/packages/ludic.i18n/tests/nest_test.ludic +++ b/packages/ludic.i18n/tests/nest_test.ludic @@ -28,8 +28,8 @@ program I18nNestTest { 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") 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, b("Settings")) == "Ayarlar") + expect(L(i18n_st, "Settings " + day) == "Settings 3. Gun") # the words round it drawn as they are + 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) { diff --git a/packages/ludic.i18n/translate.ludic b/packages/ludic.i18n/translate.ludic index 6ea88c52..85214399 100644 --- a/packages/ludic.i18n/translate.ludic +++ b/packages/ludic.i18n/translate.ludic @@ -1,83 +1,10 @@ -# translate.ludic - what the game draws, in the language in use: a key (keyed_render.ludic); else, the old -# English form, exact, else a pattern, else padding taken off, else a paragraph a sentence at a time +# translate.ludic - what the game draws, in the language in use: a key (keyed_render.ludic), a line with +# 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 { 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 kx_nested(s) { return kx_nest_line(i18n_st, s) } if mixed { return kx_line(i18n_st, s) } - if not i18n_st.on { 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) + return s } diff --git a/packages/ludic.i18n/use.ludic b/packages/ludic.i18n/use.ludic index b0533f3b..f93e70b8 100644 --- a/packages/ludic.i18n/use.ludic +++ b/packages/ludic.i18n/use.ludic @@ -40,43 +40,14 @@ export function i18n_load_text(i18n_st: mut I18nState, text: string) -> void { kc_clear(i18n_st) i18n_st.exact = Dict.new() 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.forms = new []string 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) { 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 holes(e.id) > 0 { add_pattern(i18n_st, e.id, e.str) } else { - Dict.set(i18n_st.exact, e.id, len(i18n_st.out)) - push(i18n_st.out, e.str) - } + Dict.set(i18n_st.exact, e.id, len(i18n_st.out)) + push(i18n_st.out, e.str) } } i18n_st.on = true