ludic.i18n: a language's lookups kept once read; switching back restores them

i18n_use re-read and re-parsed the .po on every change of language (ev_SettingsChanged, SetLang)
and dropped the old tables for good: the play of 22 minutes left lines_of 650 KB, fs_read_text
409 KB, PoEntry 113 KB and unquote 273 KB unreachable, and three of its @alloc_ok sites were
declared but unbounded. Each language's tables are now filed by index once built (tables.ludic,
I18nTables) and put back by reference; a new probe (i18n_init) or a different file for a code
(lang_add) reads it again.

Test: a language switched back to is its kept tables, not its file read again; a new probe reads
it (9 tests pass).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-29 12:21:53 +03:00
parent 28ad6f62ea
commit 1667d7f16e
6 changed files with 112 additions and 1 deletions

View file

@ -3,6 +3,7 @@ module ludic_i18n uses
numbers float
import "ports.ludic"
import "state.ludic"
import "tables.ludic"
import "bytes.ludic"
import "po.ludic"
import "langs.ludic"

View file

@ -6,6 +6,7 @@ export function i18n_init(i18n_st: mut I18nState) -> void {
i18n_st.names = new []string
i18n_st.paths = new []string
i18n_st.fonts = new []string
i18n_st.tabs = new []I18nTables
lang_add(i18n_st, "en", "English", "", "")
let idx = Fs.read_text(i18n_st.lang_dir + "/languages.txt")
if idx != null {
@ -37,6 +38,7 @@ function lang_add(i18n_st: mut I18nState, code: string, name: string, path: stri
i18n_st.names[i] = name
i18n_st.paths[i] = path
i18n_st.fonts[i] = font
if tables_have(i18n_st, i) { i18n_st.tabs[i] = null } # another file for it: read that one next time
return
}
}

View file

@ -44,6 +44,7 @@ export state I18nState {
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_*
tabs: []I18nTables = null # by language: its lookups once read, kept for the next switch
}
# where the shipped languages are (a directory with languages.txt and <code>.po), and the atlas a

View file

@ -0,0 +1,88 @@
# tables.ludic - a language's lookups, kept once read: switching back to it is a copy of references,
# not the .po parsed again (every change of language once dropped the old tables for good)
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
}
@alloc_ok("once per language: its lookups, kept")
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
return t
}
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
}
function tables_have(i18n_st: I18nState, k: int) -> bool {
if i18n_st.tabs == null or k >= len(i18n_st.tabs) { return false }
return i18n_st.tabs[k] != null
}
@alloc_ok("once per language: the slot its lookups are kept in")
function tables_file(i18n_st: mut I18nState, k: int) -> void {
if i18n_st.tabs == null { i18n_st.tabs = new []I18nTables }
while len(i18n_st.tabs) <= k { push(i18n_st.tabs, null) }
i18n_st.tabs[k] = tables_keep(i18n_st)
}

View file

@ -114,6 +114,19 @@ program I18nTest {
expect(i18n_font_now(i18n_st) == "font_default")
}
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"))
i18n_use(i18n_st, "en")
i18n_use(i18n_st, "tt")
expect(L(i18n_st, "Settings") == first)
i18n_init(i18n_st)
i18n_use(i18n_st, "tt")
expect(L(i18n_st, "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"))

View file

@ -7,10 +7,16 @@ export function i18n_use(i18n_st: mut I18nState, code: string) -> void {
let forced = I18nWorld.forced()
if len(forced) > 0 { k = i18n_index(i18n_st, forced) }
var font = ""
if k > 0 {
if k > 0 and tables_have(i18n_st, k) {
tables_put(i18n_st, i18n_st.tabs[k])
i18n_st.on = true
i18n_st.cur = i18n_st.codes[k]
font = i18n_st.fonts[k]
} else if k > 0 {
let text = Fs.read_text(i18n_st.paths[k])
if text != null {
i18n_load_text(i18n_st, text)
tables_file(i18n_st, k)
i18n_st.on = true
i18n_st.cur = i18n_st.codes[k]
font = i18n_st.fonts[k]