@8bitscript/i18n 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 8BitScript contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # @8bitscript/i18n
2
+
3
+ What the build's locale is like, decided at compile time — the same on
4
+ every target, VIC-20, C64, PET, C128, Atari 8-bit, NES, Commander X16,
5
+ MEGA65, and web. Two import paths: the facts, and a number printed with
6
+ them.
7
+
8
+ ```bash
9
+ pnpm add @8bitscript/i18n
10
+ ```
11
+
12
+ ```
13
+ import { Locale } from "@8bitscript/i18n"; // the facts: two ASCII codes
14
+ import { number } from "@8bitscript/i18n/number"; // a grouped number, through @8bitscript/text
15
+
16
+ text.putChar(cell, Locale.DECIMAL); // '.' — or ',' in a German build
17
+ number.print(cell, score, 6); // "12,345" — or "12.345"
18
+ ```
19
+
20
+ | Name | What it is |
21
+ | --- | --- |
22
+ | `Locale.DECIMAL` | The character between a number's whole and fractional parts, as the ASCII code `text.putChar` takes: `.` (46), or `,` (44) |
23
+ | `Locale.GROUP` | The character between groups of three digits: `,` (44), `.` (46), or a space (32) |
24
+ | `number.print(cell, value, width)` | `value` as decimal digits with `Locale.GROUP` every three from the right, right-aligned in `width` cells from `cell`, blank ahead of it. Six cells hold the widest `usmallint`, `65,535`; a narrower field prints the whole number from `cell` rather than cutting it |
25
+
26
+ ## How a locale gets in
27
+
28
+ A locale is a build input, never a run-time one: `8bs build --locale de`,
29
+ a target's `locale`, or a `release` entry's (see
30
+ [Project config](../../docs/config.md#one-binary-per-locale)). This
31
+ package is one file per locale — `src/index.8bs` for a build that names
32
+ none, `index.de.8bs`, `index.fr.8bs`, `index.it.8bs`, `index.nl.8bs`,
33
+ `index.pt-br.8bs` beside it — and the compiler's resolver picks the file
34
+ by the same twin rule that takes a program's own `strings.de.8bs` over its
35
+ `strings.8bs`. Nothing in a program names a locale; nothing is looked up
36
+ while it runs; a program that reads neither name links none of this.
37
+
38
+ A locale with no file here silently reads the plain one — `--locale sv`
39
+ gets `.` and `,`. Add a twin (five lines; cite the CLDR entry, as each of
40
+ the shipped ones does) rather than branch on `#locale("sv")` in a program.
41
+ Spanish is deliberately not shipped yet: CLDR gives `es` a
42
+ `minimumGroupingDigits` of 2, so a four-digit number is written `1234`
43
+ and only `12.345` is grouped — a rule `number.print` does not express,
44
+ and a twin that said `.` alone would print `1.234`. Check the key in
45
+ CLDR's `numbers.json` for `es` before adding it.
46
+
47
+ ## What is here, and what is not
48
+
49
+ The two facts are the ones a program on a character grid can act on.
50
+ Every value is a literal in the portable character set — `,` `.` and the
51
+ space are on every machine, including the NES's own font — so a build
52
+ never reaches for a glyph its target cannot draw. A read of one folds to
53
+ the literal: a PET, VIC-20 or C64 program that writes `Locale.GROUP` is
54
+ byte-identical to one that writes `44` (a test builds both and compares
55
+ the images). Nothing here is a
56
+ string: a program's strings are its own, in a `strings.8bs` and that
57
+ file's locale twins, where the compiler can see their lengths. Nothing
58
+ here names an accented letter or a currency sign, because no target can
59
+ show one; a language written in them is transliterated in the program's
60
+ strings file (`DRUECKEN`), the way a typewriter without the keys spelled
61
+ it, and that is a decision the program's author makes line by line.
62
+
63
+ `number.print` is code — the place-value loop every machine's
64
+ `text.printNumber` uses to find digits without a divide, plus the
65
+ separator — and lives at its own subpath so that a program printing a
66
+ zero-padded HUD field (`text.printNumber`, or a `${score:5}` template)
67
+ carries none of it. What it costs, measured 2026-09-17 as one number
68
+ printed this way instead of with `text.printNumber`, in a program that
69
+ owns its machine: +209 bytes and +11 of RAM on the PET, +202 and +11 on
70
+ the unexpanded VIC-20, +194 and +9 on the C64. The two are for different
71
+ numbers: `printNumber` is the counter that must never shift columns,
72
+ this is the total the player reads.
73
+
74
+ Tests: `pnpm --filter @8bitscript/i18n test` — resolution per locale on
75
+ every machine, a clean link on all nine with and without a locale, the
76
+ PET and VIC-20 images built for real (the fact byte-identical to its
77
+ literal; `number.print`'s frame inside their zero page), and
78
+ `number.print` built and run for the web, its cells read back, in
79
+ English and in German.
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@8bitscript/i18n",
3
+ "version": "0.13.0",
4
+ "description": "What a locale is like, decided at compile time: the bare import is the build's locale's own facts — its decimal and grouping separators — as a twin file per locale inside the package, so a program reads Locale.GROUP and the build picks the file; ./number prints a grouped number through @8bitscript/text with them. Costs nothing where it is not read.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/8BitScript/8bitscript.git",
9
+ "directory": "packages/i18n"
10
+ },
11
+ "8bitscript": {
12
+ "entry": "./src/index.8bs",
13
+ "exports": {
14
+ "./number": "./src/number.8bs"
15
+ }
16
+ },
17
+ "files": [
18
+ "src"
19
+ ],
20
+ "dependencies": {
21
+ "@8bitscript/text": "workspace:*"
22
+ },
23
+ "scripts": {
24
+ "test": "node --test"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ }
29
+ }
package/src/index.8bs ADDED
@@ -0,0 +1,29 @@
1
+ // @8bitscript/i18n — what the build's locale is like, as compile-time
2
+ // facts. This file is the one a build with no locale named reads: the
3
+ // program's source language, which for every program so far has been
4
+ // English, so the numbers below are English's. Beside it, index.de.8bs
5
+ // and the rest are one locale each, and `8bs build --locale de` (or a
6
+ // target's `locale`, or a `release` entry's) takes that file instead —
7
+ // the resolver's locale twin rule, the same one that takes a program's
8
+ // own strings.de.8bs over its strings.8bs (docs/config.md, "One binary
9
+ // per locale"). A locale with no file here reads this one, silently:
10
+ // add the twin (five lines) rather than branch on #locale("...").
11
+ //
12
+ // Every value is a literal in a namespace, so a read of Locale.GROUP is
13
+ // one byte in the image and a branch on it folds; a program that never
14
+ // reads it links none of this. What is here is what a program on a
15
+ // character grid can act on: the two separators a number is written
16
+ // with, as the ASCII codes @8bitscript/text's putChar takes. Both are
17
+ // in the portable character set on every machine (`,` and `.`; a space
18
+ // where a language groups with one). What is not here is anything the
19
+ // machines cannot show — an accented letter, a currency sign — and
20
+ // anything that is a string: strings are the program's own, in its
21
+ // strings.8bs and that file's locale twins.
22
+ //
23
+ // Sources: CLDR's number symbols for each locale (unicode.org/cldr);
24
+ // each twin cites the entry it was read from.
25
+
26
+ export namespace Locale {
27
+ const DECIMAL: utinyint = 46; // '.' — 3.5
28
+ const GROUP: utinyint = 44; // ',' — 65,535
29
+ }
@@ -0,0 +1,6 @@
1
+ // German — index.8bs's twin for `--locale de`. CLDR de: decimal ",",
2
+ // group "." (1.234,5).
3
+ export namespace Locale {
4
+ const DECIMAL: utinyint = 44; // ','
5
+ const GROUP: utinyint = 46; // '.'
6
+ }
@@ -0,0 +1,7 @@
1
+ // French — index.8bs's twin for `--locale fr`. CLDR fr: decimal ",",
2
+ // group U+202F narrow no-break space (1 234,5); a character grid has one
3
+ // space, so it is 32.
4
+ export namespace Locale {
5
+ const DECIMAL: utinyint = 44; // ','
6
+ const GROUP: utinyint = 32; // ' '
7
+ }
@@ -0,0 +1,6 @@
1
+ // Italian — index.8bs's twin for `--locale it`. CLDR it: decimal ",",
2
+ // group "." (1.234,5).
3
+ export namespace Locale {
4
+ const DECIMAL: utinyint = 44; // ','
5
+ const GROUP: utinyint = 46; // '.'
6
+ }
@@ -0,0 +1,6 @@
1
+ // Dutch — index.8bs's twin for `--locale nl`. CLDR nl: decimal ",",
2
+ // group "." (1.234,5).
3
+ export namespace Locale {
4
+ const DECIMAL: utinyint = 44; // ','
5
+ const GROUP: utinyint = 46; // '.'
6
+ }
@@ -0,0 +1,6 @@
1
+ // Brazilian Portuguese — index.8bs's twin for `--locale pt-br`. CLDR
2
+ // pt-BR: decimal ",", group "." (1.234,5).
3
+ export namespace Locale {
4
+ const DECIMAL: utinyint = 44; // ','
5
+ const GROUP: utinyint = 46; // '.'
6
+ }
package/src/number.8bs ADDED
@@ -0,0 +1,75 @@
1
+ // @8bitscript/i18n/number — a number the way the build's locale writes
2
+ // it, through @8bitscript/text: `number.print(cell, value, width)` writes
3
+ // `value` as decimal digits with Locale.GROUP between every three, from
4
+ // the right, right-aligned in a field of `width` cells from `cell`, the
5
+ // cells ahead of it blank — "1,234" in an English build and "1.234" in a
6
+ // German one, from the same call. The most a usmallint takes is six
7
+ // cells ("65,535"); a field narrower than its number is printed from
8
+ // `cell` anyway, so the number is never cut, and a caller that sizes the
9
+ // field at six is safe for every value.
10
+ //
11
+ // Opt-in, at its own subpath: unlike the facts at the bare import this is
12
+ // code — a loop over the place values, the way every machine's
13
+ // text.printNumber works out digits without a divide (the 6502 has none,
14
+ // and `value / 10` would link a 250-byte routine) — and a program that
15
+ // prints its numbers as `text.printNumber`'s zero-padded field, or in a
16
+ // template, pays nothing for it. text.printNumber is the field for a HUD
17
+ // counter that must not shift columns; this is the number for a total
18
+ // the player reads.
19
+ import { text } from "@8bitscript/text";
20
+ import { Locale } from "./index.8bs";
21
+
22
+ const PLACES: array<usmallint, 5> = [10000, 1000, 100, 10, 1];
23
+
24
+ // Written for a small zero-page frame — the 6502 backend keeps every
25
+ // local and parameter of the deepest call chain in zero page, so a local
26
+ // here is a byte of every program's tightest budget: a first cut with
27
+ // four locals for the digit count and the field needed 19 bytes, this
28
+ // one 15 (text.printNumber's own is 13). `cell` is the cursor, and
29
+ // `remaining` is the digit count, the number's width, and the countdown
30
+ // to the separator in turn. What the whole thing costs, measured
31
+ // 2026-09-17 as one number printed this way instead of with
32
+ // text.printNumber, in a program that owns its machine: +209 bytes and
33
+ // +11 of RAM on the PET, +202 and +11 on the unexpanded VIC-20, +194 and
34
+ // +9 on the C64 — @8bitscript/text's fill() and putChar() linked beside
35
+ // its place(), and the separator's own loop.
36
+ export namespace number {
37
+ function print(cell: usmallint, value: usmallint, width: utinyint): void {
38
+ // How many digits: the first place the value reaches, high to
39
+ // low — one digit for 0-9, so a zero prints as one "0".
40
+ let remaining: utinyint = 1;
41
+ let i: utinyint = 0;
42
+ while (i < 4) {
43
+ if (remaining == 1 && value >= PLACES[i]) {
44
+ remaining = 5 - i;
45
+ }
46
+ i++;
47
+ }
48
+ // Five digits at most, so at most one separator, and only when
49
+ // there is a fourth digit: the number's width is one more then.
50
+ i = remaining;
51
+ if (remaining > 3) {
52
+ i = remaining + 1;
53
+ }
54
+ if (width > i) {
55
+ text.fill(cell, width - i, 32);
56
+ cell = cell + (width - i);
57
+ }
58
+ i = 5 - remaining; // the first place printed
59
+ while (i < 5) {
60
+ let digit: utinyint = 48; // '0'
61
+ while (value >= PLACES[i]) {
62
+ value = value - PLACES[i];
63
+ digit++;
64
+ }
65
+ text.putChar(cell, digit);
66
+ cell++;
67
+ remaining--;
68
+ if (remaining == 3) {
69
+ text.putChar(cell, Locale.GROUP);
70
+ cell++;
71
+ }
72
+ i++;
73
+ }
74
+ }
75
+ }