Merge commit '10a394ae' into lang/foundations (nested and list @Text keys derived, trf's trailing "" padding not counted, string(k) no escape site); reseeded, bootstrap-cfree fixpoint holds
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
c3a37bf76c
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
|
||||
`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
|
||||
`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.
|
||||
|
||||
```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
|
||||
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
|
||||
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`,
|
||||
`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
|
||||
|
|
|
|||
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 }
|
||||
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
|
||||
function es_type(e: Node) -> pointer {
|
||||
if e == null { return null }
|
||||
|
|
@ -317,6 +319,9 @@ function es_call(e: Node) -> int {
|
|||
return g_es_heap
|
||||
}
|
||||
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)
|
||||
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 }
|
||||
# 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 {
|
||||
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
|
||||
while i < len(comp.kids) {
|
||||
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) {
|
||||
let fi = node(E_FINIT)
|
||||
fi.s = f.s
|
||||
fi.file = rec.file
|
||||
fi.line = kline
|
||||
fi.col = 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)
|
||||
if f.kind == N_FIELD {
|
||||
let path = `{base}.{f.s}`
|
||||
let fi = i18n_field_init(rec, f.s)
|
||||
if fi != null { i18n_fill_val(fi.a, f.ty, path, kline, kcol) }
|
||||
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)) }
|
||||
else if i18n_has_text(f) and i18n_is_keys_ty(f.ty) { i18n_fill_list(rec, f.s, path, kline, kcol) }
|
||||
}
|
||||
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 ----------------------------------------------------------------
|
||||
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, ...)`)
|
||||
}
|
||||
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
|
||||
# are the hole's - the file offset says where it really is
|
||||
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") }
|
||||
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)
|
||||
if (src == null) { die("ludicc: cannot open input\n") }
|
||||
|
||||
|
|
@ -224,7 +225,6 @@ entry {
|
|||
|
||||
maybe_splice_runtime()
|
||||
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
|
||||
|
||||
# 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
|
||||
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()
|
||||
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", "\"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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue