Merge lang/i18nkeys (27024de) into lang/syntax: the vocabulary gains the text keys - the Key type, @TextKey (a registry's), the k"..." and kn"..." literals - and @Text's doc; docs/language pages for Key and @TextKey; ludic-dev syntax rewrote the grammars; 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 00:02:48 +03:00
commit 9f8aec3bc1
63 changed files with 145288 additions and 114562 deletions

View file

@ -176,6 +176,7 @@ function at_decl_clear() -> void {
g_at_decl_args = new []pointer
g_pm_pending = false # permap.ludic: @PerMap and @Chunked belong to one declaration
g_pm_chunk_pending = 0
g_tk_pending = null # i18n.ludic: @TextKey belongs to one declaration too
}
# the registry just declared takes the attributes read before it
function at_registry_take() -> int {

View file

@ -0,0 +1,481 @@
# i18n.ludic — keys, not English (phase 26 of Maroon Lake's plan). A text the player reads is named
# by a key, and the key's English is en.po, a language like any other:
#
# lang "assets/lang" en # package.ludic: the languages' directory and the source language
# tr(k"pause.resume") # a Key, the builtin type of k"...": not a string, nor a string one
# trn(kn"catch.count", n, name) # a plural key: en.po's entry has a msgid_plural
# <button>{t('pause.resume')}</button> # a template's text (i18n_xml.ludic)
# @TextKey("steps") registry StepKinds of StepKind from "steps.lres"
# property StepKind { @Text hint: Key = null } # the row's key is steps.<row>.hint, filled in
#
# At run time a Key is its text after a marker byte - 1 for k"...", 2 for kn"..." - so the runtime's
# tr(k) is a cast and the translator knows a key from English. What the compiler checks, once the
# program is read (i18n_check, after the types): every key literal is in en.po (a kn"" one with a
# msgid_plural); trf / trn and a template's t() give as many values as the English has holes (a
# warning); a registry's @Text field left in the data as English, and a template's words outside
# translate="no", are "English left" (a warning each, counted as `english_left` by ludic deps); a
# @Text Key field with no value is filled with its derived key, which en.po must have; and the same
# English under several keys wants a `#.` description on each (a warning at en.po's line). With no
# `lang` line, or no en.po, nothing is checked, and a program's first key literal says so once.
const KEY_LIT: int = 1
const KEY_PLURAL: int = 2
const KS_CODE: int = 0
const KS_TEMPLATE: int = 1
const KS_DATA: int = 2
var g_i18n_on: bool = false # a `lang` line was read
var g_i18n_dir: pointer = null # its directory, as the program opens it
var g_i18n_src: pointer = "en"
var g_i18n_en: PoFile = null # <dir>/<src>.po, or null when it is not there
var g_i18n_en_path: pointer = ""
var g_i18n_nopo_warned: bool = false
var g_i18n_english_left: int = 0 # ludic deps' english_left
var g_i18n_done: bool = false
var g_rg_textkey: []pointer = new []pointer # per registry: its @TextKey prefix, or null
var g_tk_pending: pointer = null # @TextKey("x") read before the registry being parsed
# every key used, one row a site: the key, where, and what kind of source (KS_*)
var g_ks_key: []pointer = new []pointer
var g_ks_kind: []int = new []int
var g_ks_file: []pointer = new []pointer
var g_ks_line: []int = new []int
var g_ks_col: []int = new []int
var g_ks_seen: StrSet = null
var g_i18n_what: pointer = null # how a message names the key, when not as its literal
# ---- the literal ---------------------------------------------------------------------------------
# k"..." (ival 1) or kn"..." (ival 2): a string node holding the marker byte and the key
function key_lit_node(t: Tok) -> Node {
let n = node(E_STR)
n.line = t.line
n.col = t.col
n.s = key_marked(t.ival, t.text)
n.ival = t.ival
return n
}
function key_marked(kind: int, key: pointer) -> pointer {
let k = len(key)
let b = bytes(k + 2)
b[0] = kind
var i = 0
while i < k {
b[i + 1] = key[i]
i += 1
}
b[k + 1] = 0
return b
}
function i18n_is_key(e: Node) -> bool { return e != null and e.kind == E_STR and (e.ival == KEY_LIT or e.ival == KEY_PLURAL) }
function i18n_key_text(e: Node) -> pointer { return e.s[1 .. len(e.s)] }
# ---- a set of strings, grown as it fills ---------------------------------------------------------
property StrSet {
keys: []pointer = null
n: int = 0
}
function ss_new(cap: int) -> StrSet {
let s = new StrSet
s.keys = new []pointer
var i = 0
while i < cap {
push(s.keys, null)
i += 1
}
return s
}
function ss_slot(s: StrSet, k: pointer) -> int {
let cap = len(s.keys)
var h = po_hash(k) & (cap - 1)
while s.keys[h] != null and not (s.keys[h] == k) { h = (h + 1) & (cap - 1) }
return h
}
function ss_has(s: StrSet, k: pointer) -> bool { return s.keys[ss_slot(s, k)] != null }
# true when k was not in the set
function ss_add(s: StrSet, k: pointer) -> bool {
if (s.n + 1) * 2 > len(s.keys) {
let old = s.keys
let bigger = ss_new(len(old) * 2)
var i = 0
while i < len(old) {
if old[i] != null { bigger.keys[ss_slot(bigger, old[i])] = old[i] }
i += 1
}
s.keys = bigger.keys
}
let h = ss_slot(s, k)
if s.keys[h] != null { return false }
s.keys[h] = k
s.n += 1
return true
}
# ---- the manifest --------------------------------------------------------------------------------
# package.ludic's `lang "assets/lang" en`, looked for as `maps` is: the nearest package.ludic up to four
# directories above the program, the directory taken relative to it
function i18n_manifest(entry: pointer) -> void {
var dir = dir_of(entry)
var up = 0
while up < 5 {
var root = dir
if len(root) > 0 and not is_sep(root[len(root) - 1]) { root = root + "/" }
let txt = read_file(root + "package.ludic")
if txt != null {
i18n_manifest_read(txt, root)
return
}
if len(dir) == 0 { dir = ".." } else { dir = dir + "/.." }
up += 1
}
}
function i18n_manifest_read(txt: pointer, root: pointer) -> void {
var i = 0
let tn = len(txt)
while i < tn {
var e = i
while (e < tn) and (txt[e] != '\n') { e += 1 }
var le = e
if (le > i) and (txt[le - 1] == 13) { le -= 1 }
let ws = native_words(txt[i..le])
if (len(ws) == 2 or len(ws) == 3) and (ws[0] == "lang") {
g_i18n_on = true
g_i18n_dir = pm_norm(join_path(root, pm_trim_slash(ws[1])))
if len(ws) == 3 { g_i18n_src = ws[2] }
}
i = e + 1
}
if g_i18n_on {
g_i18n_en_path = `{g_i18n_dir}/{g_i18n_src}.po`
g_i18n_en = po_read(g_i18n_en_path)
}
}
# ---- @TextKey and the derived keys ---------------------------------------------------------------
# @TextKey("steps") before a registry, its '@' and name read
function i18n_parse_textkey() -> void {
eat_op("(")
if toks[pi].kind != TK_STR or len(toks[pi].text) == 0 { perr("@TextKey takes the prefix of the registry's text keys: @TextKey(\"steps\")") }
g_tk_pending = toks[pi].text
pi += 1
eat_op(")")
let b = buf_new()
jq_put(b, g_tk_pending)
at_decl_with("TextKey", `[{buf_str(b)}]`)
}
# the registry just declared takes it
function i18n_registry_declared() -> void {
push(g_rg_textkey, g_tk_pending)
g_tk_pending = null
}
# where a registry's derived keys start: its @TextKey, else its name in snake case (Items -> items,
# GearKinds -> gear_kinds). One rule, asked by the fill, the checks and the schema alike
function i18n_prefix(r: int) -> pointer {
if r >= 0 and r < len(g_rg_textkey) and g_rg_textkey[r] != null { return g_rg_textkey[r] }
return pm_snake(vis_plain(g_rg_name[r]))
}
function i18n_has_text(f: Node) -> bool {
if f == null or f.at < 0 { return false }
var j = g_at_start[f.at]
while j < g_at_start[f.at] + g_at_count[f.at] {
if (g_at_name[j] == "Text") { return true }
j += 1
}
return false
}
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 }
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)
}
i += 1
}
}
# ---- the sites and the key checks ----------------------------------------------------------------
function i18n_site(key: pointer, kind: int, file: pointer, line: int, col: int) -> bool {
if g_ks_seen == null { g_ks_seen = ss_new(1024) }
var f = file
if f == null { f = "" }
if not ss_add(g_ks_seen, `{f}:{itoa(line)}:{itoa(col)}:{key}`) { return false }
push(g_ks_key, key)
push(g_ks_kind, kind)
push(g_ks_file, f)
push(g_ks_line, line)
push(g_ks_col, col)
return true
}
function i18n_ks_name(kind: int) -> pointer {
if kind == KS_TEMPLATE { return "template" }
if kind == KS_DATA { return "data" }
return "code"
}
# 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 g_i18n_on and kind != KS_TEMPLATE and not g_i18n_nopo_warned {
g_i18n_nopo_warned = true
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
}
let i = po_find(g_i18n_en, key)
var lit = `k"{key}"`
if plural { lit = `kn"{key}"` }
if kind == KS_TEMPLATE { lit = `t('{key}')` }
if g_i18n_what != null { lit = g_i18n_what }
if i < 0 {
perr_at(file, line, col, `{lit}: the key {key} is not in {g_i18n_en_path}`)
return
}
if plural and not g_i18n_en.plural[i] {
perr_at(file, line, col, `{lit} is a plural key, and {g_i18n_en_path}:{itoa(g_i18n_en.line[i])} gives {key} no msgid_plural`)
}
}
# trf / trn / t(): as many values as en.po's English has holes ({1} is trn's count)
function i18n_holes(key: pointer, given: int, what: pointer, file: pointer, line: int, col: int) -> void {
let i = po_find(g_i18n_en, key)
if i < 0 { return }
let want = po_max_hole(g_i18n_en.strs[i])
if want == given { return }
pwarn_at(file, line, col, `{what}: {key} is given {itoa(given)} value(s), and its English in {g_i18n_en_path} has {itoa(want)} hole(s)`)
}
# words a player would read: two letters together, or any byte of a non-ASCII character
function i18n_wordy(s: pointer) -> bool {
if s == null { return false }
var run = 0
var i = 0
while i < len(s) {
let c = s[i]
if c < 0 or c >= 128 { return true }
if (c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z') { run += 1 } else { run = 0 }
if run >= 2 { return true }
i += 1
}
return false
}
function i18n_short(s: pointer) -> pointer {
var t = s
if len(t) > 40 { t = t[0 .. 37] + "..." }
let b = buf_new()
var i = 0
while i < len(t) {
if t[i] == '\n' { buf_puts(b, " ") } else { buf_putc(b, t[i]) }
i += 1
}
return buf_str(b)
}
function i18n_english_left(file: pointer, line: int, col: int, msg: pointer) -> void {
g_i18n_english_left += 1
pwarn_at(file, line, col, `English left: {msg}`)
}
# ---- the whole check -----------------------------------------------------------------------------
function i18n_check() -> void {
if g_i18n_done { return }
g_i18n_done = true
if g_i18n_on { i18n_walk_data() } # first: a def's key literal is a data site
var i = 0
while i < len(prog) {
if not is_runtime_file(prog[i].file) { i18n_walk(prog[i]) }
i += 1
}
if not g_i18n_on { return }
i18n_walk_templates() # i18n_xml.ludic
i18n_split_check()
}
function i18n_is_data_file(f: pointer) -> bool {
if f == null { return false }
let n = len(f)
return n > 5 and (f[n - 5 .. n] == ".lres")
}
function i18n_walk(n: Node) -> void {
if n == null { return }
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 { i18n_call(n) }
i18n_walk(n.a)
i18n_walk(n.b)
i18n_walk(n.c)
var i = 0
while i < len(n.kids) {
i18n_walk(n.kids[i])
i += 1
}
}
# 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 == "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]
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
function i18n_col(n: Node) -> int {
if n.pos < 0 or n.file == null { return n.col }
let src = fm_get(g_fm_src, n.file)
if src == null or len(src) <= n.pos { return n.col }
var a = n.pos
while a > 0 and src[a - 1] != '\n' { a -= 1 }
return n.pos - a + 1
}
# ---- the data: every compiled registry's rows ----------------------------------------------------
function i18n_walk_data() -> void {
var d = 0
while d < len(g_df_reg) {
let r = reg_find(g_df_reg[d])
if r >= 0 and not pm_is(r) {
let comp = find_comp(g_rg_type[r])
if comp != null { i18n_data_rec(g_df_rec[d], comp, `{i18n_prefix(r)}.{g_df_key[d]}`) }
}
d += 1
}
}
# a @PerMap registry's rows in one map, once --check has read them (permap_check.ludic)
function i18n_permap_rows(md: pointer) -> void {
if not g_i18n_on { return }
var m = md
var cut = len(m)
while cut > 0 and is_sep(m[cut - 1]) { cut -= 1 }
m = m[0 .. cut]
var s = cut
while s > 0 and not is_sep(m[s - 1]) { s -= 1 }
let map = m[s .. cut]
var e = 0
while e < len(g_pmk_rec) {
let r = g_pmk_reg[e]
let comp = find_comp(g_rg_type[r])
if comp != null { i18n_data_rec(g_pmk_rec[e], comp, `maps.{map}.{i18n_prefix(r)}.{g_pmk_key[e]}`) }
e += 1
}
}
function i18n_field_init(rec: Node, name: pointer) -> Node {
var i = 0
while i < len(rec.kids) {
let fi = rec.kids[i]
if fi.kind == E_FINIT and (fi.s == name) { return fi }
i += 1
}
return null
}
function i18n_data_rec(rec: Node, comp: Node, base: pointer) -> 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 {
let fi = i18n_field_init(rec, f.s)
let key = `{base}.{f.s}`
let text = i18n_has_text(f)
if fi != null { i18n_data_val(fi.a, f.ty, key, text) }
else if text and i18n_is_key_ty(f.ty) {
# a map's row: its key is derived and must be in en.po (the fill is compiled registries')
g_i18n_what = `{key} (the derived key of a @Text field the row gives no value)`
i18n_use(key, false, KS_DATA, rec.file, rec.line, rec.col)
g_i18n_what = null
}
}
i += 1
}
}
function i18n_data_val(e: Node, ty: pointer, key: pointer, text: bool) -> void {
if e == null { return }
if i18n_is_key(e) {
if e.ty != null and (e.ty == "derived") { g_i18n_what = `{key} (the derived key of a @Text field the row gives no value)` }
i18n_use(i18n_key_text(e), e.ival == KEY_PLURAL, KS_DATA, e.file, e.line, e.col)
g_i18n_what = null
return
}
if e.kind == E_STR {
if text and i18n_wordy(e.s) {
let dummy = i18n_site(key, KS_DATA, e.file, e.line, e.col)
i18n_english_left(e.file, e.line, e.col, `{key} is "{i18n_short(e.s)}" in the data - its English goes to en.po under {key}`)
}
return
}
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_data_val(e.kids[i], el, `{key}.{itoa(i)}`, text)
i += 1
}
return
}
if e.kind == E_NEW and e.a != null and e.a.kind == E_REC {
i18n_data_rec(e.a, find_comp(e.s), key)
return
}
if e.kind == E_REC and ty != null { i18n_data_rec(e, find_comp(ty), key) }
}
# ---- one English, several keys -------------------------------------------------------------------
# the English a key was split into wants a `#.` on each key, so a translator can choose other words
function i18n_split_check() -> void {
let p = g_i18n_en
if p == null { return }
let xs = new []int
let keys = new []pointer
var i = 0
while i < len(p.ids) {
push(xs, i)
push(keys, p.first[i])
i += 1
}
sc_sort(xs, keys)
var a = 0
while a < len(xs) {
var b = a + 1
while b < len(xs) and (p.first[xs[b]] == p.first[xs[a]]) { b += 1 }
if b - a > 1 and len(p.first[xs[a]]) > 0 {
var names: pointer = ""
var k = a
while k < b {
if k > a { names = names + ", " }
names = names + p.ids[xs[k]]
k += 1
}
k = a
while k < b {
let j = xs[k]
if p.desc[j] == null { pwarn_at(p.path, p.line[j], 1, `{p.ids[j]} has no #. description, and its English "{i18n_short(p.first[j])}" is split into {itoa(b - a)} keys ({names}) - say where it appears, so a translator can tell them apart`) }
k += 1
}
}
a = b
}
}

View file

@ -0,0 +1,252 @@
# i18n_po.ludic — a gettext .po file, read for the key checks (i18n.ludic). What is kept of each
# entry: its msgid (a key, since phase 26), whether it has a msgid_plural, its msgstr (or every
# msgstr[n], joined by a newline, so a hole in any form is seen), the first form alone (the English a
# split key is found by), the `#, fuzzy` flag, the `#.` extracted comments (the translator's
# description, lines joined by a newline) and the line its msgid is written on. msgctxt is read and
# kept but keys nothing; `#~` obsolete entries, `#:` references, `#|` previous ids and translator
# comments are skipped; a string may run over several "..." lines. The header (msgid "") is left out.
property PoFile {
path: pointer = null
ids: []pointer = null
plural: []bool = null
strs: []pointer = null # every form, joined
first: []pointer = null # msgstr, or msgstr[0]
fuzzy: []bool = null
desc: []pointer = null # the #. lines, or null
ctxt: []pointer = null
line: []int = null
slots: []int = null # the index by msgid: open addressing, -1 empty
}
# the entry being read
var g_po_cid: pointer = null
var g_po_cpl: bool = false
var g_po_cstr: []pointer = new []pointer
var g_po_cfuzzy: bool = false
var g_po_cdesc: pointer = null
var g_po_cctxt: pointer = null
var g_po_cline: int = 0
var g_po_field: int = 0 # what a continuation line adds to: 1 msgid, 2 plural, 3 msgstr (form g_po_form), 4 msgctxt
var g_po_form: int = 0
var g_po_seen_str: bool = false
function po_new(path: pointer) -> PoFile {
let p = new PoFile
p.path = path
p.ids = new []pointer
p.plural = new []bool
p.strs = new []pointer
p.first = new []pointer
p.fuzzy = new []bool
p.desc = new []pointer
p.ctxt = new []pointer
p.line = new []int
p.slots = new []int
return p
}
function po_reset_cur() -> void {
g_po_cid = null
g_po_cpl = false
g_po_cstr = new []pointer
g_po_cfuzzy = false
g_po_cdesc = null
g_po_cctxt = null
g_po_cline = 0
g_po_field = 0
g_po_form = 0
g_po_seen_str = false
}
# the entry read so far goes into the file (the header and an id-less fragment do not)
function po_flush(p: PoFile) -> void {
if g_po_cid != null and len(g_po_cid) > 0 {
var all: pointer = ""
var first: pointer = ""
var i = 0
while i < len(g_po_cstr) {
var f = g_po_cstr[i]
if f == null { f = "" }
if i == 0 { first = f }
if i > 0 { all = all + "\n" }
all = all + f
i += 1
}
push(p.ids, g_po_cid)
push(p.plural, g_po_cpl)
push(p.strs, all)
push(p.first, first)
push(p.fuzzy, g_po_cfuzzy)
push(p.desc, g_po_cdesc)
push(p.ctxt, g_po_cctxt)
push(p.line, g_po_cline)
}
po_reset_cur()
}
# the text of the "..." starting at or after s[i], its escapes read
function po_quoted(s: pointer, i0: int) -> pointer {
var i = i0
let n = len(s)
while i < n and s[i] != '"' { i += 1 }
if i >= n { return "" }
i += 1
let b = buf_new()
while i < n and s[i] != '"' {
var c = s[i]
if c == CH_BACKSLASH and i + 1 < n {
i += 1
c = unescape(s[i])
}
buf_putc(b, c)
i += 1
}
return buf_str(b)
}
function po_add_to(field: int, text: pointer) -> void {
if field == 1 { g_po_cid = g_po_cid + text }
else if field == 4 { g_po_cctxt = g_po_cctxt + text }
else if field == 3 {
while len(g_po_cstr) <= g_po_form { push(g_po_cstr, "") }
g_po_cstr[g_po_form] = g_po_cstr[g_po_form] + text
}
}
# a keyword line starts a new entry when the last one already had its msgstr
function po_maybe_new(p: PoFile) -> void {
if g_po_seen_str { po_flush(p) }
}
function po_read(path: pointer) -> PoFile {
let src = read_file(path)
if src == null { return null }
let p = po_new(path)
po_reset_cur()
var i = 0
var ln = 1
let n = len(src)
while i < n {
var e = i
while e < n and src[e] != '\n' { e += 1 }
var le = e
if le > i and src[le - 1] == 13 { le -= 1 }
var a = i
while a < le and (src[a] == ' ' or src[a] == 9) { a += 1 }
let line = src[a .. le]
po_line(p, line, ln)
i = e + 1
ln += 1
}
po_flush(p)
po_index(p)
return p
}
function po_line(p: PoFile, line: pointer, ln: int) -> void {
let n = len(line)
if n == 0 {
po_flush(p)
return
}
if line[0] == '#' {
if n > 1 and line[1] == '~' { return } # obsolete
po_maybe_new(p)
if n > 1 and line[1] == '.' {
var t = 2
if t < n and line[t] == ' ' { t += 1 }
let d = line[t .. n]
if g_po_cdesc == null { g_po_cdesc = d } else { g_po_cdesc = g_po_cdesc + "\n" + d }
}
if n > 1 and line[1] == ',' and has_sub(line, "fuzzy") { g_po_cfuzzy = true }
return
}
if line[0] == '"' {
po_add_to(g_po_field, po_quoted(line, 0))
return
}
if str_starts(line, "msgctxt") {
po_maybe_new(p)
g_po_cctxt = po_quoted(line, 7)
g_po_field = 4
return
}
if str_starts(line, "msgid_plural") {
g_po_cpl = true
g_po_field = 2
return
}
if str_starts(line, "msgid") {
po_maybe_new(p)
g_po_cid = po_quoted(line, 5)
g_po_cline = ln
g_po_field = 1
return
}
if str_starts(line, "msgstr") {
g_po_seen_str = true
g_po_form = 0
if n > 6 and line[6] == '[' {
var k = 7
var v = 0
while k < n and char_is_digit(line[k]) { v = v * 10 + (line[k] - 48); k += 1 }
g_po_form = v
}
g_po_field = 3
po_add_to(3, po_quoted(line, 6))
}
}
# ---- the index by msgid --------------------------------------------------------------------------
function po_hash(s: pointer) -> int {
var h = 5381
var i = 0
let n = len(s)
while i < n {
h = (h * 33 + s[i]) & 16777215
i += 1
}
return h
}
function po_index(p: PoFile) -> void {
var cap = 64
while cap < len(p.ids) * 2 + 1 { cap = cap * 2 }
p.slots = new []int
var i = 0
while i < cap {
push(p.slots, -1)
i += 1
}
i = 0
while i < len(p.ids) {
var h = po_hash(p.ids[i]) & (cap - 1)
var dup = false
while p.slots[h] >= 0 and not dup {
if (p.ids[p.slots[h]] == p.ids[i]) { dup = true } else { h = (h + 1) & (cap - 1) }
}
if not dup { p.slots[h] = i } # the first entry of an id is the one read
i += 1
}
}
# the entry for key, or -1
function po_find(p: PoFile, key: pointer) -> int {
if p == null or key == null { return -1 }
let cap = len(p.slots)
if cap == 0 { return -1 }
var h = po_hash(key) & (cap - 1)
while p.slots[h] >= 0 {
if (p.ids[p.slots[h]] == key) { return p.slots[h] }
h = (h + 1) & (cap - 1)
}
return -1
}
# the highest hole {n} in a text, 0 when it has none ({{ is a brace, not a hole)
function po_max_hole(s: pointer) -> int {
var best = 0
var i = 0
let n = len(s)
while i < n {
if s[i] == '{' and i + 1 < n and s[i + 1] == '{' { i += 2; continue }
if s[i] == '{' {
var k = i + 1
var v = 0
while k < n and char_is_digit(s[k]) { v = v * 10 + (s[k] - 48); k += 1 }
if k > i + 1 and k < n and s[k] == '}' and v > best { best = v }
}
i += 1
}
return best
}

View file

@ -0,0 +1,326 @@
# i18n_xml.ludic — a component's template (NAME.xml) against en.po (i18n.ludic). The compiler reads
# the template's text, so it checks it: in every {hole} - an element's text or an attribute's value -
# `t('key', ...)` with a literal key names a key en.po has, and gives as many values after it as the
# English has holes; `t(expr)` with a key worked out at run time is left alone. What a player would
# read that is not a key is "English left" (a warning, counted): an element's words outside its
# holes, a text attribute's words (text, label, hint, title, caption, and any other attribute whose
# value is not a keyword - as tools/i18n/extract.py decided), and a quoted choice inside a hole that
# reads as words (a capital or a space) and is not a key. An element with translate="no" and all it
# holds are left alone. Comments, <? ?> and <! > are skipped; `&name;` is not a word.
var g_xk_src: pointer = ""
var g_xk_file: pointer = ""
var g_xk_i: int = 0
var g_xk_n: int = 0
function i18n_walk_templates() -> void {
var c = 0
while c < len(g_cm_name) {
let xml = g_cm_xml[c]
if xml != null and len(xml) > 0 and not is_runtime_file(g_cm_file[c]) { i18n_xml(g_cm_xml_path[c], xml) }
c += 1
}
}
# the cursor on to `to`
function xk_move(to: int) -> void {
g_xk_i = to
if g_xk_i > g_xk_n { g_xk_i = g_xk_n }
}
var g_xk_at_line: int = 0
var g_xk_at_col: int = 0
function xk_find(s: pointer, from: int, pat: pointer) -> int {
let n = len(s)
let m = len(pat)
var i = from
while i + m <= n {
if s[i .. i + m] == pat { return i }
i += 1
}
return n
}
function i18n_xml(path: pointer, src: pointer) -> void {
g_xk_src = src
g_xk_file = path
g_xk_i = 0
g_xk_n = len(src)
let names = new []pointer
var no_depth = -1 # the depth an untranslated element opened at
while g_xk_i < g_xk_n {
let c = src[g_xk_i]
if c == '<' {
if g_xk_i + 3 < g_xk_n and (src[g_xk_i + 1 .. g_xk_i + 4] == "!--") {
xk_move(xk_find(src, g_xk_i + 4, "-->") + 3)
continue
}
if g_xk_i + 1 < g_xk_n and (src[g_xk_i + 1] == '?' or src[g_xk_i + 1] == '!') {
xk_move(xk_find(src, g_xk_i, ">") + 1)
continue
}
if g_xk_i + 1 < g_xk_n and src[g_xk_i + 1] == '/' {
xk_move(xk_find(src, g_xk_i, ">") + 1)
if len(names) > 0 { let gone = List.pop(names) }
if no_depth >= 0 and len(names) <= no_depth { no_depth = -1 }
continue
}
let depth = len(names)
let r = xk_tag(no_depth >= 0)
if r > 0 and no_depth < 0 and r == 2 { no_depth = depth }
if g_xk_last_open { push(names, g_xk_last_name) }
else if no_depth == depth { no_depth = -1 } # a self-closed translate="no" holds nothing
continue
}
xk_text(no_depth >= 0)
}
}
# text up to the next tag: its holes checked, its words outside them English left
function xk_text(quiet: bool) -> void {
let src = g_xk_src
var words_at = -1
var run = 0
while g_xk_i < g_xk_n and src[g_xk_i] != '<' {
let c = src[g_xk_i]
if c == '{' {
let e = hole_end(src, g_xk_i + 1, g_xk_n)
xk_hole(g_xk_i + 1, e, quiet)
xk_move(e + 1)
run = 0
continue
}
if c == '&' {
var j = g_xk_i + 1
while j < g_xk_n and j < g_xk_i + 12 and src[j] != ';' and src[j] != '<' and src[j] != ' ' { j += 1 }
if j < g_xk_n and src[j] == ';' {
xk_move(j + 1)
run = 0
continue
}
}
if words_at < 0 {
if c < 0 or c >= 128 { words_at = g_xk_i }
if (c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z') { run += 1 } else { run = 0 }
if run >= 2 { words_at = g_xk_i - 1 }
}
xk_move(g_xk_i + 1)
}
if words_at >= 0 and not quiet {
let a = xk_trim_start(words_at)
xk_where_back(a)
i18n_english_left(g_xk_file, g_xk_at_line, g_xk_at_col, `a template's words ("{i18n_short(xk_words_from(a))}") - write {{t('key')}} and give en.po the English, or mark the element translate="no"`)
}
}
# where a word starts: back from a letter to the space before it
function xk_trim_start(at: int) -> int {
var a = at
while a > 0 and g_xk_src[a - 1] != '>' and g_xk_src[a - 1] != '}' and g_xk_src[a - 1] != ' ' and g_xk_src[a - 1] != '\n' and g_xk_src[a - 1] != 9 { a -= 1 }
return a
}
function xk_words_from(a: int) -> pointer {
var e = a
while e < g_xk_n and g_xk_src[e] != '<' and g_xk_src[e] != '{' { e += 1 }
return g_xk_src[a .. e]
}
# the line and column of a byte already passed
function xk_where_back(at: int) -> void {
var ln = 1
var ls = 0
var i = 0
while i < at {
if g_xk_src[i] == '\n' {
ln += 1
ls = i + 1
}
i += 1
}
g_xk_at_line = ln
g_xk_at_col = at - ls + 1
}
# ---- a tag ---------------------------------------------------------------------------------------
var g_xk_last_open: bool = false # the tag read opens an element (it is not <x ... />)
var g_xk_last_name: pointer = ""
function xk_name_ch(c: int) -> bool { return char_is_alnum(c) or c == '_' or c == '-' or c == ':' or c == '.' or c == '@' }
function xk_space(c: int) -> bool { return c == ' ' or c == '\n' or c == 9 or c == 13 }
# `<name attr="v" ...>` from the cursor: 2 when it says translate="no", else 1
function xk_tag(quiet: bool) -> int {
let src = g_xk_src
var i = g_xk_i + 1
let a0 = i
while i < g_xk_n and xk_name_ch(src[i]) { i += 1 }
g_xk_last_name = src[a0 .. i]
g_xk_last_open = true
let an = new []pointer
let va = new []int
let vb = new []int
var no = false
while i < g_xk_n {
while i < g_xk_n and xk_space(src[i]) { i += 1 }
if i >= g_xk_n { break }
if src[i] == '>' {
i += 1
break
}
if src[i] == '/' {
g_xk_last_open = false
i += 1
continue
}
let na = i
while i < g_xk_n and not xk_space(src[i]) and src[i] != '=' and src[i] != '>' and src[i] != '/' { i += 1 }
if i == na { i += 1; continue }
let name = src[na .. i]
while i < g_xk_n and xk_space(src[i]) { i += 1 }
var a = i
var b = i
if i < g_xk_n and src[i] == '=' {
i += 1
while i < g_xk_n and xk_space(src[i]) { i += 1 }
if i < g_xk_n and (src[i] == '"' or src[i] == CH_SQUOTE) {
let q = src[i]
i += 1
a = i
while i < g_xk_n and src[i] != q {
if src[i] == '{' { i = hole_end(src, i + 1, g_xk_n) }
i += 1
}
b = i
if i < g_xk_n { i += 1 }
} else {
a = i
while i < g_xk_n and not xk_space(src[i]) and src[i] != '>' { i += 1 }
b = i
}
}
push(an, name)
push(va, a)
push(vb, b)
if (name == "translate") and (src[a .. b] == "no") { no = true }
}
var k = 0
while k < len(an) {
xk_attr(an[k], va[k], vb[k], quiet or no)
k += 1
}
g_xk_i = i
if no { return 2 }
return 1
}
function xk_not_text(n: pointer) -> bool {
if (n == "class") or (n == "id") or (n == "src") or (n == "style") or (n == "icon") or (n == "name") or (n == "type") { return true }
if (n == "key") or (n == "as") or (n == "in") or (n == "test") or (n == "index") or (n == "slot") or (n == "export") { return true }
if (n == "color") or (n == "bg") or (n == "background") or (n == "state") or (n == "min") or (n == "max") or (n == "width") { return true }
if (n == "height") or (n == "w") or (n == "h") or (n == "shade") or (n == "rows") or (n == "maxlength") or (n == "checked") { return true }
if (n == "enabled") or (n == "disabled") or (n == "hidden") or (n == "primary") or (n == "selected") or (n == "kind") { return true }
if (n == "translate") { return true }
return str_starts(n, "on") or str_starts(n, "--")
}
function xk_words_attr(n: pointer) -> bool {
return (n == "text") or (n == "label") or (n == "hint") or (n == "title") or (n == "caption")
}
# a keyword: nothing but a-z 0-9 _ . # % -
function xk_keyword(s: pointer) -> bool {
var i = 0
while i < len(s) {
let c = s[i]
let ok = (c >= 'a' and c <= 'z') or (c >= '0' and c <= '9') or c == '_' or c == '.' or c == '#' or c == '%' or c == '-'
if not ok { return false }
i += 1
}
return true
}
# one attribute's value, src[a..b]: its holes, and its words if it is text
function xk_attr(name: pointer, a: int, b: int, quiet: bool) -> void {
let src = g_xk_src
let textual = not xk_not_text(name)
let rest = buf_new()
var i = a
while i < b {
if src[i] == '{' {
let e = hole_end(src, i + 1, b)
xk_hole(i + 1, e, quiet or not textual)
i = e + 1
continue
}
buf_putc(rest, src[i])
i += 1
}
if quiet or not textual { return }
let words = buf_str(rest)
if not xk_words_attr(name) and xk_keyword(words) { return }
if not i18n_wordy(words) { return }
xk_where_back(a)
i18n_english_left(g_xk_file, g_xk_at_line, g_xk_at_col, `{name}="{i18n_short(words)}" in a template - write {name}="{{t('key')}}" and give en.po the English`)
}
# ---- a hole --------------------------------------------------------------------------------------
# the code of a hole, src[a..e]: each t('key', ...) checked; a quoted choice that reads as words is
# English left unless the text around it is quiet
function xk_hole(a: int, e: int, quiet: bool) -> void {
let src = g_xk_src
var i = a
while i < e {
let c = src[i]
if c == '"' or c == CH_SQUOTE {
var j = i + 1
while j < e and src[j] != c {
if src[j] == CH_BACKSLASH { j += 1 }
j += 1
}
let lit = src[i + 1 .. j]
if not quiet and xk_choice_wordy(lit) {
xk_where_back(i)
i18n_english_left(g_xk_file, g_xk_at_line, g_xk_at_col, `'{i18n_short(lit)}' in a template's hole - a choice the player reads is a key too: t('key')`)
}
i = j + 1
continue
}
if c == 't' and i + 1 < e and src[i + 1] == '(' and (i == a or not (xk_name_ch(src[i - 1]))) {
i = xk_t_call(i + 2, e)
continue
}
i += 1
}
}
function xk_choice_wordy(s: pointer) -> bool {
if not i18n_wordy(s) { return false }
var i = 0
while i < len(s) {
let c = s[i]
if (c >= 'A' and c <= 'Z') or c == ' ' or c < 0 or c >= 128 { return true }
i += 1
}
return false
}
# t( read, the cursor after it: a literal key is checked, and the values after it counted; where
# the scan goes on (just past the key, so a t() inside the values is found as well)
function xk_t_call(at: int, e: int) -> int {
let src = g_xk_src
var k = at
while k < e and xk_space(src[k]) { k += 1 }
if k >= e or (src[k] != CH_SQUOTE and src[k] != '"') { return at }
let q = src[k]
var j = k + 1
while j < e and src[j] != q { j += 1 }
let key = src[k + 1 .. j]
var depth = 0
var commas = 0
var m = j + 1
while m < e {
let c = src[m]
if c == '"' or c == CH_SQUOTE {
m += 1
while m < e and src[m] != c {
if src[m] == CH_BACKSLASH { m += 1 }
m += 1
}
} else if c == '(' or c == '[' or c == '{' { depth += 1 }
else if c == ')' or c == ']' or c == '}' {
if depth == 0 { break }
depth -= 1
}
else if c == ',' and depth == 0 { commas += 1 }
m += 1
}
xk_where_back(k)
i18n_use(key, false, KS_TEMPLATE, g_xk_file, g_xk_at_line, g_xk_at_col)
if g_i18n_en != null { i18n_holes(key, commas, "t()", g_xk_file, g_xk_at_line, g_xk_at_col) }
return j + 1
}

View file

@ -10,6 +10,7 @@ const TK_NL: int = 4
const TK_EOF: int = 5
const TK_FLOAT: int = 6
const TK_INTERP: int = 7 # `text {expr} text` — raw content, split by the parser
const TK_KEY: int = 8 # k"pause.resume" / kn"catch.count" — a text key (i18n.ludic); ival 1 or 2
property Tok { kind: int = 0, text: pointer = null, ival: int = 0, line: int = 0, pos: int = -1, end: int = -1, col: int = 0, off: int = -1, oend: int = -1 }
# col: the token's column (1-based) on its line; off / oend: where it starts and ends in the text
@ -244,6 +245,23 @@ function lex_at(src: pointer, first_line: int) -> void {
if char_is_alpha(c) {
let start = i
while i < n and char_is_alnum(src[i]) { i += 1 }
# `k"..."` / `kn"..."`: a key literal, the name written against the quote (`k "x"` is a name
# and a string, as it always was)
let kw = i - start
if i < n and src[i] == '"' and ((kw == 1 and c == 'k') or (kw == 2 and c == 'k' and src[start + 1] == 'n')) {
i += 1
let kout = bytes(n)
var kj = 0
while i < n and src[i] != '"' and src[i] != '\n' {
if src[i] == CH_BACKSLASH { kout[kj] = unescape(src[i + 1]); kj += 1; i += 2 }
else { kout[kj] = src[i]; kj += 1; i += 1 }
}
if i >= n or src[i] != '"' { lex_fail(line, "a key literal is never closed with \" on its line") }
i += 1
kout[kj] = 0
tok_push(TK_KEY, kout, kw, line)
continue
}
tok_push(TK_ID, src[start..i], 0, line)
continue
}

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
@ -334,6 +335,7 @@ function p_primary() -> Node {
if t.kind == TK_INT { let n = node(E_INT); n.ival = t.ival; n.s = t.text; pi += 1; return n } # s: a long literal's digits
if t.kind == TK_FLOAT { let n = node(E_FLOAT); n.ival = t.ival; n.s = t.text; pi += 1; return n }
if t.kind == TK_STR { let n = node(E_STR); n.s = t.text; pi += 1; return n }
if t.kind == TK_KEY { pi += 1; return key_lit_node(t) } # k"..." / kn"..." (i18n.ludic)
if t.kind == TK_ID {
if (t.text == "true") { let n = node(E_BOOL); n.ival = 1; pi += 1; return n }
if (t.text == "false") { let n = node(E_BOOL); n.ival = 0; pi += 1; return n }
@ -1012,6 +1014,7 @@ function parse_one_decl() -> void {
g_pm_pending = true
}
else if a == "Chunked" { pm_parse_chunked() } # @Chunked(64): read a chunk at a time
else if a == "TextKey" { i18n_parse_textkey() } # @TextKey("steps"): the registry's text keys (i18n.ludic)
else if a == "alloc_ok" { # 25.2: @alloc_ok("why") - this allocates in a frame on purpose
eat_op("(")
let why = expr()

View file

@ -139,6 +139,7 @@ function pm_kind(ty: pointer) -> int {
if (ty == "float") { return PM_FLOAT }
if (ty == "bool") { return PM_BOOL }
if (ty == "string") { return PM_STR }
if i18n_is_key_ty(ty) { return PM_STR } # a Key is text at run time: k"..." in a map's row
if str_starts(ty, "fn(") { return PM_FN }
if is_slice_ty(ty) { return PM_LIST }
let c = find_comp(ty)

View file

@ -220,6 +220,7 @@ function pmk_map(md: pointer) -> void {
}
pmk_refs()
pmk_game_assets()
i18n_permap_rows(md) # i18n.ludic: its text keys and English left
}
# a key written twice in one file
function pmk_dups() -> void {
@ -321,6 +322,7 @@ function pmk_rec(rec: Node, comp: Node, row: bool) -> void {
function pmk_desc(e: Node) -> pointer {
if e.kind == E_INT or (e.kind == E_UN and e.a != null and e.a.kind == E_INT) { return "an int" }
if e.kind == E_FLOAT or (e.kind == E_UN and e.a != null and e.a.kind == E_FLOAT) { return "a float" }
if i18n_is_key(e) { return "a key" }
if e.kind == E_STR { return "a string" }
if e.kind == E_BOOL { return "true or false" }
if e.kind == E_ID { return `the name {e.s}` }
@ -376,8 +378,12 @@ function pmk_value(e: Node, f: Node, ty: pointer, at: Node) -> void {
if e.kind != E_BOOL { pmk_want(e, f, "true or false") }
return
}
if k == PM_STR and i18n_is_key_ty(ty) {
if not i18n_is_key(e) { pmk_want(e, f, "a key k\"...\"") }
return
}
if k == PM_STR {
if e.kind != E_STR { pmk_want(e, f, "a \"string\""); return }
if e.kind != E_STR or i18n_is_key(e) { pmk_want(e, f, "a \"string\""); return }
pmk_attrs_str(e, f)
return
}

View file

@ -233,6 +233,10 @@ function pm_gen_field(b: Buf, sn: pointer, fd: Node, i: int) -> void {
let k = pm_kind(fd.ty)
let x = fd.s
let n = itoa(i)
if k == PM_STR and i18n_is_key_ty(fd.ty) {
pm_l(b, ` r.{x} = lres_key(t, c)`)
return
}
if k == PM_INT or k == PM_FLOAT or k == PM_BOOL or k == PM_STR {
pm_l(b, ` r.{x} = {pm_scalar_read(k)}(t, c)`)
return
@ -256,6 +260,8 @@ function pm_gen_field(b: Buf, sn: pointer, fd: Node, i: int) -> void {
pm_l(b, ` {sn}__reset_{el}(pl, e{n})`)
pm_l(b, ` if lres_is_rec(t, k{n}) {{ {sn}__fill_{el}(t, pl, e{n}, k{n}) }}`)
pm_l(b, ` push(r.{x}, e{n})`)
} else if i18n_is_key_ty(el) {
pm_l(b, ` push(r.{x}, lres_key(t, k{n}))`)
} else {
pm_l(b, ` push(r.{x}, {pm_scalar_read(ek)}(t, k{n}))`)
}

View file

@ -92,6 +92,7 @@ function parse_registry() -> void {
push(g_rg_var, v)
push(g_rg_open, 0)
push(g_rg_from, from)
i18n_registry_declared() # i18n.ludic: its @TextKey, before the attributes are taken
push(g_rg_at, at_registry_take()) # attrs.ludic
pm_declared(pm, chunk, v.s)
if from != null and not pm { res_read(v, from) }

View file

@ -61,6 +61,7 @@ function reg_fill(r: int) -> void {
push(rec.kids, fi)
}
if comp != null { res_type_fields(rec, comp) } # L9: nested records take their field's type
i18n_fill_row(r, key, rec, comp, g_df_kline[d], g_df_kcol[d]) # a @Text Key left out: its derived key
let nw = node(E_NEW)
nw.s = t
nw.a = rec

View file

@ -6,6 +6,7 @@ function vw_conv(v: int, ty: pointer, x: pointer, head: Node) -> pointer {
if (ty == "int") { return `value_int({x})` }
if (ty == "float") { return `value_float({x})` }
if (ty == "string") { return `value_str({x})` }
if i18n_is_key_ty(ty) { return `value_str(string({x}))` } # a Key: its marked text, for a template's t(expr)
if (ty == "bool") {
vw_helper(`function view_val_bool(b: bool) -> Val {{\n if b {{ return value_bool(1) }}\n return value_bool(0)\n}}\n`, "view_val_bool", head)
return `view_val_bool({x})`

View file

@ -6,6 +6,7 @@ function vw_set(v: int, ty: pointer, o: pointer, k: pointer, x: pointer, head: N
if (ty == "int") { return ` value_set_int({o}, "{k}", {x})\n` }
if (ty == "float") { return ` value_set_float({o}, "{k}", {x})\n` }
if (ty == "string") { return ` value_set_str({o}, "{k}", {x})\n` }
if i18n_is_key_ty(ty) { return ` value_set_str({o}, "{k}", string({x}))\n` } # a Key: its marked text, for t(expr)
if (ty == "bool") { return ` value_set_bool({o}, "{k}", {x})\n` }
if (ty == "[]int") { return ` value_set_ints({o}, "{k}", {x})\n` }
if (ty == "[]float") { return ` value_set_floats({o}, "{k}", {x})\n` }

View file

@ -163,6 +163,7 @@ function vocab_types() -> void {
vc_ty("bool", "primitive", "true or false.")
vc_ty("byte", "primitive", "An unsigned 8-bit value.")
vc_ty("string", "primitive", "Text, UTF-8.")
vc_ty("Key", "primitive", "A text key, k\"module.purpose\" (kn\"...\" a plural): names a text, is not one; tr(key) makes the text.")
vc_ty("pointer", "primitive", "An untyped address (raw memory needs unsafe).")
vc_ty("void", "primitive", "No value: a function that returns nothing.")
vc_ty("entity", "primitive", "A handle to an entity.")
@ -203,7 +204,7 @@ function vocab_attributes() -> void {
vc_at("Material", "checked", "field", "(model_field)", "A material inside the glTF that field names.")
vc_at("Tint", "checked", "field", "(SLOT)", "A colour for that tint slot (a constant).")
vc_at("Derived", "checked", "field", "", "Worked out at boot: anything written in the data is overwritten.")
vc_at("Text", "checked", "field", "", "Read by the player, so translated.")
vc_at("Text", "checked", "field", "", "Read by the player, so translated: a Key field's key is derived, <registry>.<row>.<field>.")
vc_at("Multiline", "checked", "field", "", "Prose: edited as several lines.")
vc_at("Key", "checked", "field", "", "A key code.")
# a field's, with an effect
@ -217,6 +218,7 @@ function vocab_attributes() -> void {
vc_at("ByKey", "checked", "registry", "", "Entries are saved by key, so their order is free.")
vc_at("PerMap", "checked", "registry", "", "Its rows are read per map, from that map's directory, when the map loads.")
vc_at("Chunked", "checked", "registry", "(n)", "A @PerMap table read a chunk of n by n metres at a time.")
vc_at("TextKey", "checked", "registry", "(\"prefix\")", "The prefix of its @Text fields' derived keys, in place of the registry's name in snake case.")
# a declaration's
vc_at("export", "checked", "declaration", "", "The declaration is exported (the same as the export keyword).")
vc_at("Public", "checked", "handler", "", "Promotes a lifecycle hook to an event other code may hear.")
@ -294,6 +296,8 @@ function vocab_literals() -> void {
vc("literal", "1.5", "decimal", "", "", "A fixed, or a float in a numbers float file.")
vc("literal", "'a'", "char", "", "", "A character's code, an int.")
vc("literal", "\"text\"", "string", "", "", "A string; \\n \\t \\r \\0 \\\\ \\\" are escapes.")
vc("literal", "k\"module.purpose\"", "key", "", "", "A Key, checked against the source language's .po; written against its quote.")
vc("literal", "kn\"module.purpose\"", "plural key", "", "", "A plural Key, for trn: its .po entry has a msgid_plural.")
vc("literal", "`a {expr} b`", "template", "", "", "A string with holes; {{ and }} are braces.")
vc("literal", "# ...", "comment", "", "", "A comment to the end of the line.")
}