diff --git a/packages/ludic.i18n/index.ludic b/packages/ludic.i18n/index.ludic index b4ab8e80..dda50feb 100644 --- a/packages/ludic.i18n/index.ludic +++ b/packages/ludic.i18n/index.ludic @@ -16,4 +16,5 @@ import "keyed.ludic" import "keyed_render.ludic" import "keyed_args.ludic" import "mixed.ludic" +import "nest.ludic" import "plural.ludic" diff --git a/packages/ludic.i18n/keyed_render.ludic b/packages/ludic.i18n/keyed_render.ludic index cda9f5ab..d62131bf 100644 --- a/packages/ludic.i18n/keyed_render.ludic +++ b/packages/ludic.i18n/keyed_render.ludic @@ -27,12 +27,15 @@ function kr_make(i18n_st: mut I18nState, s: string) -> string { let args = new []string var start = 1 var i = 1 + var depth = 0 while i <= len(s) { 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 { + if i < len(s) and s[i] == I18N_OPEN { depth += 1 } # a bracketed line's own 31s are its own + if i < len(s) and s[i] == I18N_CLOSE and depth > 0 { depth -= 1 } + if i >= len(s) or (s[i] == I18N_SEP and depth == 0) { var e = i if e > len(s) { e = len(s) } push(args, kr_unescape(s[start..e])) @@ -57,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) { 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) } 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/nest.ludic b/packages/ludic.i18n/nest.ludic new file mode 100644 index 00000000..440fed1d --- /dev/null +++ b/packages/ludic.i18n/nest.ludic @@ -0,0 +1,77 @@ +# nest.ludic - a line bracketed inside another (bytes 29 .. 30 round it: a game's txt_key / txt_line, a +# key's line in another's hole or glued to words): each bracket made into its text first, outermost in, then +# the whole. A line wholly inside one bracket is made already, and not looked up again as English; +# a key whole takes this path only through its arguments, which keyed() has unescaped by then +export const I18N_OPEN: int = 29 +export const I18N_CLOSE: int = 30 + +export function kx_nested(s: string) -> bool { + let p: string = s + for i in 0 .. len(p) { if p[i] == I18N_OPEN or p[i] == I18N_CLOSE { 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_nest_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] } + let q: string = s + var r = kx_flat(i18n_st, s) + if not (q[0] == I18N_OPEN and q[len(q) - 1] == I18N_CLOSE) { r = L(i18n_st, r) } + if Dict.size(i18n_st.kc) >= CACHE_MAX { kc_clear(i18n_st) } + @alloc_ok("a line with a line inside it, 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 line inside it: as above") + push(i18n_st.kc_out, r) + return r +} + +# each outermost bracket made into its text (what is inside is made the same way, by L), then any +# bracket left without a partner taken out +@alloc_ok("a line with a line inside it (a cache miss): once per distinct line") +function kx_flat(i18n_st: mut I18nState, s: string) -> string { + let p: string = s + var r = "" + var seg = 0 + var i = 0 + while i < len(p) { + let c = kx_close(p, i) + if p[i] == I18N_OPEN and c > i { + r = r + s[seg..i] + L(i18n_st, s[i + 1..c]) + seg = c + 1 + i = c + 1 + } else { + i += 1 + } + } + return kx_strip(r + s[seg..len(p)]) +} + +# where the bracket opening at i closes, or -1 when it never does +function kx_close(p: string, i: int) -> int { + var depth = 0 + for j in i .. len(p) { + if p[j] == I18N_OPEN { depth += 1 } + if p[j] == I18N_CLOSE { + depth -= 1 + if depth == 0 { return j } + } + } + return -1 +} + +# a close with no open before it (or an open with no close): no stray byte drawn +@alloc_ok("as above") +function kx_strip(s: string) -> string { + let p: string = s + var r = "" + var seg = 0 + for i in 0 .. len(p) { + if p[i] == I18N_OPEN or p[i] == I18N_CLOSE { + r = r + s[seg..i] + seg = i + 1 + } + } + return r + s[seg..len(p)] +} diff --git a/packages/ludic.i18n/tests/nest_test.ludic b/packages/ludic.i18n/tests/nest_test.ludic new file mode 100644 index 00000000..1be2f917 --- /dev/null +++ b/packages/ludic.i18n/tests/nest_test.ludic @@ -0,0 +1,42 @@ +# nest_test.ludic - a line bracketed inside another (bytes 29 .. 30) is made first, innermost out, in the +# language in use; a line wholly in one bracket is not looked up again, and a stray bracket draws nothing +import "ludic.i18n" +program I18nNestTest { + numbers float + function q(s: string) -> string { return "\"" + s + "\"" } + function po_line(id: string, tr: string) -> string { return "msgid " + q(id) + "\nmsgstr " + q(tr) + "\n\n" } + function en() -> string { return po_line("hud.day", "Day {1}") + po_line("pause.quit", "Quit") + po_line("shop.ada", "Ada: {1}") } + function tt() -> string { return po_line("hud.day", "{1}. Gun") + po_line("pause.quit", "Cik") + po_line("Settings", "Ayarlar") } + function b(s: string) -> string { + let o = buffer(1) + o[0] = I18N_OPEN + let c = buffer(1) + c[0] = I18N_CLOSE + return text_of(o, 1) + s + text_of(c, 1) + } + function start(i18n_st: mut I18nState) -> void { + i18n_off(i18n_st) + i18n_loud(i18n_st, false) + i18n_base_text(i18n_st, en()) + } + + test "a bracketed line is made first, and a line wholly in one bracket is its text" (i18n_st: mut I18nState) { + start(i18n_st) + let day = b(trf(k"hud.day", "3", "", "", "")) + expect(L(i18n_st, day) == "Day 3") + expect(L(i18n_st, "[E] " + day) == "[E] Day 3") + 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") + } + + test "a bracket with no partner draws nothing of itself" (i18n_st: mut I18nState) { + start(i18n_st) + let c = buffer(1) + c[0] = I18N_CLOSE + expect(L(i18n_st, "Quit" + text_of(c, 1)) == "Quit") + expect(kx_nested("Quit") == false) + } +} diff --git a/packages/ludic.i18n/translate.ludic b/packages/ludic.i18n/translate.ludic index fe946450..6ea88c52 100644 --- a/packages/ludic.i18n/translate.ludic +++ b/packages/ludic.i18n/translate.ludic @@ -1,8 +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 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) } + 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] }