diff --git a/LANGUAGE.md b/LANGUAGE.md index c9568b84..a399493f 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -800,6 +800,7 @@ object with `"schema_version": 1`: `handler` the `fn` value it passes last (else null). A tag worked out at run time cannot be known and is left out. - `units` - `@Unit`'s canonical spellings. +- `lang` - the text keys and the languages (see Text keys), or null without a `lang` line. Each list is sorted by name (then file and line), a registry's entries are in index order, and the paths are the ones the compiler read, so two runs over the same source write the same file. The @@ -810,6 +811,79 @@ worked out once and only when the program names it - so a tool talking to a runn whether it was compiled from the data in front of it. It is 0 in a release (`ludicc --release`, which `ludic bundle` passes). +### Text keys (`Key`, `k"..."`, `lang`) + +A text the player reads is named by a KEY, and the key's English is a language file like any +other's. `package.ludic` says where the languages are and which is the source: + +``` +lang "assets/lang" en # the directory, and the source language: assets/lang/en.po +``` + +In code a key is a literal of the builtin type `Key` - `k"module.purpose"`, or `kn"..."` for a key +whose text has plural forms - written against its quote (`k "x"` is a name and a string, as ever): + +```ludic +# doc-check: skip — tr / trf / trn are ludic.i18n's +let title = tr(k"pause.resume") # tr(key: Key) -> string +let day = trf(k"hud.day", txt_num(n)) # the English's holes {1}..{4}, in order +let got = trn(kn"catch.count", n, fish) # {1} is the count, the rest follow +``` + +A `Key` is not a `string`, and a `string` is not a `Key`: giving one where the other is wanted is an +error either way, and so is `+` on a key. Keys compare with `==` and `!=`, and are fields, parameters, +results, list elements and registry values like any other type (`null` is none). At run time a key is +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.) + +**The data.** A registry's `@Text` field is text by derivation: its key is +`..` - the registry's name in snake case (`Items` -> `items`, `GearKinds` -> +`gear_kinds`), a list field adding `.` and a nested record `.` - or, with +`@TextKey("steps")` on the registry, `steps..`. A `@PerMap` table's is +`maps....`. 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: +k"items.lamp.name"`, in a resource file or a map's file alike. + +```ludic +# doc-check: skip — the data file is the game's +property Item { + key: string = "" + @Text name: Key = null # items..name, filled in when the row gives none +} +registry Items of Item as IT from "data/items.lres" +@TextKey("steps") registry StepKinds of StepKind from "steps.lres" # steps.. +``` + +**The templates.** A component's text is a key too: `{t('pause.resume')}`, `title="{t('pause.close')}"`, +`{t('hud.day', day)}`; `t(expr)` takes a key worked out at run time (a `Key` field reaches a template +as its marked text). Words outside an element with `translate="no"` are English still waiting for a +key. + +**The checks.** With a `lang` line and its source `.po` there, the compiler holds every key to it, +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**; +- **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 + **warning**, and `ludic deps` counts them as `english_left`, a ratchet like every number there; +- 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. +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; `#~` +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 +language in the directory its `missing`, `fuzzy` and `extra` keys. + ### Default parameters, and calls that name what they change A parameter can have a default, and a call leaves out what it does not change - the last ones when @@ -1330,6 +1404,7 @@ for a complete title screen. | `bool` | boolean | `i32` | | `entity` | entity handle | `i32` | | `string` | text (a string literal, an interpolation, a concatenation) | `ptr` | +| `Key` | a text key, `k"pause.resume"` (see Text keys) - not a `string`, and no string is one | `ptr` | | `pointer` | raw address (runtime/FFI, records, anything untyped) | `ptr` | | `byte` | one byte value (what `p[i]` on a `bytes` buffer reads) | `i8` | | `bytes` | buffer of bytes — `b[i]` reads/writes one byte | `ptr` | diff --git a/changes/text-keys.md b/changes/text-keys.md new file mode 100644 index 00000000..5f98cafd --- /dev/null +++ b/changes/text-keys.md @@ -0,0 +1,13 @@ +bump: minor +type: feature +**Text keys: `k"pause.resume"`, a `Key`, checked against `en.po`.** A text the player reads is named by +a key of the new builtin type `Key` (`kn"..."` for a plural) - not a string, and no string one; at run +time its text after a marker byte, so a runtime's `tr` is a cast. `lang "assets/lang" en` in +`package.ludic` names the source language's `.po`, and the compiler holds every key to it: a key +literal, a template's `t('key', ...)` and a registry's derived `@Text` key (`..`, +or `@TextKey("steps")`'s prefix; filled into a `Key` field a row leaves out) must be there, a plural +one with a `msgid_plural`; `trf` / `trn` / `t()` given another number of values than the English has +holes, text still written in English (a template's words, a `@Text` row's value - counted as +`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. diff --git a/docs/SHIPPING.md b/docs/SHIPPING.md index 80d50ead..0fb8520a 100644 --- a/docs/SHIPPING.md +++ b/docs/SHIPPING.md @@ -49,6 +49,7 @@ pack "assets" | `app out` | where to write the bundle | `build/.app` | | `pack` | an asset root, repeatable | `assets/` if it exists | | `maps` | where the maps' own tables are, one directory a map (`@PerMap` registries, LANGUAGE.md) - the compiler builds it into the program and `ludicc --check` reads every map under it | `assets/maps` | +| `lang` | the languages' directory and the source language (`lang "assets/lang" en`): the compiler checks every text key against `/.po` (LANGUAGE.md, Text keys) | none: no key is checked | Only `app name` is worth setting deliberately. The identifier is derived from the package path when you omit it - `git.workshopsoft.io/workshopsoft/maroon-lake`