From 6bd32289906445429cd18c7df2982698afa9454d Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Thu, 24 Sep 2026 22:03:49 +0300 Subject: [PATCH] feat(ui): translate="no", and a title of several lines translated whole or line by line Co-Authored-By: Claude Opus 5.5 --- LANGUAGE.md | 3 ++ changes/defaults-views-templates.md | 2 ++ examples/library/ui_translate.ludic | 44 ++++++++++++++++++++++++++++ packages/ludic.ui/backend.ludic | 2 ++ packages/ludic.ui/elem.ludic | 4 +++ packages/ludic.ui/index.ludic | 1 + packages/ludic.ui/tooltip.ludic | 2 +- packages/ludic.ui/tooltip_text.ludic | 18 ++++++++++++ tools/ludic-cli/test.ludic | 1 + 9 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 examples/library/ui_translate.ludic create mode 100644 packages/ludic.ui/tooltip_text.ludic diff --git a/LANGUAGE.md b/LANGUAGE.md index 259442e0..e5577cd5 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -529,6 +529,9 @@ applies to both: `ui_translator(fn)`, `ui_sounds(fn)` and `ui_clock(fn)` hand the runtime the program's language, sounds and time. `import "ludic.ui/screen.ludic"` is the 2D screen's renderer. + Every text a template shows is translated, except inside `translate="no"` (and `translate="yes"` + inside that translates again). A `title` of several lines is translated whole when the translator + knows it whole, else line by line. - **The look is CSS.** - Colours: `#rgb`, `#rgba`, `#rrggbb`, `#rrggbbaa`, `rgb()`, `rgba()` and the basic names, usually through `var(--...)` from a theme. diff --git a/changes/defaults-views-templates.md b/changes/defaults-views-templates.md index 65594a31..c72f1a27 100644 --- a/changes/defaults-views-templates.md +++ b/changes/defaults-views-templates.md @@ -100,6 +100,8 @@ What a game's screens found missing, now in `ludic.ui`: `ui_scroll_set(id, px)` moves one once; - `linear-gradient(...)` backgrounds; `aspect-ratio`; `object-fit` for pictures (the renderer's `image_w` / `image_h`) and `ui_object_fit` for natives; +- `translate="no"` keeps an element's text as written; a title of several lines is translated whole, + else line by line; - a transition lands exactly on its end value (it had stopped a rounding error short of it, at every frame rate). diff --git a/examples/library/ui_translate.ludic b/examples/library/ui_translate.ludic new file mode 100644 index 00000000..2370a584 --- /dev/null +++ b/examples/library/ui_translate.ludic @@ -0,0 +1,44 @@ +# ui_translate.ludic — translation: translate="no" keeps an element's text (and everything inside it) +# as written, translate="yes" inside it translates again; a title of several lines is translated whole +# when the translator knows it whole, else line by line, and not inside translate="no". +import "ludic.ui" +program UiTranslate { + numbers float + var said: []string = new []string + var now: float = 0.0 + const PAGE: string = "

Hello

Hello

Hello

Hello

a

b

c

