wip(0.S3): the packages migrated by ludic migrate state packages - every package test green

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-25 14:02:21 +03:00
parent 1e8b5b0523
commit 07505e7ef2
294 changed files with 14996 additions and 14675 deletions

View file

@ -1,18 +1,18 @@
# langs.ludic - where languages come from, later ones replacing earlier ones with the same code:
# <lang_dir>/languages.txt (one code per line, # a comment) and every .po in the mod folder
export function i18n_init() -> void {
codes = new []string
names = new []string
paths = new []string
fonts = new []string
lang_add("en", "English", "", "")
let idx = Fs.read_text(lang_dir + "/languages.txt")
export function i18n_init(i18n_st: mut I18nState) -> void {
i18n_st.codes = new []string
i18n_st.names = new []string
i18n_st.paths = new []string
i18n_st.fonts = new []string
lang_add(i18n_st, "en", "English", "", "")
let idx = Fs.read_text(i18n_st.lang_dir + "/languages.txt")
if idx != null {
let ls = lines_of(idx)
for i in 0 .. len(ls) {
let ln = trim(ls[i])
if len(ln) == 0 or ln[0] == 35 { continue }
probe(ln, `{lang_dir}/{ln}.po`, lang_dir)
probe(i18n_st, ln, `{i18n_st.lang_dir}/{ln}.po`, i18n_st.lang_dir)
}
}
let md = i18n_mod_dir()
@ -23,60 +23,60 @@ export function i18n_init() -> void {
for i in 0 .. len(files) {
let nm = files[i]
let n = len(nm)
if n > 3 and i18n_ends(nm, ".po") { probe(nm[0..n - 3], md + "/" + nm, md) }
if n > 3 and i18n_ends(nm, ".po") { probe(i18n_st, nm[0..n - 3], md + "/" + nm, md) }
}
}
# where a player's own .po files go; "" when the game has none
export function i18n_mod_dir() -> string { return I18nWorld.mod_dir() }
function lang_add(code: string, name: string, path: string, font: string) -> void {
for i in 0 .. len(codes) {
if codes[i] == code {
names[i] = name
paths[i] = path
fonts[i] = font
function lang_add(i18n_st: mut I18nState, code: string, name: string, path: string, font: string) -> void {
for i in 0 .. len(i18n_st.codes) {
if i18n_st.codes[i] == code {
i18n_st.names[i] = name
i18n_st.paths[i] = path
i18n_st.fonts[i] = font
return
}
}
push(codes, code)
push(names, name)
push(paths, path)
push(fonts, font)
push(i18n_st.codes, code)
push(i18n_st.names, name)
push(i18n_st.paths, path)
push(i18n_st.fonts, font)
}
function probe(code: string, path: string, dir: string) -> void {
function probe(i18n_st: mut I18nState, code: string, path: string, dir: string) -> void {
let text = Fs.read_text(path)
if text == null { return }
var name = i18n_header(text, "X-Language-Name")
if len(name) == 0 { name = code }
var font = i18n_header(text, "X-Font")
if len(font) > 0 { font = dir + "/" + font }
lang_add(code, name, path, font)
lang_add(i18n_st, code, name, path, font)
}
export function i18n_count() -> int {
if codes == null { return 1 }
return len(codes)
export function i18n_count(i18n_st: I18nState) -> int {
if i18n_st.codes == null { return 1 }
return len(i18n_st.codes)
}
export function i18n_index(code: string) -> int {
if codes != null { for i in 0 .. len(codes) { if codes[i] == code { return i } } }
export function i18n_index(i18n_st: I18nState, code: string) -> int {
if i18n_st.codes != null { for i in 0 .. len(i18n_st.codes) { if i18n_st.codes[i] == code { return i } } }
return 0
}
export function i18n_code_at(i: int) -> string {
if codes == null or i < 0 or i >= len(codes) { return "en" }
return codes[i]
export function i18n_code_at(i18n_st: I18nState, i: int) -> string {
if i18n_st.codes == null or i < 0 or i >= len(i18n_st.codes) { return "en" }
return i18n_st.codes[i]
}
export function i18n_name_at(i: int) -> string {
if names == null or i < 0 or i >= len(names) { return "English" }
return names[i]
export function i18n_name_at(i18n_st: I18nState, i: int) -> string {
if i18n_st.names == null or i < 0 or i >= len(i18n_st.names) { return "English" }
return i18n_st.names[i]
}
# the font atlas a language names with X-Font, beside its .po; "" is the default
export function i18n_font_at(i: int) -> string {
if fonts == null or i < 0 or i >= len(fonts) { return "" }
return fonts[i]
export function i18n_font_at(i18n_st: I18nState, i: int) -> string {
if i18n_st.fonts == null or i < 0 or i >= len(i18n_st.fonts) { return "" }
return i18n_st.fonts[i]
}

View file

@ -2,13 +2,13 @@
# 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(p: int, s: string) -> bool {
function try_pattern(i18n_st: mut I18nState, p: int, s: string) -> bool {
let n = len(s)
let l0 = pat_l0[p]
let nl = pat_nl[p]
let first = lits[l0]
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 = lits[l0 + nl - 1]
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
@ -20,76 +20,76 @@ function try_pattern(p: int, s: string) -> bool {
var stop = lim
var adv = 0
if g < nl - 2 {
let mid = lits[l0 + g + 1]
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 }
caps[g] = s[pos..stop]
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(head: int, next: words, s: string, best: int) -> int {
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 = pat_sc[b] }
if pat_sc[p] > sc and try_pattern(p, s) { b = p }
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
}
function pattern(s: string, depth: int) -> string {
hit = false
if pat_n == 0 { return s }
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(Dict.get_or(pfx, s[0..4], 0), nx_p, s, best)
best = best_of(Dict.get_or(sfx, s[n - 4..n], 0), nx_s, s, best)
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(gen_head, nx_g, 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(best, s)
let l0 = pat_l0[best]
let nl = pat_nl[best]
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, caps[g]) }
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(raw[g], depth + 1)) }
let r = fill_holes(pat_out[best], l0, nl, cs)
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 }
hit = true
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(c: string, depth: int) -> string {
let e = Dict.get_or(exact, c, -1)
if e >= 0 { return out[e] }
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(exact, c[bare..n], -1)
if e2 >= 0 { return out[e2] }
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(c, depth)
if hit { return r }
let r = pattern(i18n_st, c, depth)
if i18n_st.hit { return r }
}
return c
}

View file

@ -15,10 +15,10 @@ function holes(s: string) -> int {
return h
}
function add_pattern(id: string, tr: string) -> void {
let p = pat_n
pat_n += 1
let l0 = len(lits)
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
@ -27,9 +27,9 @@ function add_pattern(id: string, tr: string) -> void {
while i < n {
let num = hole_at(id, i)
if num >= 0 {
push(lits, id[seg..i])
push(i18n_st.lits, id[seg..i])
score += i - seg
pat_ph[l0 + nl] = num
i18n_st.pat_ph[l0 + nl] = num
nl += 1
i = hole_end(id, i)
seg = i
@ -37,33 +37,33 @@ function add_pattern(id: string, tr: string) -> void {
}
i += 1
}
push(lits, id[seg..n])
push(i18n_st.lits, id[seg..n])
score += n - seg
nl += 1
pat_l0[p] = l0
pat_nl[p] = nl
pat_sc[p] = score
push(pat_out, tr)
chain(p, lits[l0], lits[l0 + 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(p: int, first: string, last: string) -> void {
function chain(i18n_st: mut I18nState, p: int, first: string, last: string) -> void {
if len(first) >= 4 {
let key: string = first[0..4]
nx_p[p] = Dict.get_or(pfx, key, 0)
Dict.set(pfx, key, p + 1)
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)]
nx_s[p] = Dict.get_or(sfx, key, 0)
Dict.set(sfx, key, p + 1)
i18n_st.nx_s[p] = Dict.get_or(i18n_st.sfx, key, 0)
Dict.set(i18n_st.sfx, key, p + 1)
} else {
nx_g[p] = gen_head
gen_head = p + 1
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(t: string, l0: int, nl: int, cs: []string) -> string {
function fill_holes(i18n_st: I18nState, t: string, l0: int, nl: int, cs: []string) -> string {
let n = len(t)
var r = ""
var seg = 0
@ -72,7 +72,7 @@ function fill_holes(t: string, l0: int, nl: int, cs: []string) -> string {
let num = hole_at(t, i)
if num >= 0 {
r = r + t[seg..i]
for g in 0 .. nl - 1 { if pat_ph[l0 + g] == num { r = r + cs[g] } }
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

View file

@ -15,7 +15,7 @@ function plural_rule_of(h: string) -> int {
return PL_NOT_ONE
}
export function i18n_plural_rule() -> int { return plural_rule }
export function i18n_plural_rule(i18n_st: I18nState) -> int { return i18n_st.plural_rule }
# which msgstr[k] a count takes under a rule
export function i18n_plural_form(rule: int, n: int) -> int {
@ -33,23 +33,23 @@ export function i18n_plural_form(rule: int, n: int) -> int {
return 0
}
function plural_add(e: PoEntry) -> void {
function plural_add(i18n_st: mut I18nState, e: PoEntry) -> void {
if len(e.forms) == 0 or blank(e.forms[0]) { return }
Dict.set(plural, e.id, len(forms) / 3)
Dict.set(i18n_st.plural, e.id, len(i18n_st.forms) / 3)
for k in 0 .. 3 {
if k < len(e.forms) { push(forms, e.forms[k]) } else { push(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(one: string, many: string, n: int) -> string {
export function Ln(i18n_st: I18nState, one: string, many: string, n: int) -> string {
var t = many
if n == 1 { t = one }
if on {
let k = Dict.get_or(plural, one, -1)
if i18n_st.on {
let k = Dict.get_or(i18n_st.plural, one, -1)
if k >= 0 {
let f = forms[k * 3 + i18n_plural_form(plural_rule, n)]
let f = i18n_st.forms[k * 3 + i18n_plural_form(i18n_st.plural_rule, n)]
if not blank(f) { t = f }
}
}

View file

@ -12,13 +12,44 @@ function i18n__no_code() -> string { return "" }
function i18n__no() -> bool { return false }
function i18n__no_font(dir: string) -> bool { return false }
var lang_dir: string = "assets/lang"
var font_default: string = "assets/kit/font"
export state I18nState {
lang_dir: string = "assets/lang"
font_default: string = "assets/kit/font"
on: bool = false # a language other than English is drawing
cur: string = "en"
codes: []string = null # every language found, "en" first
names: []string = null
paths: []string = null
fonts: []string = null # a font atlas directory, "" for the game's own
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_*
}
# where the shipped languages are (a directory with languages.txt and <code>.po), and the atlas a
# language with no X-Font draws in
export function i18n_config(dir: string, default_font: string) -> void {
lang_dir = dir
font_default = default_font
codes = null
export function i18n_config(i18n_st: mut I18nState, dir: string, default_font: string) -> void {
i18n_st.lang_dir = dir
i18n_st.font_default = default_font
i18n_st.codes = null
}

View file

@ -1,47 +1,18 @@
# state.ludic - the languages found, the one in use, and the loaded file's lookups
var on: bool = false # a language other than English is drawing
var cur: string = "en"
var codes: []string = null # every language found, "en" first
var names: []string = null
var paths: []string = null
var fonts: []string = null # a font atlas directory, "" for the game's own
var font_now: string = ""
var exact: Dict = null # msgid -> index into out
var out: []string = null
var lits: []string = null # every pattern's literal pieces, one run per pattern
var pat_out: []string = null
var pat_l0: words = null # where a pattern's pieces start in lits
var pat_nl: words = null # how many pieces (holes + 1)
var pat_sc: words = null # bytes of English it pins down: more is a better match
var pat_ph: words = null # by piece: the hole number that follows it
var pat_n: int = 0
var pfx: Dict = null # a pattern's first four bytes -> its chain head + 1
var sfx: Dict = null # else its last four
var gen_head: int = 0 # else the short list, tried on short strings only
var nx_p: words = null
var nx_s: words = null
var nx_g: words = null
var cache: Dict = null # English as drawn -> index into cache_out
var cache_out: []string = null
var caps: []string = null
var hit: bool = false
var plural: Dict = null # a plural msgid (the singular) -> index into forms / 3
var forms: []string = null # three forms per entry, "" where the file has fewer
var plural_rule: int = 0 # PL_*
const CACHE_MAX: int = 4000
const CAPS: int = 16
export function i18n_on() -> bool { return on }
export function i18n_current() -> string { return cur }
export function i18n_font_now() -> string { return font_now }
export function i18n_exact_count() -> int {
if out == null { return 0 }
return len(out)
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_font_now(i18n_st: I18nState) -> string { return i18n_st.font_now }
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() -> int { return pat_n }
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(path: string) -> void { font_now = path }
export function i18n_font_base(i18n_st: mut I18nState, path: string) -> void { i18n_st.font_now = path }

View file

@ -4,18 +4,20 @@
import "ludic.i18n"
program I18nTest {
numbers float
var forced_code: string = ""
var fonts_set: []string = null
var font_ok: bool = true
state I18ntestState {
forced_code: string = ""
fonts_set: []string = null
font_ok: bool = true
}
function dir() -> string { return Os.temp_dir() + "/ludic-i18n-test" }
function mods() -> string { return dir() + "/mods" }
function t_mods() -> string { return mods() }
function t_forced() -> string { return forced_code }
function t_forced(i18ntest_st: I18ntestState) -> string { return i18ntest_st.forced_code }
function t_ready() -> bool { return true }
function t_font(d: string) -> bool {
push(fonts_set, d)
return font_ok
function t_font(i18ntest_st: mut I18ntestState, d: string) -> bool {
push(i18ntest_st.fonts_set, d)
return i18ntest_st.font_ok
}
bind I18nWorld { mod_dir: fn t_mods, forced: fn t_forced, font_ready: fn t_ready, set_font: fn t_font }
@ -36,116 +38,116 @@ program I18nTest {
return s
}
function fresh() -> void {
forced_code = ""
fonts_set = new []string
font_ok = true
function fresh(i18n_st: mut I18nState, i18ntest_st: mut I18ntestState) -> void {
i18ntest_st.forced_code = ""
i18ntest_st.fonts_set = new []string
i18ntest_st.font_ok = true
Fs.mkdir(dir())
Fs.mkdir(mods())
Fs.write_text(dir() + "/languages.txt", "# shipped\ntt\nnope\n")
Fs.write_text(dir() + "/tt.po", tt())
Fs.remove(mods() + "/zz.po")
i18n_config(dir(), "font_default")
i18n_init()
i18n_config(i18n_st, dir(), "font_default")
i18n_init(i18n_st)
}
test "the shipped list and English are found, a missing file is not" {
fresh()
expect_eq(i18n_count(), 2)
expect(i18n_code_at(0) == "en")
expect_eq(i18n_index("tt"), 1)
expect_eq(i18n_index("nope"), 0)
expect(i18n_name_at(1) == "Test Tongue")
expect(i18n_font_at(1) == dir() + "/font_tt")
expect(L("Settings") == "Settings")
test "the shipped list and English are found, a missing file is not" (i18n_st: mut I18nState, i18ntest_st: mut I18ntestState) {
fresh(i18n_st, i18ntest_st)
expect_eq(i18n_count(i18n_st), 2)
expect(i18n_code_at(i18n_st, 0) == "en")
expect_eq(i18n_index(i18n_st, "tt"), 1)
expect_eq(i18n_index(i18n_st, "nope"), 0)
expect(i18n_name_at(i18n_st, 1) == "Test Tongue")
expect(i18n_font_at(i18n_st, 1) == dir() + "/font_tt")
expect(L(i18n_st, "Settings") == "Settings")
}
test "exact lines, patterns with translated holes, and English where unknown" {
fresh()
i18n_use("tt")
expect(i18n_on())
expect(i18n_current() == "tt")
expect(L("Settings") == "Zettings")
expect(L("Day 12") == "12. Gun")
expect(L("+2 Plant fiber") == "Lif +2")
expect(L("You saw a deer.") == "geyik gordun.")
expect(L("zzqx words nobody wrote") == "zzqx words nobody wrote")
expect(L("Multiline") == "Coksatir\n")
expect_eq(i18n_exact_count(), 6)
expect_eq(i18n_pattern_count(), 3)
test "exact lines, patterns with translated holes, and English where unknown" (i18n_st: mut I18nState, i18ntest_st: mut I18ntestState) {
fresh(i18n_st, i18ntest_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)
}
test "a paragraph a sentence at a time, and a layout's padding kept" {
fresh()
i18n_use("tt")
expect(L("You need to eat. zzqx words nobody wrote.") == "Yemek lazim. zzqx words nobody wrote.")
expect(L("zzqx. zzqy.") == "zzqx. zzqy.")
expect(L(" owned") == " senin")
expect(L(" zzqx") == " zzqx")
test "a paragraph a sentence at a time, and a layout's padding kept" (i18n_st: mut I18nState, i18ntest_st: mut I18ntestState) {
fresh(i18n_st, i18ntest_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" {
fresh()
expect(Ln("{1} trout", "{1} trouts", 1) == "1 trout")
expect(Ln("{1} trout", "{1} trouts", 3) == "3 trouts")
i18n_use("tt")
expect_eq(i18n_plural_rule(), PL_SLAVIC)
expect(Ln("{1} trout", "{1} trouts", 21) == "21 alabalik")
expect(Ln("{1} trout", "{1} trouts", 5) == "5 alabaliklar")
expect(Ln("{1} fish", "{1} fishes", 2) == "2 fishes")
test "plurals by the file's rule, and English by one and many" (i18n_st: mut I18nState, i18ntest_st: mut I18ntestState) {
fresh(i18n_st, i18ntest_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_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)
expect_eq(i18n_plural_form(PL_ONE_FORM, 7), 0)
}
test "the font follows the language, and falls back when it will not load" {
fresh()
i18n_font_base("font_default")
i18n_use("tt")
expect_eq(len(fonts_set), 1)
expect(i18n_font_now() == dir() + "/font_tt")
i18n_use("en")
expect(i18n_font_now() == "font_default")
font_ok = false
i18n_use("tt")
expect(i18n_font_now() == "font_default")
test "the font follows the language, and falls back when it will not load" (i18n_st: mut I18nState, i18ntest_st: mut I18ntestState) {
fresh(i18n_st, i18ntest_st)
i18n_font_base(i18n_st, "font_default")
i18n_use(i18n_st, "tt")
expect_eq(len(i18ntest_st.fonts_set), 1)
expect(i18n_font_now(i18n_st) == dir() + "/font_tt")
i18n_use(i18n_st, "en")
expect(i18n_font_now(i18n_st) == "font_default")
i18ntest_st.font_ok = false
i18n_use(i18n_st, "tt")
expect(i18n_font_now(i18n_st) == "font_default")
}
test "a mod in the player's folder is found, replaces a shipped code, and goes when removed" {
fresh()
test "a mod in the player's folder is found, replaces a shipped code, and goes when removed" (i18n_st: mut I18nState, i18ntest_st: mut I18ntestState) {
fresh(i18n_st, i18ntest_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"))
i18n_init()
expect_eq(i18n_count(), 3)
expect(i18n_name_at(i18n_index("zz")) == "Mod Tongue")
i18n_use("zz")
expect(L("Settings") == "Mettings")
expect(L("Day 3") == "Day 3")
i18n_use("tt")
expect(L("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, "Day 3") == "Day 3")
i18n_use(i18n_st, "tt")
expect(L(i18n_st, "Settings") == "Z2")
Fs.remove(mods() + "/zz.po")
Fs.remove(mods() + "/tt.po")
i18n_init()
expect_eq(i18n_index("zz"), 0)
i18n_use("en")
expect(not i18n_on())
expect(L("Settings") == "Settings")
i18n_init(i18n_st)
expect_eq(i18n_index(i18n_st, "zz"), 0)
i18n_use(i18n_st, "en")
expect(not i18n_on(i18n_st))
expect(L(i18n_st, "Settings") == "Settings")
}
test "a forced code wins over the one asked for" {
fresh()
forced_code = "tt"
i18n_use("en")
expect(i18n_current() == "tt")
test "a forced code wins over the one asked for" (i18n_st: mut I18nState, i18ntest_st: mut I18ntestState) {
fresh(i18n_st, i18ntest_st)
i18ntest_st.forced_code = "tt"
i18n_use(i18n_st, "en")
expect(i18n_current(i18n_st) == "tt")
}
test "a header field, and a .po loaded from text" {
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(po_line("Quit", "Cik"))
expect(L("Quit") == "Cik")
i18n_off()
expect(L("Quit") == "Quit")
i18n_load_text(i18n_st, po_line("Quit", "Cik"))
expect(L(i18n_st, "Quit") == "Cik")
i18n_off(i18n_st)
expect(L(i18n_st, "Quit") == "Quit")
}
}

View file

@ -1,26 +1,26 @@
# translate.ludic - what the game draws, in the language in use: exact, else a pattern, else the
# padding a layout put round it taken off, else a paragraph a sentence at a time; English otherwise
export function L(s: string) -> string {
if not on { return s }
let ci = Dict.get_or(cache, s, -1)
if ci >= 0 { return cache_out[ci] }
let r = whole(s)
if Dict.size(cache) >= CACHE_MAX {
Dict.clear(cache)
cache_out = new []string
export function L(i18n_st: mut I18nState, s: string) -> string {
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)
i18n_st.cache_out = new []string
}
Dict.set(cache, s, len(cache_out))
push(cache_out, r)
Dict.set(i18n_st.cache, s, len(i18n_st.cache_out))
push(i18n_st.cache_out, r)
return r
}
function whole(s: string) -> string {
function whole(i18n_st: mut I18nState, s: string) -> string {
let n = len(s)
if n == 0 { return s }
let e = Dict.get_or(exact, s, -1)
if e >= 0 { return out[e] }
let r = pattern(s, 0)
if hit { return r }
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
@ -29,15 +29,15 @@ function whole(s: string) -> string {
if a > 0 or b < n {
if b <= a { return s }
let core: string = s[a..b]
let t = whole(core)
let t = whole(i18n_st, core)
if t == core { return s }
return s[0..a] + t + s[b..n]
}
return sentences(s)
return sentences(i18n_st, s)
}
# a paragraph built from sentences, one at a time; English stays where a sentence is unknown
function sentences(s: string) -> string {
function sentences(i18n_st: mut I18nState, s: string) -> string {
let n = len(s)
var r = ""
var start = 0
@ -47,8 +47,8 @@ function sentences(s: string) -> string {
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(s[start..i + 1])
if hit { any = true }
let t = sentence(i18n_st, s[start..i + 1])
if i18n_st.hit { any = true }
r = r + t + " "
start = i + 2
pieces += 1
@ -58,18 +58,18 @@ function sentences(s: string) -> string {
i += 1
}
if pieces == 0 { return s }
let t = sentence(s[start..n])
if hit { any = true }
let t = sentence(i18n_st, s[start..n])
if i18n_st.hit { any = true }
if not any { return s }
return r + t
}
function sentence(p: string) -> string {
hit = false
let e = Dict.get_or(exact, p, -1)
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 {
hit = true
return out[e]
i18n_st.hit = true
return i18n_st.out[e]
}
return pattern(p, 0)
return pattern(i18n_st, p, 0)
}

View file

@ -1,43 +1,43 @@
# use.ludic - choosing a language: its file loaded into the lookups, and the font atlas it names
export function i18n_use(code: string) -> void {
if codes == null { i18n_init() }
on = false
cur = "en"
var k = i18n_index(code)
export function i18n_use(i18n_st: mut I18nState, code: string) -> void {
if i18n_st.codes == null { i18n_init(i18n_st) }
i18n_st.on = false
i18n_st.cur = "en"
var k = i18n_index(i18n_st, code)
let forced = I18nWorld.forced()
if len(forced) > 0 { k = i18n_index(forced) }
if len(forced) > 0 { k = i18n_index(i18n_st, forced) }
var font = ""
if k > 0 {
let text = Fs.read_text(paths[k])
let text = Fs.read_text(i18n_st.paths[k])
if text != null {
i18n_load_text(text)
on = true
cur = codes[k]
font = fonts[k]
i18n_load_text(i18n_st, text)
i18n_st.on = true
i18n_st.cur = i18n_st.codes[k]
font = i18n_st.fonts[k]
}
}
# the atlas follows the language, and is only reloaded when it actually changes
if len(font) == 0 { font = font_default }
if I18nWorld.font_ready() and not (font == font_now) {
if I18nWorld.set_font(font) { font_now = font } else if not (font_now == font_default) {
I18nWorld.set_font(font_default)
font_now = font_default
if len(font) == 0 { font = i18n_st.font_default }
if I18nWorld.font_ready() and not (font == i18n_st.font_now) {
if I18nWorld.set_font(font) { i18n_st.font_now = font } else if not (i18n_st.font_now == i18n_st.font_default) {
I18nWorld.set_font(i18n_st.font_default)
i18n_st.font_now = i18n_st.font_default
}
}
}
# a .po's text as the lookups, whatever the language is called (a test, a file from elsewhere)
export function i18n_load_text(text: string) -> void {
export function i18n_load_text(i18n_st: mut I18nState, text: string) -> void {
let es = i18n_parse(text)
exact = Dict.new()
out = new []string
cache = Dict.new()
cache_out = new []string
caps = new []string
for c in 0 .. CAPS { push(caps, "") }
plural = Dict.new()
forms = new []string
plural_rule = plural_rule_of(i18n_header(text, "Plural-Forms"))
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) {
@ -48,33 +48,33 @@ export function i18n_load_text(text: string) -> void {
nlit += h + 1
}
}
pat_out = new []string
lits = new []string
pat_l0 = words(np + 1)
pat_nl = words(np + 1)
pat_sc = words(np + 1)
pat_ph = words(nlit + 1)
nx_p = words(np + 1)
nx_s = words(np + 1)
nx_g = words(np + 1)
pfx = Dict.new()
sfx = Dict.new()
gen_head = 0
pat_n = 0
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(e) } else if not (blank(e.id) or blank(e.str)) {
if holes(e.id) > 0 { add_pattern(e.id, e.str) } else {
Dict.set(exact, e.id, len(out))
push(out, 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))
push(i18n_st.out, e.str)
}
}
}
on = true
i18n_st.on = true
}
# English again: nothing loaded, nothing translated
export function i18n_off() -> void {
on = false
cur = "en"
export function i18n_off(i18n_st: mut I18nState) -> void {
i18n_st.on = false
i18n_st.cur = "en"
}