From bdda22cb2e0311badc4f0b82bee651bad96ba4aa Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Wed, 30 Sep 2026 00:32:23 +0300 Subject: [PATCH] ludic.i18n: a key with holes inside another key's hole survives - an argument's own 31s and 27s are escaped with 27 and the decoder splits on the unescaped ones (trf(k"a", trf(k"b", x))); keys_test holds it Co-Authored-By: Claude Opus 5.5 --- packages/ludic.i18n/index.ludic | 1 + packages/ludic.i18n/keyed_args.ludic | 37 +++++++++++++++++++++++ packages/ludic.i18n/keyed_render.ludic | 28 +++++------------ packages/ludic.i18n/keys.ludic | 35 ++++++++++++++++----- packages/ludic.i18n/tests/keys_test.ludic | 6 ++-- 5 files changed, 77 insertions(+), 30 deletions(-) create mode 100644 packages/ludic.i18n/keyed_args.ludic diff --git a/packages/ludic.i18n/index.ludic b/packages/ludic.i18n/index.ludic index cba83ae4..6466e3bc 100644 --- a/packages/ludic.i18n/index.ludic +++ b/packages/ludic.i18n/index.ludic @@ -14,4 +14,5 @@ import "translate.ludic" import "keys.ludic" import "keyed.ludic" import "keyed_render.ludic" +import "keyed_args.ludic" import "plural.ludic" diff --git a/packages/ludic.i18n/keyed_args.ludic b/packages/ludic.i18n/keyed_args.ludic new file mode 100644 index 00000000..6de5a29c --- /dev/null +++ b/packages/ludic.i18n/keyed_args.ludic @@ -0,0 +1,37 @@ +# keyed_args.ludic - a key's arguments as the decoder reads them: unescaped, looked up while still English, a count + +# an argument as it was handed in: each 27 before a 27 or a 31 taken off +@alloc_ok("a key made into text (a cache miss): an argument that held a key with holes") +function kr_unescape(s: string) -> string { + var esc = false + for i in 0 .. len(s) { if s[i] == I18N_ESC { esc = true } } + if not esc { return s } + let buf = buffer(len(s)) + var n = 0 + var i = 0 + while i < len(s) { + if s[i] == I18N_ESC and i + 1 < len(s) { i += 1 } + buf[n] = s[i] + n += 1 + i += 1 + } + 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 + var neg = false + for i in 0 .. len(s) { + if i == 0 and s[i] == 45 { neg = true } else if s[i] >= 48 and s[i] <= 57 { v = v * 10 + (s[i] - 48) } + } + if neg { return -v } + return v +} diff --git a/packages/ludic.i18n/keyed_render.ludic b/packages/ludic.i18n/keyed_render.ludic index 7795d1ad..aeb8e7b9 100644 --- a/packages/ludic.i18n/keyed_render.ludic +++ b/packages/ludic.i18n/keyed_render.ludic @@ -28,8 +28,14 @@ function kr_make(i18n_st: mut I18nState, s: string) -> string { var start = 1 var i = 1 while i <= len(s) { - if i == len(s) or s[i] == I18N_SEP { - push(args, s[start..i]) + if i < len(s) and s[i] == I18N_ESC { + i += 2 # an escaped byte belongs to its argument + continue + } + if i >= len(s) or s[i] == I18N_SEP { + var e = i + if e > len(s) { e = len(s) } + push(args, kr_unescape(s[start..e])) start = i + 1 } i += 1 @@ -61,21 +67,3 @@ function kr_fill(i18n_st: mut I18nState, t: string, args: []string) -> string { } return r + t[seg..len(t)] } - -# 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 - var neg = false - for i in 0 .. len(s) { - if i == 0 and s[i] == 45 { neg = true } else if s[i] >= 48 and s[i] <= 57 { v = v * 10 + (s[i] - 48) } - } - if neg { return -v } - return v -} diff --git a/packages/ludic.i18n/keys.ludic b/packages/ludic.i18n/keys.ludic index 6c4af390..f0a3635e 100644 --- a/packages/ludic.i18n/keys.ludic +++ b/packages/ludic.i18n/keys.ludic @@ -4,6 +4,7 @@ export const I18N_KEY: int = 1 # k"pause.resume" is "\\x01pause.resume" export const I18N_PLURAL: int = 2 # kn"catch.count": a count chooses the form export const I18N_SEP: int = 31 # between the key and each argument +export const I18N_ESC: int = 27 # inside an argument, before a 27 or a 31 of its own (a key with its own holes) # is this a key (or a key with its arguments), rather than English export function i18n_is_key(s: string) -> bool { return len(s) > 0 and (s[0] == I18N_KEY or s[0] == I18N_PLURAL) } @@ -43,23 +44,41 @@ function kx_count(a: string, b: string, c: string, d: string) -> int { # the marker, the name, then each of the first `n` arguments after a byte 31 function kx_join(mark: int, name: string, a: string, b: string, c: string, d: string, n: int) -> string { var size = 1 + len(name) - if n > 0 { size += 1 + len(a) } - if n > 1 { size += 1 + len(b) } - if n > 2 { size += 1 + len(c) } - if n > 3 { size += 1 + len(d) } + if n > 0 { size += 1 + kx_len(a) } + if n > 1 { size += 1 + kx_len(b) } + if n > 2 { size += 1 + kx_len(c) } + if n > 3 { size += 1 + kx_len(d) } let buf = buffer(size) buf[0] = mark var at = kx_put(buf, 1, name) - if n > 0 { at = kx_put(buf, kx_sep(buf, at), a) } - if n > 1 { at = kx_put(buf, kx_sep(buf, at), b) } - if n > 2 { at = kx_put(buf, kx_sep(buf, at), c) } - if n > 3 { at = kx_put(buf, kx_sep(buf, at), d) } + if n > 0 { at = kx_arg(buf, kx_sep(buf, at), a) } + if n > 1 { at = kx_arg(buf, kx_sep(buf, at), b) } + if n > 2 { at = kx_arg(buf, kx_sep(buf, at), c) } + if n > 3 { at = kx_arg(buf, kx_sep(buf, at), d) } return text_of(buf, size) } function kx_sep(buf: []byte, at: int) -> int { buf[at] = I18N_SEP return at + 1 } +# an argument's bytes, a 27 before each 27 or 31 of its own, so a key with holes can be one +function kx_len(s: string) -> int { + var n = len(s) + for i in 0 .. len(s) { if s[i] == I18N_SEP or s[i] == I18N_ESC { n += 1 } } + return n +} +function kx_arg(buf: []byte, at: int, s: string) -> int { + var j = at + for i in 0 .. len(s) { + if s[i] == I18N_SEP or s[i] == I18N_ESC { + buf[j] = I18N_ESC + j += 1 + } + buf[j] = s[i] + j += 1 + } + return j +} function kx_put(buf: []byte, at: int, s: string) -> int { for i in 0 .. len(s) { buf[at + i] = s[i] } return at + len(s) diff --git a/packages/ludic.i18n/tests/keys_test.ludic b/packages/ludic.i18n/tests/keys_test.ludic index 6a69995a..851898eb 100644 --- a/packages/ludic.i18n/tests/keys_test.ludic +++ b/packages/ludic.i18n/tests/keys_test.ludic @@ -53,6 +53,8 @@ program I18nKeysTest { expect(L(i18n_st, trf(k"hud.day", "12", "", "", "")) == "12. Gun") expect(L(i18n_st, saw) == "5:30 saatinde bir geyik gordun.") expect(L(i18n_st, trf(k"hud.day", "", "", "", "")) == ". Gun") # a missing argument is empty + let nested = trf(k"catch.saw", trf(k"hud.day", "3", "", "", ""), "5:30", "", "") # a key with holes inside a hole + expect(L(i18n_st, nested) == "5:30 saatinde 3. Gun gordun.") expect(key_text(saw) == "catch.saw" and key_text("Settings") == "Settings") } @@ -76,11 +78,11 @@ program I18nKeysTest { } 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) + 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 + expect(L(i18n_st, trf(k"catch.saw", "Nowhere known", "7", "", "")) == "7 saatinde Nowhere known gordun.") # no line: as it is } }