i18n 26.9, error mode: English left is an error under a lang line (a template's words, a text attribute's, a choice that reads as words, a @Text row's English), ludic deps still counting english_left; hole counts and undescribed splits stay warnings. ludic.ui's own words are keys - ui_tk(ui_st, k"ui.right_click", plain) for the key field's Right / Middle / Left click and press a key..., its plain text for a program with no translator; the examples' en.po / tr.po carry ui.*, i18n_ui checks clean, rejected/i18n_keys/english_left holds the error, data_missing counts 2; LANGUAGE.md and the changeset; on lang/foundations 2288feeb; reseeded, bootstrap-cfree fixpoint holds

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-30 02:45:15 +03:00
parent 2288feeb59
commit 25a57c099b
18 changed files with 3667 additions and 3585 deletions

View file

@ -877,6 +877,12 @@ its text after a marker byte - byte 1, or 2 for `kn"..."` - so `k"pause.resume"`
`trn` takes a plural key and `tr` / `trf` refuse one, with or without a `lang` line. (A program that `trn` takes a plural key and `tr` / `trf` refuse one, with or without a `lang` line. (A program that
declares its own type named `Key` keeps it; the builtin is then out of reach.) declares its own type named `Key` keeps it; the builtin is then out of reach.)
**A package's own words** are keys too. ludic.ui's key field says "Right click" as `ui_tk(ui_st,
k"ui.right_click", "Right click")`: translated through the program's translator when one is bound, the
plain text otherwise (a program with no languages). A program with a `lang` line that imports ludic.ui
therefore carries `ui.right_click`, `ui.middle_click`, `ui.left_click` and `ui.press_key` in its
source `.po`, and the check says so if it does not.
**The data.** A registry's `@Text` field is text by derivation: its key is **The data.** A registry's `@Text` field is text by derivation: its key is
`<registry>.<row>.<field>` - the registry's name in snake case (`Items` -> `items`, `GearKinds` -> `<registry>.<row>.<field>` - the registry's name in snake case (`Items` -> `items`, `GearKinds` ->
`gear_kinds`), a list field adding `.<index>` and a nested record `.<field>` - or, with `gear_kinds`), a list field adding `.<index>` and a nested record `.<field>` - or, with
@ -914,8 +920,8 @@ each diagnostic at its `file:line:col`:
are padding and not counted), or it is a **warning**; are padding and not counted), or it is a **warning**;
- **English left** - a template's words outside `translate="no"`, a text attribute's words (`title`, - **English left** - a template's words outside `translate="no"`, a text attribute's words (`title`,
`label`, `hint`, `text`, `caption`, and any other whose value is not a keyword), a quoted choice `label`, `hint`, `text`, `caption`, and any other whose value is not a keyword), a quoted choice
inside a hole that reads as words, and a `@Text` row whose value is still English - is a inside a hole that reads as words, and a `@Text` row whose value is still English - is an
**warning**, and `ludic deps` counts them as `english_left`, a ratchet like every number there; **error** (a warning until the migration was done; `ludic deps` still counts it as `english_left`);
- one English under several keys (a split) wants a `#.` description on each, for a translator to - one English under several keys (a split) wants a `#.` description on each, for a translator to
tell them apart: a **warning** at the `.po`'s line. tell them apart: a **warning** at the `.po`'s line.

View file

@ -0,0 +1,8 @@
bump: minor
type: feature
**English left is an error (phase 26.9).** Under a `lang` line, a template's own words, a text
attribute's, a quoted choice that reads as words and a `@Text` row still holding English now refuse
the build, where they were warnings; `ludic deps` still counts them as `english_left`. Hole counts and
undescribed splits stay warnings. ludic.ui's own words - the key field's "Right click", "Middle
click", "Left click" and "press a key..." - are keys (`ui_tk(ui_st, k"ui.right_click", plain)`,
`ui.*` in the program's `.po`), with their plain English for a program that binds no translator.

View file