" + function tr(s: string) -> string { + if s == "Hello" { return "Merhaba" } + if s == "Save\\nQuit" { return "Kaydet ve cik" } + if s == "Line one" { return "Satir bir" } + if s == "Line two" { return "Satir iki" } + return s + } + function rec_text(x: float, y: float, s: string, size: float, c: int, a: float) -> void { push(said, s) } + function clock() -> float { return now } + function tip(root: UiNode, k: int) -> string { + let n = root.children[k] + let i = new UiInput + i.x = n.x + 1.0 + i.y = n.y + 1.0 + ui_input(i) + ui_show("t", null, 0.0, 0.0, 400.0, 400.0) + now = now + 1.0 + said = new []string + ui_input(i) + ui_show("t", null, 0.0, 0.0, 400.0, 400.0) + return `{said[len(said) - 2]}/{said[len(said) - 1]}` + } + entry { + let b = new UiBackend + b.text = fn rec_text + ui_backend(b) + ui_translator(fn tr) + ui_clock(fn clock) + ui_load_text(PAGE, "t.xml") + let root: UiNode = ui_nodes("t", null) + ui_place(root, 0.0, 0.0, 400.0, 400.0) + let texts = `{root.children[0].text} {root.children[1].text} {root.children[2].children[0].text} {root.children[2].children[1].text}` + print(`{texts} | {tip(root, 3)} | {tip(root, 4)} | {tip(root, 5)}`) + } +} diff --git a/packages/ludic.ui/backend.ludic b/packages/ludic.ui/backend.ludic index 9334f941..0ae11962 100644 --- a/packages/ludic.ui/backend.ludic +++ b/packages/ludic.ui/backend.ludic @@ -26,7 +26,9 @@ function ui_px(v: float) -> float { if ui_be != null and ui_be.scale != null { return v * ui_be.scale() } return v } +var tr_no: bool = false # inside translate="no" while a frame is built function ui_tr(s: string) -> string { + if tr_no { return s } if ui_be != null and ui_be.translate != null { return ui_be.translate(s) } return s } diff --git a/packages/ludic.ui/elem.ludic b/packages/ludic.ui/elem.ludic index 4d7120e2..e11b1094 100644 --- a/packages/ludic.ui/elem.ludic +++ b/packages/ludic.ui/elem.ludic @@ -24,10 +24,14 @@ function bd_elem(k: UiTpl, e: UiEnv, key: string) -> UiNode { if k.on_keys[i] == "press" { n.act = k.on_acts[i] } } lc_note(key, k.on_keys, k.on_acts, e) + let was_no = tr_no + let say = ui_attr_text(n, "translate") + if say == "no" { tr_no = true } else if say == "yes" { tr_no = false } bd_kids(k.kids, e, key, n.children) if k.text != null { el_text(n, ui_tr(ui_eval_text(k.text, e))) } el_inline(n) if k.tag == "input" or k.tag == "select" or k.tag == "progress" or k.tag == "meter" { ct_build(n) } + tr_no = was_no return n } function el_kind(n: UiNode, tag: string) -> void { diff --git a/packages/ludic.ui/index.ludic b/packages/ludic.ui/index.ludic index fa45806c..f9e674b0 100644 --- a/packages/ludic.ui/index.ludic +++ b/packages/ludic.ui/index.ludic @@ -30,6 +30,7 @@ import "popover.ludic" import "popover_anchor.ludic" import "popover_within.ludic" import "tooltip.ludic" +import "tooltip_text.ludic" import "life.ludic" import "tpl.ludic" import "tpl_lines.ludic" diff --git a/packages/ludic.ui/tooltip.ludic b/packages/ludic.ui/tooltip.ludic index d5c647dc..79c0c3cd 100644 --- a/packages/ludic.ui/tooltip.ludic +++ b/packages/ludic.ui/tooltip.ludic @@ -16,7 +16,7 @@ function tt_track(scope: UiNode, x: float, y: float) -> void { tt_key = n.key tt_since = an_now() } - tt_text = ui_tr(ui_attr_text(n, "title")) + tt_text = tt_translate(n, ui_attr_text(n, "title")) tt_x = x tt_y = y } diff --git a/packages/ludic.ui/tooltip_text.ludic b/packages/ludic.ui/tooltip_text.ludic new file mode 100644 index 00000000..ff09e417 --- /dev/null +++ b/packages/ludic.ui/tooltip_text.ludic @@ -0,0 +1,18 @@ +# tooltip_text.ludic - a title in the program's language: the whole of it when the translator knows +# it, else line by line; and not at all inside translate="no" (the nearest translate= out from it) +function tt_translate(n: UiNode, s: string) -> string { + var at = n + while at != null { + let say = ui_attr_text(at, "translate") + if say == "no" { return s } + if say == "yes" { break } + at = at.parent + } + let whole = ui_tr(s) + if whole != s { return whole } + let lines = tt_lines(s) + if len(lines) < 2 { return whole } + var out = ui_tr(lines[0]) + for i in 1 .. len(lines) { out = out + "\n" + ui_tr(lines[i]) } + return out +} diff --git a/tools/ludic-cli/test.ludic b/tools/ludic-cli/test.ludic index c70898c3..3a6ae557 100644 --- a/tools/ludic-cli/test.ludic +++ b/tools/ludic-cli/test.ludic @@ -771,6 +771,7 @@ function cmd_dev_test() -> int { controller_case("library/ui_nine", "", "cap 0 12 18 30/ 0 8 8 16 | wide 0 12 88 100/ 0 12 28 40 | odd 10 21 21 31/ 4 8 8 13", "ui_nine.ludic (L11: a nine-slice's corners clamped to half the box each way, on whole pixels)") controller_case("library/ui_scroll_hold", "", "120 50 0 top 120 | 80 50 0 top 80 | 80 50 0 top 80 | 80 50 70 top 80 | 80 50 30 top 80", "ui_scroll_hold.ludic (L11: scroll-top holds a box at an offset, on-scroll follows the wheel, ui_scroll_set moves one once)") controller_case("library/ui_fill", "", "0,0 2x20 #030303 .. 98,0 2x20 #fcfcfc | 0,20 50x2 #f2000d .. 0,38 50x2 #0d00f2 .. 0,58 50x2 #0000ff of 70 | 0,85 100x50 | -50,160 200x100 clipped 1 | ratio 90 knob 60", "ui_fill.ludic (L11: linear-gradient backgrounds, object-fit contain and cover, aspect-ratio for boxes and natives)") + controller_case("library/ui_translate", "", "Merhaba Hello Hello Merhaba | c/Kaydet ve cik | Satir bir/Satir iki | Line one/Line two", "ui_translate.ludic (L11: translate=no and yes, a title of several lines translated whole or line by line)") controller_case("library/ui_inputs", "", "count 12 12 32 0.5 0 | 20 32 0.5 0 | 19 32 0.5 0 | How many to carry | Space bar listening 1 19 9 0.5 0 | 19 9 0.5 0 | 19 0 0.5 0 | 60% 0.50 s | 19 0 0.6 9 | 7!", "ui_inputs.ludic (L11: number fields, key capture with Esc and Backspace, notes, range formats, an event named set, a string prop given a number)") controller_case("library/ui_boxes", "", "list 120 of 200 | text at 32 152 wide, 3 lines, row 60 | zero 0x0 | half 140x36 pad 4 2 | order abc at 16 | fill 98x8 of 200x12", "ui_boxes.ludic (L11: flex-shrink to the leftover room, text wrapping beside its siblings, zero sizes, calc(), order, the range track as a row)") controller_case("library/ui_look", "", "root div.in.i.m pad 5 margin 9 color #ff0000 text Hi 2,53 #102030 | text Hi 0,50 #eeeeee | opacity 0.25 | nine 40 #ffffff nine 40 #ff0000 of 2 image photo.png #ffffff | image icon:tent #00ff00 | text first line 29,205 #eeeeee | text second line 29,223 #eeeeee by id at 84,50 | flipped at 246,268 100x32", "ui_look.ludic (L11: nested components styled by specificity, text-shadow, ui_opacity, border-image tints, pictures, tooltip lines, anchored popovers)")