From a3377215e9493c1e2fab42db7d9ceb7e113bcbef Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Wed, 30 Sep 2026 00:48:29 +0300 Subject: [PATCH 1/3] ludic.i18n: tr and key_text lose their @alloc_ok - string(k) of a Key is no allocation to the escape analysis now, and key_text's slice is not one either, so both pass arena strict as they stand (trf / trn keep theirs: they make a line) Co-Authored-By: Claude Opus 5.5 --- packages/ludic.i18n/keys.ludic | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/ludic.i18n/keys.ludic b/packages/ludic.i18n/keys.ludic index e1191d82..5d9a9b38 100644 --- a/packages/ludic.i18n/keys.ludic +++ b/packages/ludic.i18n/keys.ludic @@ -16,7 +16,6 @@ export function i18n_key(name: string) -> string { return kx_join(I18N_KEY, name export function i18n_key_plural(name: string) -> string { return kx_join(I18N_PLURAL, name, "", "", "", "", 0) } # the key's name without its marker ("pause.resume"), or the string as it is -@alloc_ok("a key's name: a slice of text already made, kept by whoever holds the key") export function key_text(k: string) -> string { if not i18n_is_key(k) { return k } var e = 1 @@ -25,7 +24,6 @@ export function key_text(k: string) -> string { } # the text of a key: drawn translated where it is drawn (its marked text, a free cast) -@alloc_ok("a cast: the key's own marked text, nothing is made") export function tr(k: Key) -> string { return string(k) } # a key's text with its holes, {1} .. {4}, filled by strings already made (a number, a name, another key) @alloc_ok("a line with its holes, made when something happens and kept by what shows it") From 96c6e27d983ce909acfa6cec42b3ec45910d1305 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Wed, 30 Sep 2026 00:50:53 +0300 Subject: [PATCH 2/3] ludic.i18n: a key's text glued into an English line is made where it sits - L decodes each marked name inside a line that is not a key of its own ("A {1}, {2} cm." given a name that is a key, name + "\n" + blurb), in the language in use, then the line takes its old English lookup; kept in the keyed cache. Interim until every such line is a key (26.9). keys_test covers the stop at a sentence's full stop, a key at the head with words after, a mixed argument in a key's hole, and a key whole left alone. Since the data's text became keys (26.4), a game's English patterns that take a table's name (Notify, txt_pat, a concatenation) drew the raw mark and key. Co-Authored-By: Claude Opus 5.5 --- packages/ludic.i18n/index.ludic | 1 + packages/ludic.i18n/keyed_render.ludic | 2 +- packages/ludic.i18n/mixed.ludic | 58 +++++++++++++++++++++++ packages/ludic.i18n/tests/keys_test.ludic | 11 +++++ packages/ludic.i18n/translate.ludic | 1 + 5 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 packages/ludic.i18n/mixed.ludic diff --git a/packages/ludic.i18n/index.ludic b/packages/ludic.i18n/index.ludic index 6466e3bc..b4ab8e80 100644 --- a/packages/ludic.i18n/index.ludic +++ b/packages/ludic.i18n/index.ludic @@ -15,4 +15,5 @@ import "keys.ludic" import "keyed.ludic" import "keyed_render.ludic" import "keyed_args.ludic" +import "mixed.ludic" import "plural.ludic" diff --git a/packages/ludic.i18n/keyed_render.ludic b/packages/ludic.i18n/keyed_render.ludic index aeb8e7b9..cda9f5ab 100644 --- a/packages/ludic.i18n/keyed_render.ludic +++ b/packages/ludic.i18n/keyed_render.ludic @@ -57,7 +57,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) { v = keyed(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) { v = L(i18n_st, v) } else if i18n_st.on and kr_words(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/mixed.ludic b/packages/ludic.i18n/mixed.ludic new file mode 100644 index 00000000..13bbeb49 --- /dev/null +++ b/packages/ludic.i18n/mixed.ludic @@ -0,0 +1,58 @@ +# 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, +# 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: its alphabet, and a full stop at its end left to the sentence +function kx_name_byte(c: int) -> bool { return (c >= 97 and c <= 122) or (c >= 65 and c <= 90) or (c >= 48 and c <= 57) or c == 95 or c == 46 or c == 45 } + +function kx_name_end(s: string, i: int) -> int { + var e = i + while e < len(s) and kx_name_byte(s[e]) { e += 1 } + while e > i and s[e - 1] == 46 { e -= 1 } + return e +} + +# a string with a mark that is not the whole of it: a mark past its start, or words after a key's name +export function kx_mixed(s: string) -> bool { + let n = len(s) + if n == 0 { return false } + if i18n_is_key(s) { + let e = kx_name_end(s, 1) + return e < n and s[e] != I18N_SEP + } + for i in 1 .. n { if s[i] == I18N_KEY or s[i] == I18N_PLURAL { return true } } + return false +} + +# what L does with such a line: made once per distinct line in this language, kept with the keys +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) } + 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)) + @alloc_ok("a line with a key glued in: as above") + push(i18n_st.kc_out, r) + return r +} + +@alloc_ok("a line with a key glued in (a cache miss): once per distinct line") +function kx_unmix(i18n_st: mut I18nState, s: string) -> string { + var r = "" + var start = 0 + var i = 0 + while i < len(s) { + if s[i] == I18N_KEY or s[i] == I18N_PLURAL { + let e = kx_name_end(s, i + 1) + r = r + s[start..i] + keyed(i18n_st, s[i..e]) + start = e + i = e + } else { + i += 1 + } + } + return r + s[start..len(s)] +} diff --git a/packages/ludic.i18n/tests/keys_test.ludic b/packages/ludic.i18n/tests/keys_test.ludic index 851898eb..5ddbf7da 100644 --- a/packages/ludic.i18n/tests/keys_test.ludic +++ b/packages/ludic.i18n/tests/keys_test.ludic @@ -85,4 +85,15 @@ program I18nKeysTest { 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") + expect(L(i18n_st, "Saw " + elk + ", 30 cm.") == "Saw an elk, 30 cm." and L(i18n_st, "Saw " + elk + ".") == "Saw an elk.") + expect(L(i18n_st, elk + ": 30 cm") == "an elk: 30 cm" and L(i18n_st, elk + "\n" + tr(k"pause.quit")) == "an elk\nQuit") + 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, trf(k"catch.saw", elk, "5:30", "", "")) == "5:30 saatinde bir geyik gordun.") # a key whole is untouched + } } diff --git a/packages/ludic.i18n/translate.ludic b/packages/ludic.i18n/translate.ludic index e5a2d70c..fe946450 100644 --- a/packages/ludic.i18n/translate.ludic +++ b/packages/ludic.i18n/translate.ludic @@ -1,6 +1,7 @@ # 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 export function L(i18n_st: mut I18nState, s: string) -> string { + if kx_mixed(s) { return kx_line(i18n_st, s) } if i18n_is_key(s) { return keyed(i18n_st, s) } if not i18n_st.on { return s } let ci = Dict.get_or(i18n_st.cache, s, -1) From d703d79feb0af0d11faf75070ec84c3e5617a4ee Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Wed, 30 Sep 2026 00:52:49 +0300 Subject: [PATCH 3/3] ludic.i18n: a glued key's name is a-z, 0-9, _ and . only (every key en.po holds), so a word glued on after it is not read as part of it; keys_test: "Not owned. " + a key, and a capital straight after one Co-Authored-By: Claude Opus 5.5 --- packages/ludic.i18n/mixed.ludic | 5 +++-- packages/ludic.i18n/tests/keys_test.ludic | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/ludic.i18n/mixed.ludic b/packages/ludic.i18n/mixed.ludic index 13bbeb49..a1576e37 100644 --- a/packages/ludic.i18n/mixed.ludic +++ b/packages/ludic.i18n/mixed.ludic @@ -2,8 +2,9 @@ # name + "\n" + its blurb): each marked name made in the language in use, then the line as English. 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: its alphabet, and a full stop at its end left to the sentence -function kx_name_byte(c: int) -> bool { return (c >= 97 and c <= 122) or (c >= 65 and c <= 90) or (c >= 48 and c <= 57) or c == 95 or c == 46 or c == 45 } +# a key's name: a-z, 0-9, _ and . (every key the compiler derives or a game writes), a full stop at its +# end left to the sentence +function kx_name_byte(c: int) -> bool { return (c >= 97 and c <= 122) or (c >= 48 and c <= 57) or c == 95 or c == 46 } function kx_name_end(s: string, i: int) -> int { var e = i diff --git a/packages/ludic.i18n/tests/keys_test.ludic b/packages/ludic.i18n/tests/keys_test.ludic index 5ddbf7da..045692eb 100644 --- a/packages/ludic.i18n/tests/keys_test.ludic +++ b/packages/ludic.i18n/tests/keys_test.ludic @@ -91,6 +91,7 @@ program I18nKeysTest { let elk = tr(k"species.elk.a_name") expect(L(i18n_st, "Saw " + elk + ", 30 cm.") == "Saw an elk, 30 cm." and L(i18n_st, "Saw " + elk + ".") == "Saw an elk.") expect(L(i18n_st, elk + ": 30 cm") == "an elk: 30 cm" and L(i18n_st, elk + "\n" + tr(k"pause.quit")) == "an elk\nQuit") + 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