compiler: text keys below a row, trf padding and tr's cast (ECS's 26.4 gaps) - a @Text Key in a record nested in a row, or in each item of a list of them, is filled with its derived key <registry>.<row>.<field>.<i>.<field> as a top-level one is, and a @Text []Key a row leaves out takes <...>.0, .1, ... for as many as en.po has (so no .lres spells a key; the manifest and en.po are now read before the parse that fills the rows); field: null is no text; trf / trn's trailing "" literals are padding, not counted against the English's holes; and string(x) of a string or a Key is x itself to the escape analysis, no allocation site, so tr(key) passes arena strict (a template's lone hole still copies); examples/lang/i18n_nested and its feat_case (added, not run); LANGUAGE.md and the changeset; reseeded, bootstrap-cfree fixpoint holds
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
9a0011c38f
commit
10a394ae6c
12 changed files with 75642 additions and 73902 deletions
|
|
@ -883,7 +883,10 @@ declares its own type named `Key` keeps it; the builtin is then out of reach.)
|
||||||
`@TextKey("steps")` on the registry, `steps.<row>.<field>`. A `@PerMap` table's is
|
`@TextKey("steps")` on the registry, `steps.<row>.<field>`. A `@PerMap` table's is
|
||||||
`maps.<map>.<table>.<row>.<field>`. When the field's type is `Key` and a row of a compiled registry
|
`maps.<map>.<table>.<row>.<field>`. When the field's type is `Key` and a row of a compiled registry
|
||||||
gives it no value, the compiler fills in the derived key (marked, as a literal is), so code writes
|
gives it no value, the compiler fills in the derived key (marked, as a literal is), so code writes
|
||||||
`tr(Items[i].name)` and the `.lres` never spells a key; a row may still name one, `name:
|
`tr(Items[i].name)` and the `.lres` never spells a key. The same holds below the row: a nested
|
||||||
|
record's field adds `.<field>` and a list item `.<index>` (`disruptions.<row>.arrive.steps.0.text`), and a
|
||||||
|
`@Text []Key` a row leaves out takes `<...>.0`, `.1`, ... for as many as the source `.po` has; `field:
|
||||||
|
null` is no text, and is not checked; a row may still name one, `name:
|
||||||
k"items.lamp.name"`, in a resource file or a map's file alike.
|
k"items.lamp.name"`, in a resource file or a map's file alike.
|
||||||
|
|
||||||
```ludic
|
```ludic
|
||||||
|
|
@ -907,7 +910,8 @@ each diagnostic at its `file:line:col`:
|
||||||
- a `k"..."` literal, a template's `t('...')` literal, or a key a data row names or derives that the
|
- a `k"..."` literal, a template's `t('...')` literal, or a key a data row names or derives that the
|
||||||
source `.po` does not have is an **error**; so is a `kn"..."` whose entry has no `msgid_plural`;
|
source `.po` does not have is an **error**; so is a `kn"..."` whose entry has no `msgid_plural`;
|
||||||
- a call of `trf` or `trn` with a key literal, and a template's `t('key', ...)`, gives as many values
|
- a call of `trf` or `trn` with a key literal, and a template's `t('key', ...)`, gives as many values
|
||||||
after the key as the English's highest hole `{n}` (`trn`'s count is `{1}`), or it is a **warning**;
|
after the key as the English's highest hole `{n}` (`trn`'s count is `{1}`; trailing `""` literals
|
||||||
|
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 a
|
||||||
|
|
|
||||||
9
changes/i18n-fill-nested.md
Normal file
9
changes/i18n-fill-nested.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
bump: patch
|
||||||
|
type: fix
|
||||||
|
**Text keys below a row, padding, and `tr`'s cast.** A `@Text Key` in a record nested in a registry
|
||||||
|
row (and in each item of a list of them) is filled with its derived key, `<registry>.<row>.<field>.<i>.<field>`,
|
||||||
|
as a top-level one is, and a `@Text []Key` a row leaves out takes `<...>.0`, `.1`, ... for as many as
|
||||||
|
the source `.po` has - so no `.lres` spells a key. `field: null` is no text. `trf` / `trn`'s trailing
|
||||||
|
`""` arguments are padding and not counted against the English's holes. And `string(x)` of a string or
|
||||||
|
a `Key` is no allocation to the escape analysis: it is `x` itself, so a `tr(key)` that returns it
|
||||||
|
passes `arena strict` (a template's lone hole still copies).
|
||||||
4
examples/lang/i18n_nested/data/raids.lres
Normal file
4
examples/lang/i18n_nested/data/raids.lres
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
# i18n_nested.ludic's raids: no row spells a key - a nested record's @Text Key, a list of them and a
|
||||||
|
# @Text []Key are all derived; `null` says a row has no text there
|
||||||
|
camp { arrive: { steps: [ { need: 2 }, { need: 5 } ] } }
|
||||||
|
quiet { arrive: { steps: [ { need: 1 } ] }, said: null }
|
||||||
35
examples/lang/i18n_nested/i18n_nested.ludic
Normal file
35
examples/lang/i18n_nested/i18n_nested.ludic
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
# i18n_nested.ludic — derived text keys below a row (LANGUAGE.md, "Text keys"): a nested record's
|
||||||
|
# @Text Key is <registry>.<row>.<field>.<field>, a list item's adds .<index>, and a @Text []Key a row
|
||||||
|
# leaves out takes <...>.0, .1, ... for as many as en.po has. `field: null` is no text. trf's trailing
|
||||||
|
# "" pad its unused values and are not counted against the English's holes.
|
||||||
|
#
|
||||||
|
# Running it prints: 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)
|
||||||
|
program I18nNested {
|
||||||
|
property Step {
|
||||||
|
@Text text: Key = null
|
||||||
|
need: int = 0
|
||||||
|
}
|
||||||
|
property Arrive { steps: []Step = null }
|
||||||
|
property Raid {
|
||||||
|
key: string = ""
|
||||||
|
arrive: Arrive = null
|
||||||
|
@Text said: []Key = null
|
||||||
|
}
|
||||||
|
registry Raids of Raid as RD from "data/raids.lres"
|
||||||
|
function tr(key: Key) -> string {
|
||||||
|
let s = string(key)
|
||||||
|
return s[1..len(s)]
|
||||||
|
}
|
||||||
|
function trf(key: Key, a: string, b: string, c: string) -> string { return `{tr(key)}({a}{b}{c})` }
|
||||||
|
function said_n(r: Raid) -> int {
|
||||||
|
if r.said == null { return 0 }
|
||||||
|
return len(r.said)
|
||||||
|
}
|
||||||
|
entry {
|
||||||
|
let camp = Raids[RD_CAMP]
|
||||||
|
let quiet = Raids[RD_QUIET]
|
||||||
|
var out = `{tr(camp.arrive.steps[0].text)} {tr(camp.arrive.steps[1].text)} {said_n(camp)} {tr(camp.said[1])}`
|
||||||
|
out = out + ` {said_n(quiet)} {tr(quiet.arrive.steps[0].text)} {trf(k"hud.day", "3", "", "")}`
|
||||||
|
print(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
28
examples/lang/i18n_nested/lang/en.po
Normal file
28
examples/lang/i18n_nested/lang/en.po
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
# The source language of the nested keys example.
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
|
||||||
|
#. A raid's first step, at the camp.
|
||||||
|
msgid "raids.camp.arrive.steps.0.text"
|
||||||
|
msgstr "Find the tracks"
|
||||||
|
|
||||||
|
#. A raid's second step, at the camp.
|
||||||
|
msgid "raids.camp.arrive.steps.1.text"
|
||||||
|
msgstr "Count {1} footprints"
|
||||||
|
|
||||||
|
#. The quiet raid's only step.
|
||||||
|
msgid "raids.quiet.arrive.steps.0.text"
|
||||||
|
msgstr "Wait"
|
||||||
|
|
||||||
|
#. What Ada says when the camp is raided, first.
|
||||||
|
msgid "raids.camp.said.0"
|
||||||
|
msgstr "They came in the night."
|
||||||
|
|
||||||
|
#. What Ada says when the camp is raided, second.
|
||||||
|
msgid "raids.camp.said.1"
|
||||||
|
msgstr "Check the box."
|
||||||
|
|
||||||
|
#. The day on the HUD. {1} = the day number
|
||||||
|
msgid "hud.day"
|
||||||
|
msgstr "Day {1}"
|
||||||
2
examples/lang/i18n_nested/package.ludic
Normal file
2
examples/lang/i18n_nested/package.ludic
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# the nested text keys example (i18n_nested.ludic): lang/en.po is the source every key is checked against
|
||||||
|
lang "lang" en
|
||||||
|
|
@ -104,6 +104,8 @@ function es_local_ty(name: pointer) -> pointer {
|
||||||
while i >= 0 { if (g_es_ln[i] == name) { return g_es_lt[i] }; i -= 1 }
|
while i >= 0 { if (g_es_ln[i] == name) { return g_es_lt[i] }; i -= 1 }
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
# a type string() hands back as it is
|
||||||
|
function es_passes(t: pointer) -> bool { return t != null and ((t == "string") or i18n_is_key_ty(t)) }
|
||||||
# the static type of an expression, as far as the analysis can tell without emitting it
|
# the static type of an expression, as far as the analysis can tell without emitting it
|
||||||
function es_type(e: Node) -> pointer {
|
function es_type(e: Node) -> pointer {
|
||||||
if e == null { return null }
|
if e == null { return null }
|
||||||
|
|
@ -317,6 +319,9 @@ function es_call(e: Node) -> int {
|
||||||
return g_es_heap
|
return g_es_heap
|
||||||
}
|
}
|
||||||
if (nm == "string") {
|
if (nm == "string") {
|
||||||
|
# string(s) of a string or a Key is s itself (emit_call.ludic) - no text is made, unless a
|
||||||
|
# template's lone hole copies it
|
||||||
|
if len(e.kids) == 1 and c.uns != TPL_COPY and es_passes(es_type(e.kids[0])) { return es_val(e.kids[0]) }
|
||||||
es_args_walk(e)
|
es_args_walk(e)
|
||||||
return es_site(e)
|
return es_site(e)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -183,29 +183,72 @@ function i18n_has_text(f: Node) -> bool {
|
||||||
function i18n_is_key_ty(t: pointer) -> bool { return t != null and (t == "Key") and find_comp(t) == null }
|
function i18n_is_key_ty(t: pointer) -> bool { return t != null and (t == "Key") and find_comp(t) == null }
|
||||||
# a compiled registry's row (reg_fill): a @Text Key field it gives no value takes its derived key
|
# a compiled registry's row (reg_fill): a @Text Key field it gives no value takes its derived key
|
||||||
function i18n_fill_row(r: int, key: pointer, rec: Node, comp: Node, kline: int, kcol: int) -> void {
|
function i18n_fill_row(r: int, key: pointer, rec: Node, comp: Node, kline: int, kcol: int) -> void {
|
||||||
if comp == null { return }
|
i18n_fill_rec(rec, comp, `{i18n_prefix(r)}.{key}`, kline, kcol)
|
||||||
|
}
|
||||||
|
# a record at `base` (a row, or a record nested in one): a @Text Key it leaves out takes base.field,
|
||||||
|
# a @Text []Key it leaves out takes base.field.0, .1, ... as many as en.po has; given ones are walked
|
||||||
|
function i18n_fill_rec(rec: Node, comp: Node, base: pointer, kline: int, kcol: int) -> void {
|
||||||
|
if rec == null or comp == null { return }
|
||||||
var i = 0
|
var i = 0
|
||||||
while i < len(comp.kids) {
|
while i < len(comp.kids) {
|
||||||
let f = comp.kids[i]
|
let f = comp.kids[i]
|
||||||
if f.kind == N_FIELD and i18n_has_text(f) and i18n_is_key_ty(f.ty) and not reg_rec_has(rec, f.s) {
|
if f.kind == N_FIELD {
|
||||||
let fi = node(E_FINIT)
|
let path = `{base}.{f.s}`
|
||||||
fi.s = f.s
|
let fi = i18n_field_init(rec, f.s)
|
||||||
fi.file = rec.file
|
if fi != null { i18n_fill_val(fi.a, f.ty, path, kline, kcol) }
|
||||||
fi.line = kline
|
else if i18n_has_text(f) and i18n_is_key_ty(f.ty) { push(rec.kids, i18n_finit(f.s, i18n_derived(path, rec.file, kline, kcol), rec.file, kline, kcol)) }
|
||||||
fi.col = kcol
|
else if i18n_has_text(f) and i18n_is_keys_ty(f.ty) { i18n_fill_list(rec, f.s, path, kline, kcol) }
|
||||||
let ks = node(E_STR)
|
|
||||||
ks.s = key_marked(KEY_LIT, `{i18n_prefix(r)}.{key}.{f.s}`)
|
|
||||||
ks.ival = KEY_LIT
|
|
||||||
ks.ty = "derived" # the checks know it was not written
|
|
||||||
ks.file = rec.file
|
|
||||||
ks.line = kline
|
|
||||||
ks.col = kcol
|
|
||||||
fi.a = ks
|
|
||||||
push(rec.kids, fi)
|
|
||||||
}
|
}
|
||||||
i += 1
|
i += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function i18n_fill_val(e: Node, ty: pointer, path: pointer, kline: int, kcol: int) -> void {
|
||||||
|
if e == null { return }
|
||||||
|
if e.kind == E_NEW and e.a != null and e.a.kind == E_REC { i18n_fill_rec(e.a, find_comp(e.s), path, kline, kcol) }
|
||||||
|
else if e.kind == E_REC and ty != null { i18n_fill_rec(e, find_comp(ty), path, kline, kcol) }
|
||||||
|
else if e.kind == E_LIST {
|
||||||
|
var el: pointer = null
|
||||||
|
if ty != null and is_slice_ty(ty) { el = slice_elem(ty) }
|
||||||
|
var i = 0
|
||||||
|
while i < len(e.kids) {
|
||||||
|
i18n_fill_val(e.kids[i], el, `{path}.{itoa(i)}`, kline, kcol)
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# a []Key the row leaves out: en.po's path.0, path.1, ... while they last (none without an en.po)
|
||||||
|
function i18n_fill_list(rec: Node, name: pointer, path: pointer, kline: int, kcol: int) -> void {
|
||||||
|
if g_i18n_en == null { return }
|
||||||
|
let lst = node(E_LIST)
|
||||||
|
lst.file = rec.file
|
||||||
|
lst.line = kline
|
||||||
|
var i = 0
|
||||||
|
while po_find(g_i18n_en, `{path}.{itoa(i)}`) >= 0 {
|
||||||
|
push(lst.kids, i18n_derived(`{path}.{itoa(i)}`, rec.file, kline, kcol))
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
if i > 0 { push(rec.kids, i18n_finit(name, lst, rec.file, kline, kcol)) }
|
||||||
|
}
|
||||||
|
function i18n_is_keys_ty(t: pointer) -> bool { return t != null and is_slice_ty(t) and i18n_is_key_ty(slice_elem(t)) }
|
||||||
|
function i18n_derived(key: pointer, file: pointer, line: int, col: int) -> Node {
|
||||||
|
let ks = node(E_STR)
|
||||||
|
ks.s = key_marked(KEY_LIT, key)
|
||||||
|
ks.ival = KEY_LIT
|
||||||
|
ks.ty = "derived" # the checks know it was not written
|
||||||
|
ks.file = file
|
||||||
|
ks.line = line
|
||||||
|
ks.col = col
|
||||||
|
return ks
|
||||||
|
}
|
||||||
|
function i18n_finit(name: pointer, v: Node, file: pointer, line: int, col: int) -> Node {
|
||||||
|
let fi = node(E_FINIT)
|
||||||
|
fi.s = name
|
||||||
|
fi.file = file
|
||||||
|
fi.line = line
|
||||||
|
fi.col = col
|
||||||
|
fi.a = v
|
||||||
|
return fi
|
||||||
|
}
|
||||||
|
|
||||||
# ---- the sites and the key checks ----------------------------------------------------------------
|
# ---- the sites and the key checks ----------------------------------------------------------------
|
||||||
function i18n_site(key: pointer, kind: int, file: pointer, line: int, col: int) -> bool {
|
function i18n_site(key: pointer, kind: int, file: pointer, line: int, col: int) -> bool {
|
||||||
|
|
@ -339,8 +382,11 @@ function i18n_call(n: Node) -> void {
|
||||||
perr_at(e.file, e.line, i18n_col(e), `{nm}(kn"{key}", ...): a plural key needs its count - write trn(kn"{key}", n, ...)`)
|
perr_at(e.file, e.line, i18n_col(e), `{nm}(kn"{key}", ...): a plural key needs its count - write trn(kn"{key}", n, ...)`)
|
||||||
}
|
}
|
||||||
if nm == "tr" or g_i18n_en == null { return }
|
if nm == "tr" or g_i18n_en == null { return }
|
||||||
i18n_holes(key, len(n.kids) - k - 1, nm, e.file, e.line, i18n_col(e))
|
var last = len(n.kids)
|
||||||
|
while last > k + 1 and i18n_is_pad(n.kids[last - 1]) { last -= 1 } # trailing "" fill trf / trn's unused slots
|
||||||
|
i18n_holes(key, last - k - 1, nm, e.file, e.line, i18n_col(e))
|
||||||
}
|
}
|
||||||
|
function i18n_is_pad(e: Node) -> bool { return e != null and e.kind == E_STR and not i18n_is_key(e) and (e.s == null or len(e.s) == 0) }
|
||||||
# a node's column in its file: a template literal's hole is lexed on its own, so its tokens' columns
|
# a node's column in its file: a template literal's hole is lexed on its own, so its tokens' columns
|
||||||
# are the hole's - the file offset says where it really is
|
# are the hole's - the file offset says where it really is
|
||||||
function i18n_col(n: Node) -> int {
|
function i18n_col(n: Node) -> int {
|
||||||
|
|
|
||||||
72743
selfhost/ludicc.seed.ll
72743
selfhost/ludicc.seed.ll
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
|
|
@ -202,6 +202,7 @@ entry {
|
||||||
if (target != null) { g_target_win = has_sub(target, "windows") }
|
if (target != null) { g_target_win = has_sub(target, "windows") }
|
||||||
else { g_target_win = host_is_windows() }
|
else { g_target_win = host_is_windows() }
|
||||||
|
|
||||||
|
i18n_manifest(path) # phase 26: a `lang` line and its en.po, before the parse fills rows from it
|
||||||
let src = read_file(path)
|
let src = read_file(path)
|
||||||
if (src == null) { die("ludicc: cannot open input\n") }
|
if (src == null) { die("ludicc: cannot open input\n") }
|
||||||
|
|
||||||
|
|
@ -224,7 +225,6 @@ entry {
|
||||||
|
|
||||||
maybe_splice_runtime()
|
maybe_splice_runtime()
|
||||||
fence_manifest(path) # 25.1: a `fence` line in the program's package.ludic
|
fence_manifest(path) # 25.1: a `fence` line in the program's package.ludic
|
||||||
i18n_manifest(path) # phase 26: a `lang` line, and its en.po (i18n.ludic)
|
|
||||||
ui_register_gotos() # `goto:` buttons -> generated UiClicked listeners
|
ui_register_gotos() # `goto:` buttons -> generated UiClicked listeners
|
||||||
|
|
||||||
# a game gets a window by default; a plain program stays headless. An explicit
|
# a game gets a window by default; a plain program stays headless. An explicit
|
||||||
|
|
|
||||||
|
|
@ -1352,6 +1352,7 @@ function cmd_dev_test() -> int {
|
||||||
# phase 26: text keys - k"..." / kn"..." of type Key, checked against package.ludic's lang en.po
|
# phase 26: text keys - k"..." / kn"..." of type Key, checked against package.ludic's lang en.po
|
||||||
feat_case("lang/i18n/i18n", "", "pause.resume yes no 13 12 hud.day(3) catch.count[2](trout) items.rope.name items.lamp.name steps.gather_wood.hint", "i18n.ludic (phase 26: k\"...\" and kn\"...\" of type Key, their marker byte, == on keys, a @Text Key field filled with its derived key, @TextKey's prefix)")
|
feat_case("lang/i18n/i18n", "", "pause.resume yes no 13 12 hud.day(3) catch.count[2](trout) items.rope.name items.lamp.name steps.gather_wood.hint", "i18n.ludic (phase 26: k\"...\" and kn\"...\" of type Key, their marker byte, == on keys, a @Text Key field filled with its derived key, @TextKey's prefix)")
|
||||||
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)")
|
||||||
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\"]", "--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")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue