compiler: text keys (phase 26.2) - k"pause.resume" / kn"catch.count" of the builtin type Key (not a string, and no string one; == compares), its run-time value the key after a marker byte (1, or 2 for a plural); package.ludic's lang "assets/lang" en and a gettext .po reader; every key literal checked against en.po (a kn"" one wants a msgid_plural), trf / trn and a template's t('key', ...) against the English's holes, a component's template words and a @Text row's English as "English left" (warnings, english_left in ludic deps), a @Text Key field a compiled row leaves out filled with its derived key <registry>.<row>.<field> (or @TextKey's prefix; maps.<map>.<table>.<row>.<field> for a @PerMap row, checked), one English under several undescribed keys warned at en.po's line; the schema's "lang" (keys and their sites, unused, undescribed, split, and every other language's missing / fuzzy / extra); k"" in .lres data, @PerMap rows included (lres_key); ludic-fmt keeps a key literal whole; reseeded, bootstrap-cfree fixpoint holds
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
82c652ca31
commit
cc1ea0f4ae
28 changed files with 139268 additions and 109746 deletions
326
selfhost/frontend/i18n_xml.ludic
Normal file
326
selfhost/frontend/i18n_xml.ludic
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue