docs: LANGUAGE.md's Text keys (Key, k"..." / kn"...", the lang line, derived @Text keys and @TextKey, the checks, the schema's "lang"), the lang manifest key in SHIPPING.md, and the changeset

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-29 23:38:03 +03:00
parent f9808e31ec
commit 66b8e51026
3 changed files with 89 additions and 0 deletions

View file

@ -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 `handler` the `fn` value it passes last (else null). A tag worked out at run time cannot be known
and is left out. and is left out.
- `units` - `@Unit`'s canonical spellings. - `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 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 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`, whether it was compiled from the data in front of it. It is 0 in a release (`ludicc --release`,
which `ludic bundle` passes). 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
`<registry>.<row>.<field>` - the registry's name in snake case (`Items` -> `items`, `GearKinds` ->
`gear_kinds`), a list field adding `.<index>` and a nested record `.<field>` - or, with
`@TextKey("steps")` on the registry, `steps.<row>.<field>`. A `@PerMap` table's is
`maps.<map>.<table>.<row>.<field>`. 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.<row>.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.<row>.<field>
```
**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 ### 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 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` | | `bool` | boolean | `i32` |
| `entity` | entity handle | `i32` | | `entity` | entity handle | `i32` |
| `string` | text (a string literal, an interpolation, a concatenation) | `ptr` | | `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` | | `pointer` | raw address (runtime/FFI, records, anything untyped) | `ptr` |
| `byte` | one byte value (what `p[i]` on a `bytes` buffer reads) | `i8` | | `byte` | one byte value (what `p[i]` on a `bytes` buffer reads) | `i8` |
| `bytes` | buffer of bytes — `b[i]` reads/writes one byte | `ptr` | | `bytes` | buffer of bytes — `b[i]` reads/writes one byte | `ptr` |

13
changes/text-keys.md Normal file
View file

@ -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 (`<registry>.<row>.<field>`,
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.

View file

@ -49,6 +49,7 @@ pack "assets"
| `app out` | where to write the bundle | `build/<name>.app` | | `app out` | where to write the bundle | `build/<name>.app` |
| `pack` | an asset root, repeatable | `assets/` if it exists | | `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` | | `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 `<dir>/<source>.po` (LANGUAGE.md, Text keys) | none: no key is checked |
Only `app name` is worth setting deliberately. The identifier is derived from the 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` package path when you omit it - `git.workshopsoft.io/workshopsoft/maroon-lake`