feat(ui): translate="no", and a title of several lines translated whole or line by line

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-24 22:03:49 +03:00
parent 7605a0253c
commit 6bd3228990
9 changed files with 76 additions and 1 deletions

View file

@ -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.

View file

@ -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).

View file

@ -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 = "<ui><screen name='t'><p>Hello</p><p translate='no'>Hello</p><div translate='no'><p>Hello</p><p translate='yes'>Hello</p></div><p class='a' title='Save\\nQuit'>a</p><p class='b' title='Line one\\nLine two'>b</p><div translate='no'><p class='c' title='Line one\\nLine two'>c</p></div></screen></ui>"
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)}`)
}
}

View file

@ -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
}

View file

@ -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 {

View file

@ -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"

View file

@ -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
}

View file

@ -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
}

View file

@ -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)")