@ -38,3 +38,19 @@ msgstr "Lamp"
#. What a chapter step says to do: gather firewood. #. What a chapter step says to do: gather firewood.
msgid "steps.gather_wood.hint" msgid "steps.gather_wood.hint"
msgstr "Gather wood" msgstr "Gather wood"
#. ludic.ui's key field: its value when the binding is the right mouse button.
msgid "ui.right_click"
msgstr "Right click"
#. ludic.ui's key field: the middle mouse button.
msgid "ui.middle_click"
msgstr "Middle click"
#. ludic.ui's key field: the left mouse button.
msgid "ui.left_click"
msgstr "Left click"
#. ludic.ui's key field: listening for the key the player presses next.
msgid "ui.press_key"
msgstr "press a key..."

View file

@ -30,3 +30,15 @@ msgstr "Odun topla"
msgid "old.title" msgid "old.title"
msgstr "Eski" msgstr "Eski"
msgid "ui.right_click"
msgstr "Sağ tık"
msgid "ui.middle_click"
msgstr "Orta tık"
msgid "ui.left_click"
msgstr "Sol tık"
msgid "ui.press_key"
msgstr "bir tuşa bas..."

View file

@ -4,5 +4,4 @@
<button>{t(back)}</button> <button>{t(back)}</button>
<p>{t('catch.count', caught, 'trout')}</p> <p>{t('catch.count', caught, 'trout')}</p>
<p translate="no">Maroon Lake 0.5 <b>beta</b></p> <p translate="no">Maroon Lake 0.5 <b>beta</b></p>
<p>Paused</p>
</div> </div>

View file

