diff --git a/examples/lang/i18n/data/i18n_items.lres b/examples/lang/i18n/data/i18n_items.lres new file mode 100644 index 00000000..9ff2f8e1 --- /dev/null +++ b/examples/lang/i18n/data/i18n_items.lres @@ -0,0 +1,3 @@ +# i18n.ludic's items: a @Text name is a Key, derived when the row gives none (items.rope.name) +rope { weight: 0.4 } +lamp { name: k"items.lamp.name", weight: 1.2 } diff --git a/examples/lang/i18n/i18n.ludic b/examples/lang/i18n/i18n.ludic new file mode 100644 index 00000000..342517c9 --- /dev/null +++ b/examples/lang/i18n/i18n.ludic @@ -0,0 +1,41 @@ +# i18n.ludic — keys, not English (LANGUAGE.md, "Text keys"). A text the player reads is named by a +# key, k"pause.resume", of the builtin type Key: not a string, and no string is one. Its English is +# lang/en.po (package.ludic says `lang "lang" en`), and the compiler checks every key against it. A +# registry's @Text Key field a row leaves out is filled with its derived key - .., +# or the registry's @TextKey prefix. At run time a Key is its text after a marker byte (1, or 2 for a +# plural kn"..."), so ludic.i18n's tr() is a cast; here tr, trf and trn are stand-ins that show the key. +# +# Running it prints: pause.resume yes no 13 12 hud.day(3) catch.count[2](trout) items.rope.name items.lamp.name steps.gather_wood.hint +program I18n { + property Item { + key: string = "" + @Text name: Key = null + weight: float = 0.0 + } + registry Items of Item as IT from "data/i18n_items.lres" + property StepKind { + key: string = "" + @Text hint: Key = null + } + @TextKey("steps") registry StepKinds of StepKind as SK + def StepKinds gather_wood { } + # ludic.i18n's three, stood in for: the key's text without its marker + function tr(key: Key) -> string { + let s = string(key) + return s[1..len(s)] + } + function trf(key: Key, a: string) -> string { return `{tr(key)}({a})` } + function trn(key: Key, n: int, a: string) -> string { return `{tr(key)}[{n}]({a})` } + function yn(b: bool) -> string { + if b { return "yes" } + return "no" + } + entry { + let resume = k"pause.resume" + let same = resume == k"pause.resume" + let other = resume == k"pause.close" + var out = `{tr(resume)} {yn(same)} {yn(other)} {len(string(resume))} {len(string(kn"catch.count"))} {trf(k"hud.day", "3")}` + out = out + ` {trn(kn"catch.count", 2, "trout")} {tr(Items[IT_ROPE].name)} {tr(Items[IT_LAMP].name)} {tr(StepKinds[SK_GATHER_WOOD].hint)}` + print(out) + } +} diff --git a/examples/lang/i18n/i18n_ui.ludic b/examples/lang/i18n/i18n_ui.ludic new file mode 100644 index 00000000..75b83950 --- /dev/null +++ b/examples/lang/i18n/i18n_ui.ludic @@ -0,0 +1,9 @@ +# i18n_ui.ludic — a component's template against lang/en.po: every t('key', ...) in a {hole} names +# a key en.po has, and gives as many values as its English has holes. Words a player would read that +# are not a key are "English left" (a warning, counted by ludic deps as english_left) - here the +# "Paused" line; an element with translate="no" is left alone. `ludicc --check` reports both. +import "ludic.ui" +program I18nUi { + import "parts/Pause.ludic" + entry { print(1) } +} diff --git a/examples/lang/i18n/lang/en.po b/examples/lang/i18n/lang/en.po new file mode 100644 index 00000000..dafcfdc4 --- /dev/null +++ b/examples/lang/i18n/lang/en.po @@ -0,0 +1,40 @@ +# The example's source language: the English of every key i18n.ludic and i18n_ui.ludic use. +msgid "" +msgstr "" +"Content-Type: text/plain; charset=UTF-8\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. On the pause menu: returns to play. +msgid "pause.resume" +msgstr "Resume" + +#. On the pause menu: closes the menu and returns to play. +msgid "pause.close" +msgstr "Close" + +#. On the map screen: closes the map. +msgid "map.close" +msgstr "Close" + +#. The day on the HUD. {1} = the day number +msgid "hud.day" +msgstr "Day {1}" + +#. A catch, counted. {1} = how many, {2} = the species +msgid "catch.count" +msgid_plural "catch.count" +msgstr[0] "{1} {2} caught" +msgstr[1] "{1} {2} " +"caught" + +#. An item's name: a coil of rope. +msgid "items.rope.name" +msgstr "Rope" + +#. An item's name: a brass lamp. +msgid "items.lamp.name" +msgstr "Lamp" + +#. What a chapter step says to do: gather firewood. +msgid "steps.gather_wood.hint" +msgstr "Gather wood" diff --git a/examples/lang/i18n/lang/tr.po b/examples/lang/i18n/lang/tr.po new file mode 100644 index 00000000..87c13b09 --- /dev/null +++ b/examples/lang/i18n/lang/tr.po @@ -0,0 +1,32 @@ +# Turkish: one key missing (hud.day), one fuzzy (pause.close) and one en.po does not have (old.title) +msgid "" +msgstr "" +"Content-Type: text/plain; charset=UTF-8\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "pause.resume" +msgstr "Devam et" + +#, fuzzy +msgid "pause.close" +msgstr "Kapat" + +msgid "map.close" +msgstr "Kapat" + +msgid "catch.count" +msgid_plural "catch.count" +msgstr[0] "{1} {2} tutuldu" +msgstr[1] "{1} {2} tutuldu" + +msgid "items.rope.name" +msgstr "Halat" + +msgid "items.lamp.name" +msgstr "Fener" + +msgid "steps.gather_wood.hint" +msgstr "Odun topla" + +msgid "old.title" +msgstr "Eski" diff --git a/examples/lang/i18n/package.ludic b/examples/lang/i18n/package.ludic new file mode 100644 index 00000000..716176a3 --- /dev/null +++ b/examples/lang/i18n/package.ludic @@ -0,0 +1,3 @@ +# the key-based text example (i18n.ludic, i18n_ui.ludic): its languages are lang/.po, and +# lang/en.po is the source every key is checked against +lang "lang" en diff --git a/examples/lang/i18n/parts/Pause.ludic b/examples/lang/i18n/parts/Pause.ludic new file mode 100644 index 00000000..a98eb90b --- /dev/null +++ b/examples/lang/i18n/parts/Pause.ludic @@ -0,0 +1,7 @@ +# Pause.ludic - i18n_ui.ludic's component: its text is keys (Pause.xml), one of them a field the +# template reads - a Key reaches a template as its marked text, for t(expr) +component Pause { + prop day: int = 1 + prop caught: int = 0 + back: Key = k"pause.close" +} diff --git a/examples/lang/i18n/parts/Pause.xml b/examples/lang/i18n/parts/Pause.xml new file mode 100644 index 00000000..8f143d00 --- /dev/null +++ b/examples/lang/i18n/parts/Pause.xml @@ -0,0 +1,8 @@ +
+

{t('pause.resume')}

+ + +

{t('catch.count', caught, 'trout')}

+

Maroon Lake 0.5 beta

+

Paused

+
diff --git a/examples/rejected/i18n_keys/data_missing.ludic b/examples/rejected/i18n_keys/data_missing.ludic new file mode 100644 index 00000000..1f139896 --- /dev/null +++ b/examples/rejected/i18n_keys/data_missing.ludic @@ -0,0 +1,12 @@ +# phase 26: a @Text Key field a row leaves out takes its derived key (things.axe.name), and en.po must +# have it; a @Text string field still holding English is a warning, counted as english_left +program DataMissing { + property Thing { + key: string = "" + @Text name: Key = null + @Text blurb: string = "" + } + registry Things of Thing as TH + def Things axe { blurb: "A sharp axe" } + entry { print(TH_COUNT) } +} diff --git a/examples/rejected/i18n_keys/key_missing.ludic b/examples/rejected/i18n_keys/key_missing.ludic new file mode 100644 index 00000000..6ee65b79 --- /dev/null +++ b/examples/rejected/i18n_keys/key_missing.ludic @@ -0,0 +1,11 @@ +# phase 26: a key literal en.po does not have is refused, and so is a plural key (kn"...") whose +# en.po entry has no msgid_plural; trf given more values than the English has holes is a warning +program KeyMissing { + function tr(key: Key) -> string { return string(key) } + function trf(key: Key, a: string, b: string) -> string { return string(key) + a + b } + entry { + print(tr(k"pause.gone")) + print(tr(kn"pause.resume")) + print(trf(k"hud.day", "1", "2")) + } +} diff --git a/examples/rejected/i18n_keys/key_mixup.ludic b/examples/rejected/i18n_keys/key_mixup.ludic new file mode 100644 index 00000000..41183557 --- /dev/null +++ b/examples/rejected/i18n_keys/key_mixup.ludic @@ -0,0 +1,11 @@ +# phase 26: a Key is not a string and a string is not a Key - text is made from a key, and a key is +# written k"..."; nor does a key join text with + +program KeyMixup { + function show(s: string) -> void { print(s) } + function name_of(k: Key) -> Key { return k } + entry { + show(k"pause.resume") + let k2 = name_of("Resume") + print(k2 + "!") + } +} diff --git a/examples/rejected/i18n_keys/lang/en.po b/examples/rejected/i18n_keys/lang/en.po new file mode 100644 index 00000000..5f659d55 --- /dev/null +++ b/examples/rejected/i18n_keys/lang/en.po @@ -0,0 +1,18 @@ +# The source language of the refused examples: two keys, and one English under two undescribed keys. +msgid "" +msgstr "" +"Content-Type: text/plain; charset=UTF-8\n" + +#. On the pause menu: returns to play. +msgid "pause.resume" +msgstr "Resume" + +#. The day on the HUD. {1} = the day number +msgid "hud.day" +msgstr "Day {1}" + +msgid "shop.close" +msgstr "Close" + +msgid "map.close" +msgstr "Close" diff --git a/examples/rejected/i18n_keys/package.ludic b/examples/rejected/i18n_keys/package.ludic new file mode 100644 index 00000000..e2404959 --- /dev/null +++ b/examples/rejected/i18n_keys/package.ludic @@ -0,0 +1,2 @@ +# the key checks' refusals (key_missing, t_missing, key_mixup, data_missing): lang/en.po is the source +lang "lang" en diff --git a/examples/rejected/i18n_keys/parts/Menu.ludic b/examples/rejected/i18n_keys/parts/Menu.ludic new file mode 100644 index 00000000..ce6260a7 --- /dev/null +++ b/examples/rejected/i18n_keys/parts/Menu.ludic @@ -0,0 +1,4 @@ +# Menu.ludic - t_missing.ludic's component: one key en.po lacks +component Menu { + prop day: int = 1 +} diff --git a/examples/rejected/i18n_keys/parts/Menu.xml b/examples/rejected/i18n_keys/parts/Menu.xml new file mode 100644 index 00000000..9dc527a4 --- /dev/null +++ b/examples/rejected/i18n_keys/parts/Menu.xml @@ -0,0 +1,5 @@ + diff --git a/examples/rejected/i18n_keys/t_missing.ludic b/examples/rejected/i18n_keys/t_missing.ludic new file mode 100644 index 00000000..29f3097d --- /dev/null +++ b/examples/rejected/i18n_keys/t_missing.ludic @@ -0,0 +1,7 @@ +# phase 26: a template's t('key') is checked against en.po like a key literal - refused when en.po lacks +# it; t('hud.day') with no value for its {1} is a warning +import "ludic.ui" +program TMissing { + import "parts/Menu.ludic" + entry { print(1) } +} diff --git a/tools/ludic-cli/test.ludic b/tools/ludic-cli/test.ludic index 9c3e41b4..294516e2 100644 --- a/tools/ludic-cli/test.ludic +++ b/tools/ludic-cli/test.ludic @@ -74,6 +74,18 @@ function permap_check_case() -> void { if not s_contains(capture(`cat {js}`), "the unit is spelled") or not s_contains(capture(`cat {js}`), "deg") { bad2(lbl, capture_line(`head -3 {js}`)); return } ok(lbl) } +# phase 26: a template checks clean against en.po, its one line of words a warning, and ludic deps +# counts it as english_left +function i18n_check_case() -> void { + let lbl = "--check: a component's template against en.po - clean, \"Paused\" English left (a warning), english_left 1 in ludic deps" + let js = `{tmp_dir()}/dj_i18n.json` + if not shq(`bin/ludicc examples/lang/i18n/i18n_ui.ludic --check --diagnostics=json > {js} 2>/dev/null`) { bad2(lbl, capture_line(`head -3 {js}`)); return } + let got = capture(`cat {js}`) + if not s_contains(got, "English left: a template's words") or not s_contains(got, "Paused") { bad2(lbl, capture_line(`head -3 {js}`)); return } + let dp = capture(`bin/ludic deps examples/lang/i18n/i18n_ui.ludic 2>&1`) + if not s_contains(dp, "english_left: 1") { bad2(lbl, `ludic deps said [{s_trim(dp)}]`); return } + ok(lbl) +} # --check --diagnostics=json: refused, with exactly `n` ERRORS on stdout (warnings beside them are not # counted), one of the diagnostics holding `want` function diag_json_case(path: pointer, n: int, want: pointer, label: pointer) -> void { @@ -1284,6 +1296,24 @@ function cmd_dev_test() -> int { diag_json_case("rejected/permap_maps/permap_maps", 6, "there is no Spots row nowhere in this map", "--check reads every map: a cross-row @Ref names a row of the same map") diag_json_case("rejected/permap_maps/permap_maps", 6, "the key t1 is also written at", "--check reads every map: a chunked table's key is unique across the map's chunk files, both places named") diag_json_case("rejected/permap_maps/permap_maps", 6, "this map has no examples/rejected/permap_maps/maps/m1/blades.png", "--check reads every map: a game-wide row's @Asset(..., map) file is in each map's directory, unless optional") + # 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() + 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") + schema_case("lang/i18n/i18n", "{\"english\": \"Close\", \"keys\": [\"map.close\", \"pause.close\"]}", "--emit-schema: lang.split - one English under several keys") + diag_json_case("rejected/i18n_keys/key_missing", 2, "the key pause.gone is not in examples/rejected/i18n_keys/lang/en.po", "a key literal en.po does not have is refused, at its line and column") + diag_json_case("rejected/i18n_keys/key_missing", 2, "is a plural key, and examples/rejected/i18n_keys/lang/en.po:7 gives pause.resume no msgid_plural", "kn\"...\" is a plural key: en.po's entry has a msgid_plural") + diag_json_case("rejected/i18n_keys/key_missing", 2, "trf: hud.day is given 2 value(s), and its English in examples/rejected/i18n_keys/lang/en.po has 1 hole(s)", "trf given more values than the key's English has holes: a warning") + diag_json_case("rejected/i18n_keys/key_missing", 2, "shop.close has no #. description, and its English", "one English split into several keys wants a #. description on each: a warning at en.po's line") + 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/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") + diag_json_case("rejected/i18n_keys/t_missing", 1, "t(): hud.day is given 0 value(s)", "a template's t('key') given fewer values than the English has holes: a warning") feat_case("lang/defaults", "", "a 300.0 8.0 / b 120.0 8.0 / c! 300.0 8.0 / d 300.0 2.0 / x / y", "defaults.ludic (L11: default parameters, positional then named arguments, in a generic too)") reject_case("rejected/default_missing", "this call to f leaves out a, which has no default", "a call leaves out only what has a default") reject_case("rejected/default_unknown", "f has no parameter c", "a named argument names a parameter the function has")