compiler: text keys' follow-up (Master's rulings) - a Key in a template literal's hole is an error (the parser marks each hole's string(), TPL_HOLE; write trf(k"...", ...)), trn takes only a plural key kn"..." and tr / trf refuse one (with or without en.po), and a program with no lang line (a package test) uses key literals unchecked and silently - the warning stays for a lang line whose en.po is missing; rejected/i18n_keys gains key_hole (2) and key_rules (3), key_missing's plural moves to trn; LANGUAGE.md and the changeset say so; reseeded, bootstrap-cfree fixpoint holds

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-29 23:46:48 +03:00
parent 66b8e51026
commit 9f9ac4a7ce
13 changed files with 72839 additions and 72164 deletions

View file

@ -836,7 +836,9 @@ results, list elements and registry values like any other type (`null` is none).
its text after a marker byte - byte 1, or 2 for `kn"..."` - so `k"pause.resume"` is the string
`"\x01pause.resume"`: the runtime's `tr` is a cast, and a translator knows a key from English.
`string(key)` gives that marked text, for a runtime that needs it; a program itself makes text with
`tr`. (A program that declares its own type named `Key` keeps it; the builtin is then out of reach.)
`tr`, and a key in a template literal's hole (`` `at {k}` ``) is an error - write `trf(k"...", ...)`.
`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.)
**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` ->
@ -876,9 +878,10 @@ each diagnostic at its `file:line:col`:
- 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.
With no `lang` line, or no source `.po`, nothing is checked, and the first key literal says so once.
With no `lang` line nothing is checked and nothing is said (a package test uses key literals freely); a
`lang` line whose source `.po` is missing checks nothing, and the first key literal says so once.
The source `.po` is gettext's: `msgid` (the key), `msgid_plural` and `msgstr[n]`, `#,` flags
(`fuzzy`), `#.` descriptions, `msgctxt` (read, keying nothing), strings over several lines; `#~`
(`fuzzy`), `#.` descriptions, `msgctxt` (read and ignored: the key is the context), strings over several lines; `#~`
entries are obsolete. `ludic schema` carries a `"lang"` object (null without a `lang` line): every
key used with its kind (`code`, `template`, `data`), its sites, its English, whether it is a plural,
and its description; `unused` (the source's keys used nowhere), `undescribed`, `split`, and per other

View file

@ -11,3 +11,6 @@ holes, text still written in English (a template's words, a `@Text` row's value
`english_left` by `ludic deps`) and one English split into keys without `#.` descriptions are
warnings. `ludic schema` gains `"lang"`: every key with its sites, unused and undescribed keys, splits,
and each other language's missing, fuzzy and extra keys.
A `Key` in a template literal's hole is an error (write `trf(k"...", ...)`), `trn` takes only a plural
key and `tr` / `trf` refuse one; a program with no `lang` line (a package test) uses key literals
unchecked and silently.

View file

@ -0,0 +1,9 @@
# phase 26: a Key in a template literal's {hole} is refused - it would draw its marked name, never
# translated; text is made with tr / trf
program KeyHole {
entry {
let k = k"pause.resume"
print(`{k}`)
print(`at {k}!`)
}
}

View file

@ -3,9 +3,10 @@
program KeyMissing {
function tr(key: Key) -> string { return string(key) }
function trf(key: Key, a: string, b: string) -> string { return string(key) + a + b }
function trn(key: Key, n: int) -> string { return string(key) }
entry {
print(tr(k"pause.gone"))
print(tr(kn"pause.resume"))
print(trn(kn"pause.resume", 1))
print(trf(k"hud.day", "1", "2"))
}
}

View file

@ -0,0 +1,12 @@
# phase 26: trn wants a plural key kn"...", and tr / trf refuse one
program KeyRules {
function tr(key: Key) -> string { return string(key) }
function trf(key: Key, a: string) -> string { return string(key) + a }
function trn(key: Key, n: int) -> string { return string(key) }
entry {
print(trn(k"hud.day", 2))
print(trf(kn"catch.count", "1"))
print(tr(kn"catch.count"))
print(tr(k"pause.resume"))
}
}

View file

@ -16,3 +16,9 @@ msgstr "Close"
msgid "map.close"
msgstr "Close"
#. The catch on the HUD. {1} = how many
msgid "catch.count"
msgid_plural "catch.count"
msgstr[0] "{1} fish"
msgstr[1] "{1} fish"

View file

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

View file

@ -100,6 +100,7 @@ function ck_builtin(e: Node, name: pointer) -> pointer {
ck_walk_args(e)
return "string"
}
if (name == "string") and ck_tpl_hole(e) { return ck_hole(e) }
if (name == "string") or (name == "int") or (name == "float") or (name == "fixed") or (name == "long") or (name == "double") {
ck_walk_args(e)
return name
@ -313,3 +314,12 @@ function ck_emit(s: Node) -> void {
i += 1
}
}
# a template literal's {hole}: text, never a key - a Key there would draw its marked name untranslated
function ck_tpl_hole(e: Node) -> bool {
return e.a != null and (e.a.uns == TPL_HOLE or e.a.uns == TPL_COPY) and len(e.kids) == 1
}
function ck_hole(e: Node) -> pointer {
let t = ck_expr(e.kids[0])
if ck_is_key(t) { ck_err("key", e.kids[0], "a Key in a template's {hole}: a key names a text, it is not one - translate it with trf(k\"...\", ...) (tr(key) for no values), or say string(key) outside the template if the marked name is really wanted") }
return "string"
}

View file

@ -225,15 +225,14 @@ function i18n_ks_name(kind: int) -> pointer {
if kind == KS_DATA { return "data" }
return "code"
}
# a key used: in en.po, and a plural one as a plural there. Nothing to check against: said once
# a key used: in en.po, and a plural one as a plural there. A lang line with no en.po: said once;
# no lang line at all (a package test) checks nothing and says nothing
function i18n_use(key: pointer, plural: bool, kind: int, file: pointer, line: int, col: int) -> void {
if not i18n_site(key, kind, file, line, col) { return }
if g_i18n_en == null {
if kind != KS_TEMPLATE and not g_i18n_nopo_warned {
if g_i18n_on and kind != KS_TEMPLATE and not g_i18n_nopo_warned {
g_i18n_nopo_warned = true
var why = "this program's package.ludic has no `lang \"assets/lang\" en` line"
if g_i18n_on { why = `{g_i18n_en_path} is not there` }
pwarn_at(file, line, col, `k"{key}": no en.po to check against - {why}; no key is checked`)
pwarn_at(file, line, col, `k"{key}": no en.po to check against - {g_i18n_en_path} is not there; no key is checked`)
}
return
}
@ -312,7 +311,7 @@ function i18n_walk(n: Node) -> void {
if i18n_is_key(n) and not i18n_is_data_file(n.file) {
i18n_use(i18n_key_text(n), n.ival == KEY_PLURAL, KS_CODE, n.file, n.line, i18n_col(n))
}
if n.kind == E_CALL and n.a != null and n.a.kind == E_ID and g_i18n_en != null { i18n_call(n) }
if n.kind == E_CALL and n.a != null and n.a.kind == E_ID { i18n_call(n) }
i18n_walk(n.a)
i18n_walk(n.b)
i18n_walk(n.c)
@ -322,15 +321,25 @@ function i18n_walk(n: Node) -> void {
i += 1
}
}
# trf(k"...", a, b) / trn(kn"...", n, a): the values after the key against its holes
# tr(k"...") / trf(k"...", a, b) / trn(kn"...", n, a): a plural key only with trn, and trn only with
# one (en.po or not); then the values after the key against its holes
function i18n_call(n: Node) -> void {
let nm = vis_plain(n.a.s)
if not (nm == "trf") and not (nm == "trn") { return }
if not (nm == "tr") and not (nm == "trf") and not (nm == "trn") { return }
var k = 0
while k < len(n.kids) and not i18n_is_key(n.kids[k]) { k += 1 }
if k >= len(n.kids) { return }
let e = n.kids[k]
i18n_holes(i18n_key_text(e), len(n.kids) - k - 1, nm, e.file, e.line, i18n_col(e))
let key = i18n_key_text(e)
let plural = e.ival == KEY_PLURAL
if nm == "trn" and not plural {
perr_at(e.file, e.line, i18n_col(e), `trn(k"{key}", ...): trn counts, so its key is a plural one - write kn"{key}" (and give en.po a msgid_plural)`)
}
if plural and not (nm == "trn") {
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))
}
# 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

View file

@ -248,7 +248,7 @@ function interp_add(acc: Node, part: Node) -> Node {
return mkbin("+", acc, part)
}
function interp_str(e: Node) -> Node { # wrap a hole in string(...)
let c = node(E_CALL); let id = node(E_ID); id.s = "string"; c.a = id; push(c.kids, e); return c
let c = node(E_CALL); let id = node(E_ID); id.s = "string"; id.uns = TPL_HOLE; c.a = id; push(c.kids, e); return c
}
var g_interp_line: int = 1 # the line the interpolated string sits on
var g_interp_pos: int = -1 # the backtick's place in its file
@ -297,6 +297,7 @@ function parse_interp(raw: pointer) -> Node {
return acc
}
const TPL_COPY: int = 91 # on the callee of a template's lone string(hole)
const TPL_HOLE: int = 92 # on the callee of every other string(hole) (a Key is refused there)
# emit E(field: v, ...) — shared by the statement form and the expression form.
# As an expression it yields a cancellable event's cancelled flag (0/1); a

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -1310,6 +1310,9 @@ function cmd_dev_test() -> int {
diag_json_case("rejected/i18n_keys/key_mixup", 3, "argument 1 of show wants a string and this is a Key", "a Key is not a string")
diag_json_case("rejected/i18n_keys/key_mixup", 3, "argument 1 of name_of wants a Key and this is a string", "a string is not a Key")
diag_json_case("rejected/i18n_keys/key_mixup", 3, "+ on a Key", "a Key joins no text")
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, "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", 1, "English left: things.axe.blurb is", "a @Text string field still holding English: a warning")
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")