@ -1,5 +1,5 @@
# phase 26: a @Text Key field a row leaves out takes its derived key (things.axe.name), and en.po must # phase 26: a @Text Key field a row leaves out takes its derived key (things.axe.name), and en.po must
# have it; a @Text string field still holding English is a warning, counted as english_left # have it; a @Text string field still holding English is an error (26.9), counted as english_left
program DataMissing { program DataMissing {
property Thing { property Thing {
key: string = "" key: string = ""

View file

@ -0,0 +1,7 @@
# phase 26.9: English left in a program with a `lang` line is an error - a template's own words, and
# a text attribute's, outside translate="no"
import "ludic.ui"
program EnglishLeft {
import "parts/Words.ludic"
entry { print(1) }
}

View file

@ -22,3 +22,19 @@ msgid "catch.count"
msgid_plural "catch.count" msgid_plural "catch.count"
msgstr[0] "{1} fish" msgstr[0] "{1} fish"
msgstr[1] "{1} fish" msgstr[1] "{1} fish"
#. ludic.ui's key field: its value when the binding is the right mouse button.
msgid "ui.right_click"
msgstr "Right click"
#. ludic.ui's key field: the middle mouse button.
msgid "ui.middle_click"
msgstr "Middle click"
#. ludic.ui's key field: the left mouse button.
msgid "ui.left_click"
msgstr "Left click"
#. ludic.ui's key field: listening for the key the player presses next.
msgid "ui.press_key"
msgstr "press a key..."

View file

@ -1,2 +1,2 @@
# the key checks' refusals (key_missing, t_missing, key_mixup, data_missing, key_hole, key_rules): lang/en.po is the source # the key checks' refusals (key_missing, t_missing, key_mixup, data_missing, key_hole, key_rules, english_left): lang/en.po is the source
lang "lang" en lang "lang" en

View file

@ -0,0 +1,4 @@
# Words.ludic - english_left.ludic's component: words where keys belong
component Words {
prop day: int = 1
}

View file

@ -0,0 +1,6 @@
<div class="words">
<p>{t('pause.resume')}</p>
<p>Paused</p>
<button title="Close it">{t('hud.day', day)}</button>
<p translate="no">Maroon Lake 0.5</p>
</div>

View file

@ -236,6 +236,12 @@ function ui_tr(ui_st: UiState, s: string) -> string {
if ui_st.ui_be != null and ui_st.ui_be.translate != null { return ui_st.ui_be.translate(s) } if ui_st.ui_be != null and ui_st.ui_be.translate != null { return ui_st.ui_be.translate(s) }
return s return s
} }
# ludic.ui's own words by key (ui.* in the program's en.po); a program with no translator bound has
# no languages, and draws `plain`
function ui_tk(ui_st: UiState, k: Key, plain: string) -> string {
if ui_st.tr_no or ui_st.ui_be == null or ui_st.ui_be.translate == null { return plain }
return ui_st.ui_be.translate(string(k))
}
function ui_sound(ui_st: UiState, name: string) -> void { function ui_sound(ui_st: UiState, name: string) -> void {
if ui_st.ui_be != null and ui_st.ui_be.sound != null { ui_st.ui_be.sound(name) } if ui_st.ui_be != null and ui_st.ui_be.sound != null { ui_st.ui_be.sound(name) }
} }

View file

@ -41,7 +41,7 @@ function ct_key_pressed_away(ui_st: mut UiState, hit: UiNode) -> void {
if ui_st.ct_capture != "" and (hit == null or hit.key != ui_st.ct_capture) { ui_st.ct_capture = "" } if ui_st.ct_capture != "" and (hit == null or hit.key != ui_st.ct_capture) { ui_st.ct_capture = "" }
} }
function ct_key_text(ui_st: UiState, n: UiNode) -> string { function ct_key_text(ui_st: UiState, n: UiNode) -> string {
if ui_st.ct_capture == n.key { return ui_tr(ui_st, "press a key...") } if ui_st.ct_capture == n.key { return ui_tk(ui_st, k"ui.press_key", "press a key...") }
if Value.kind(ui_attr(n, "shown")) != 0 { return ui_tr(ui_st, ui_attr_text(n, "shown")) } if Value.kind(ui_attr(n, "shown")) != 0 { return ui_tr(ui_st, ui_attr_text(n, "shown")) }
let v = Value.as_int(ui_attr(n, "value")) let v = Value.as_int(ui_attr(n, "value"))
if v >= UI_MOUSE_LEFT and v <= UI_MOUSE_MIDDLE { return ct_mouse_name(ui_st, v) } if v >= UI_MOUSE_LEFT and v <= UI_MOUSE_MIDDLE { return ct_mouse_name(ui_st, v) }

View file

@ -16,7 +16,7 @@ function ct_key_mouse(ui_st: mut UiState, root: UiNode) -> bool {
return true return true
} }
function ct_mouse_name(ui_st: UiState, v: int) -> string { function ct_mouse_name(ui_st: UiState, v: int) -> string {
if v == UI_MOUSE_RIGHT { return ui_tr(ui_st, "Right click") } if v == UI_MOUSE_RIGHT { return ui_tk(ui_st, k"ui.right_click", "Right click") }
if v == UI_MOUSE_MIDDLE { return ui_tr(ui_st, "Middle click") } if v == UI_MOUSE_MIDDLE { return ui_tk(ui_st, k"ui.middle_click", "Middle click") }
return ui_tr(ui_st, "Left click") return ui_tk(ui_st, k"ui.left_click", "Left click")
} }

View file

@ -325,9 +325,10 @@ function i18n_short(s: pointer) -> pointer {
} }
return buf_str(b) return buf_str(b)
} }
# 26.9: an error - a program with a `lang` line has no English of its own left to draw
function i18n_english_left(file: pointer, line: int, col: int, msg: pointer) -> void { function i18n_english_left(file: pointer, line: int, col: int, msg: pointer) -> void {
g_i18n_english_left += 1 g_i18n_english_left += 1
pwarn_at(file, line, col, `English left: {msg}`) perr_at(file, line, col, `English left: {msg}`)
} }
# ---- the whole check ----------------------------------------------------------------------------- # ---- the whole check -----------------------------------------------------------------------------

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -77,13 +77,13 @@ function permap_check_case() -> void {
# phase 26: a template checks clean against en.po, its one line of words a warning, and ludic deps # phase 26: a template checks clean against en.po, its one line of words a warning, and ludic deps
# counts it as english_left # counts it as english_left
function i18n_check_case() -> void { function i18n_check_case() -> void {
let lbl = "--check: a component's template against en.po - clean, \"Paused\" English left (a warning), english_left 1 in ludic deps" let lbl = "--check: a component's template against en.po - clean, english_left 0 in ludic deps"
let js = `{tmp_dir()}/dj_i18n.json` let js = `{tmp_dir()}/dj_i18n.json`
if not shq(`bin/ludicc examples/lang/i18n/i18n_ui.ludic --check --diagnostics=json > {js} 2>/dev/null`) { bad2(lbl, capture_line(`head -3 {js}`)); return } if not shq(`bin/ludicc examples/lang/i18n/i18n_ui.ludic --check --diagnostics=json > {js} 2>/dev/null`) { bad2(lbl, capture_line(`head -3 {js}`)); return }
let got = capture(`cat {js}`) let got = capture(`cat {js}`)
if not s_contains(got, "English left: a template's words") or not s_contains(got, "Paused") { bad2(lbl, capture_line(`head -3 {js}`)); return } if s_contains(got, "\"error\"") { bad2(lbl, capture_line(`head -3 {js}`)); return }
let dp = capture(`bin/ludic deps examples/lang/i18n/i18n_ui.ludic 2>&1`) let dp = capture(`bin/ludic deps examples/lang/i18n/i18n_ui.ludic 2>&1`)
if not s_contains(dp, "english_left: 1") { bad2(lbl, `ludic deps said [{s_trim(dp)}]`); return } if not s_contains(dp, "english_left: 0") { bad2(lbl, `ludic deps said [{s_trim(dp)}]`); return }
ok(lbl) ok(lbl)
} }
# --check --diagnostics=json: refused, with exactly `n` ERRORS on stdout (warnings beside them are not # --check --diagnostics=json: refused, with exactly `n` ERRORS on stdout (warnings beside them are not
@ -1354,7 +1354,7 @@ function cmd_dev_test() -> int {
i18n_check_case() i18n_check_case()
feat_case("lang/i18n_nested/i18n_nested", "", "raids.camp.arrive.steps.0.text raids.camp.arrive.steps.1.text 2 raids.camp.said.1 0 raids.quiet.arrive.steps.0.text hud.day(3)", "i18n_nested.ludic (phase 26: derived keys below a row - a nested record's, a list item's, a @Text []Key filled from en.po; null is no text; trf's trailing \"\" not counted)") feat_case("lang/i18n_nested/i18n_nested", "", "raids.camp.arrive.steps.0.text raids.camp.arrive.steps.1.text 2 raids.camp.said.1 0 raids.quiet.arrive.steps.0.text hud.day(3)", "i18n_nested.ludic (phase 26: derived keys below a row - a nested record's, a list item's, a @Text []Key filled from en.po; null is no text; trf's trailing \"\" not counted)")
schema_case("lang/i18n/i18n", "{\"key\": \"items.rope.name\", \"kind\": \"data\", \"kinds\": [\"data\"], \"sites\": [\"examples/lang/i18n/data/i18n_items.lres:2:1\"], \"in_source\": true, \"english\": \"Rope\"", "--emit-schema: lang.keys - a derived key, its kind, its site and en.po's English") schema_case("lang/i18n/i18n", "{\"key\": \"items.rope.name\", \"kind\": \"data\", \"kinds\": [\"data\"], \"sites\": [\"examples/lang/i18n/data/i18n_items.lres:2:1\"], \"in_source\": true, \"english\": \"Rope\"", "--emit-schema: lang.keys - a derived key, its kind, its site and en.po's English")
schema_case("lang/i18n/i18n", "\"unused\": [\"map.close\"]", "--emit-schema: lang.unused - en.po's keys the program uses nowhere") schema_case("lang/i18n/i18n", "\"unused\": [\"map.close\", \"ui.left_click\", \"ui.middle_click\", \"ui.press_key\", \"ui.right_click\"]", "--emit-schema: lang.unused - en.po's keys the program uses nowhere")
schema_case("lang/i18n/i18n", "\"missing\": [\"hud.day\"], \"fuzzy\": [\"pause.close\"], \"extra\": [\"old.title\"]", "--emit-schema: lang.languages - another .po's missing, fuzzy and extra keys against en.po") schema_case("lang/i18n/i18n", "\"missing\": [\"hud.day\"], \"fuzzy\": [\"pause.close\"], \"extra\": [\"old.title\"]", "--emit-schema: lang.languages - another .po's missing, fuzzy and extra keys against en.po")
schema_case("lang/i18n/i18n", "{\"english\": \"Close\", \"keys\": [\"map.close\", \"pause.close\"]}", "--emit-schema: lang.split - one English under several keys") schema_case("lang/i18n/i18n", "{\"english\": \"Close\", \"keys\": [\"map.close\", \"pause.close\"]}", "--emit-schema: lang.split - one English under several keys")
schema_case("actions/modules", "{\"state\": \"Bag\", \"action\": \"PickUp\", \"module\": \"bag\", \"at\": \"examples/actions/mods/bag/index.ludic:4:16\"}", "--emit-schema: code.reducers - a reducer's state, action, module and place") schema_case("actions/modules", "{\"state\": \"Bag\", \"action\": \"PickUp\", \"module\": \"bag\", \"at\": \"examples/actions/mods/bag/index.ludic:4:16\"}", "--emit-schema: code.reducers - a reducer's state, action, module and place")
@ -1387,8 +1387,9 @@ function cmd_dev_test() -> int {
diag_json_case("rejected/i18n_keys/key_hole", 2, "a Key in a template's {hole}", "a Key in a template literal's hole is refused: it would draw its marked name, untranslated") diag_json_case("rejected/i18n_keys/key_hole", 2, "a Key in a template's {hole}", "a Key in a template literal's hole is refused: it would draw its marked name, untranslated")
diag_json_case("rejected/i18n_keys/key_rules", 3, "trn counts, so its key is a plural one", "trn wants a plural key, kn\"...\"") diag_json_case("rejected/i18n_keys/key_rules", 3, "trn counts, so its key is a plural one", "trn wants a plural key, kn\"...\"")
diag_json_case("rejected/i18n_keys/key_rules", 3, "a plural key needs its count", "tr / trf refuse a plural key") diag_json_case("rejected/i18n_keys/key_rules", 3, "a plural key needs its count", "tr / trf refuse a plural key")
diag_json_case("rejected/i18n_keys/data_missing", 1, "things.axe.name (the derived key of a @Text field the row gives no value): the key things.axe.name is not in", "a @Text Key field a row leaves out: its derived key must be in en.po") diag_json_case("rejected/i18n_keys/data_missing", 2, "things.axe.name (the derived key of a @Text field the row gives no value): the key things.axe.name is not in", "a @Text Key field a row leaves out: its derived key must be in en.po")
diag_json_case("rejected/i18n_keys/data_missing", 1, "English left: things.axe.blurb is", "a @Text string field still holding English: a warning") diag_json_case("rejected/i18n_keys/data_missing", 2, "English left: things.axe.blurb is", "a @Text string field still holding English: an error (26.9)")
diag_json_case("rejected/i18n_keys/english_left", 2, "English left: a template's words (\\\"Paused\\\")", "a template's words outside translate=\"no\": an error (26.9)")
diag_json_case("rejected/i18n_keys/t_missing", 1, "t('menu.gone'): the key menu.gone is not in", "a template's t('key') is checked against en.po") diag_json_case("rejected/i18n_keys/t_missing", 1, "t('menu.gone'): the key menu.gone is not in", "a template's t('key') is checked against en.po")
diag_json_case("rejected/i18n_keys/t_missing", 1, "t(): hud.day is given 0 value(s)", "a template's t('key') given fewer values than the English has holes: a warning") diag_json_case("rejected/i18n_keys/t_missing", 1, "t(): hud.day is given 0 value(s)", "a template's t('key') given fewer values than the English has holes: a warning")
feat_case("lang/defaults", "", "a 300.0 8.0 / b 120.0 8.0 / c! 300.0 8.0 / d 300.0 2.0 / x / y", "defaults.ludic (L11: default parameters, positional then named arguments, in a generic too)") feat_case("lang/defaults", "", "a 300.0 8.0 / b 120.0 8.0 / c! 300.0 8.0 / d 300.0 2.0 / x / y", "defaults.ludic (L11: default parameters, positional then named arguments, in a generic too)")