@lingo.dev/spec 1.0.0 → 1.0.1

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/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@lingo.dev/spec",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "type": "module",
5
- "main": "./dist/index.js",
6
5
  "exports": {
7
6
  ".": {
7
+ "typescript": {
8
+ "types": "./src/index.ts",
9
+ "default": "./src/index.ts"
10
+ },
8
11
  "import": {
9
12
  "types": "./dist/index.d.ts",
10
13
  "default": "./dist/index.js"
@@ -16,7 +19,8 @@
16
19
  }
17
20
  },
18
21
  "files": [
19
- "dist"
22
+ "dist",
23
+ "src"
20
24
  ],
21
25
  "sideEffects": false,
22
26
  "dependencies": {
@@ -0,0 +1,96 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { computeKey } from "./hash";
3
+
4
+ describe("computeKey", () => {
5
+ it("returns an 8-character string", () => {
6
+ expect(computeKey("Hello")).toHaveLength(8);
7
+ });
8
+
9
+ it("is deterministic - same input produces same output", () => {
10
+ expect(computeKey("Save")).toBe(computeKey("Save"));
11
+ expect(computeKey("Save", "Form button")).toBe(computeKey("Save", "Form button"));
12
+ });
13
+
14
+ it("produces different keys for different source text", () => {
15
+ expect(computeKey("Save")).not.toBe(computeKey("Cancel"));
16
+ });
17
+
18
+ it("produces different keys for same text with different context", () => {
19
+ const withoutContext = computeKey("Save");
20
+ const withContext = computeKey("Save", "Form button");
21
+ const withOtherContext = computeKey("Save", "Toolbar action");
22
+
23
+ expect(withoutContext).not.toBe(withContext);
24
+ expect(withContext).not.toBe(withOtherContext);
25
+ expect(withoutContext).not.toBe(withOtherContext);
26
+ });
27
+
28
+ it("uses only base62 characters", () => {
29
+ const key = computeKey("Hello, world!");
30
+ expect(key).toMatch(/^[0-9A-Za-z]+$/);
31
+ });
32
+
33
+ it("handles empty string", () => {
34
+ const key = computeKey("");
35
+ expect(key).toHaveLength(8);
36
+ expect(key).toMatch(/^[0-9A-Za-z]+$/);
37
+ });
38
+
39
+ it("handles unicode text", () => {
40
+ const key = computeKey("こんにちは");
41
+ expect(key).toHaveLength(8);
42
+ expect(key).toMatch(/^[0-9A-Za-z]+$/);
43
+ });
44
+
45
+ it("handles long strings", () => {
46
+ const key = computeKey("a".repeat(10_000));
47
+ expect(key).toHaveLength(8);
48
+ expect(key).toMatch(/^[0-9A-Za-z]+$/);
49
+ });
50
+
51
+ it("treats empty context same as no context", () => {
52
+ expect(computeKey("Save", "")).toBe(computeKey("Save"));
53
+ expect(computeKey("Save", undefined)).toBe(computeKey("Save"));
54
+ });
55
+
56
+ it("NFC-normalizes input - composed and decomposed forms produce the same key", () => {
57
+ // e + combining acute accent (U+0065 U+0301) vs precomposed e-acute (U+00E9)
58
+ const decomposed = "re\u0301sume\u0301"; // r + e + ́ + s + u + m + e + ́
59
+ const composed = "r\u00E9sum\u00E9"; // résumé (precomposed)
60
+ expect(computeKey(decomposed)).toBe(computeKey(composed));
61
+ });
62
+
63
+ it("NFC-normalizes context too", () => {
64
+ const decomposed = "cafe\u0301"; // cafe + combining acute
65
+ const composed = "caf\u00E9"; // café (precomposed)
66
+ expect(computeKey("Save", decomposed)).toBe(computeKey("Save", composed));
67
+ });
68
+
69
+ it("handles emoji and surrogate pairs", () => {
70
+ const key = computeKey("Hello 👋 World 🌍");
71
+ expect(key).toHaveLength(8);
72
+ expect(key).toMatch(/^[0-9A-Za-z]+$/);
73
+ });
74
+
75
+ it("handles strings with null bytes", () => {
76
+ const key = computeKey("hello\0world");
77
+ expect(key).toHaveLength(8);
78
+ expect(key).not.toBe(computeKey("hello"));
79
+ expect(key).not.toBe(computeKey("world"));
80
+ });
81
+
82
+ it("produces stable reference vectors for cross-platform verification", () => {
83
+ // These values anchor the algorithm. Any change to hashing, normalization,
84
+ // or encoding MUST update these vectors across all platform implementations.
85
+ const vectors: Record<string, string> = {
86
+ "": computeKey(""),
87
+ Hello: computeKey("Hello"),
88
+ "Hello, world!": computeKey("Hello, world!"),
89
+ };
90
+
91
+ // Snapshot: values must never change once published
92
+ expect(vectors[""]).toMatchInlineSnapshot(`"00000000"`);
93
+ expect(vectors["Hello"]).toMatchInlineSnapshot(`"TT6OR9Mc"`);
94
+ expect(vectors["Hello, world!"]).toMatchInlineSnapshot(`"D3IiJAcZ"`);
95
+ });
96
+ });
package/src/hash.ts ADDED
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Deterministic key generation from (source text, context) pairs.
3
+ * Shared between runtime (message lookup) and build tools (extraction, JSONC generation).
4
+ *
5
+ * Uses MurmurHash3 x86_128 on NFC-normalized UTF-8 bytes.
6
+ * Outputs an 8-character base62 string (e.g., "aB3dEf9x").
7
+ *
8
+ * Cross-platform deterministic: JS, Rust, Go, Python produce identical keys
9
+ * when given the same (source, context) pair.
10
+ *
11
+ * 1% collision probability at ~2.4 million messages (48-bit effective entropy).
12
+ */
13
+
14
+ const BASE62_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
15
+ const KEY_LENGTH = 8;
16
+ const BASE62_SPACE = 62 ** KEY_LENGTH;
17
+
18
+ const encoder = new TextEncoder();
19
+
20
+ // --- MurmurHash3 x86_128 (reference: github.com/aappleby/smhasher) ---
21
+
22
+ const C1 = 0x239b961b;
23
+ const C2 = 0xab0e9789;
24
+ const C3 = 0x38b34ae5;
25
+ const C4 = 0xa1e38b93;
26
+
27
+ function rotl32(x: number, r: number): number {
28
+ return ((x << r) | (x >>> (32 - r))) >>> 0;
29
+ }
30
+
31
+ function fmix32(h: number): number {
32
+ h ^= h >>> 16;
33
+ h = Math.imul(h, 0x85ebca6b) >>> 0;
34
+ h ^= h >>> 13;
35
+ h = Math.imul(h, 0xc2b2ae35) >>> 0;
36
+ h ^= h >>> 16;
37
+ return h >>> 0;
38
+ }
39
+
40
+ function readU32LE(bytes: Uint8Array, i: number): number {
41
+ return (bytes[i] | (bytes[i + 1] << 8) | (bytes[i + 2] << 16) | (bytes[i + 3] << 24)) >>> 0;
42
+ }
43
+
44
+ function murmurhash3_x86_128(bytes: Uint8Array): [number, number, number, number] {
45
+ const len = bytes.length;
46
+ const nblocks = len >>> 4;
47
+
48
+ let h1 = 0;
49
+ let h2 = 0;
50
+ let h3 = 0;
51
+ let h4 = 0;
52
+
53
+ for (let i = 0; i < nblocks; i++) {
54
+ const off = i << 4;
55
+ let k1 = readU32LE(bytes, off);
56
+ let k2 = readU32LE(bytes, off + 4);
57
+ let k3 = readU32LE(bytes, off + 8);
58
+ let k4 = readU32LE(bytes, off + 12);
59
+
60
+ k1 = Math.imul(k1, C1) >>> 0;
61
+ k1 = rotl32(k1, 15);
62
+ k1 = Math.imul(k1, C2) >>> 0;
63
+ h1 ^= k1;
64
+ h1 = rotl32(h1, 19);
65
+ h1 = (h1 + h2) >>> 0;
66
+ h1 = (Math.imul(h1, 5) + 0x561ccd1b) >>> 0;
67
+
68
+ k2 = Math.imul(k2, C2) >>> 0;
69
+ k2 = rotl32(k2, 16);
70
+ k2 = Math.imul(k2, C3) >>> 0;
71
+ h2 ^= k2;
72
+ h2 = rotl32(h2, 17);
73
+ h2 = (h2 + h3) >>> 0;
74
+ h2 = (Math.imul(h2, 5) + 0x0bcaa747) >>> 0;
75
+
76
+ k3 = Math.imul(k3, C3) >>> 0;
77
+ k3 = rotl32(k3, 17);
78
+ k3 = Math.imul(k3, C4) >>> 0;
79
+ h3 ^= k3;
80
+ h3 = rotl32(h3, 15);
81
+ h3 = (h3 + h4) >>> 0;
82
+ h3 = (Math.imul(h3, 5) + 0x96cd1c35) >>> 0;
83
+
84
+ k4 = Math.imul(k4, C4) >>> 0;
85
+ k4 = rotl32(k4, 18);
86
+ k4 = Math.imul(k4, C1) >>> 0;
87
+ h4 ^= k4;
88
+ h4 = rotl32(h4, 13);
89
+ h4 = (h4 + h1) >>> 0;
90
+ h4 = (Math.imul(h4, 5) + 0x32ac3b17) >>> 0;
91
+ }
92
+
93
+ const tail = nblocks << 4;
94
+ let k1 = 0;
95
+ let k2 = 0;
96
+ let k3 = 0;
97
+ let k4 = 0;
98
+ const rem = len & 15;
99
+
100
+ if (rem >= 15) k4 ^= bytes[tail + 14] << 16;
101
+ if (rem >= 14) k4 ^= bytes[tail + 13] << 8;
102
+ if (rem >= 13) {
103
+ k4 ^= bytes[tail + 12];
104
+ k4 = Math.imul(k4, C4) >>> 0;
105
+ k4 = rotl32(k4, 18);
106
+ k4 = Math.imul(k4, C1) >>> 0;
107
+ h4 ^= k4;
108
+ }
109
+ if (rem >= 12) k3 ^= bytes[tail + 11] << 24;
110
+ if (rem >= 11) k3 ^= bytes[tail + 10] << 16;
111
+ if (rem >= 10) k3 ^= bytes[tail + 9] << 8;
112
+ if (rem >= 9) {
113
+ k3 ^= bytes[tail + 8];
114
+ k3 = Math.imul(k3, C3) >>> 0;
115
+ k3 = rotl32(k3, 17);
116
+ k3 = Math.imul(k3, C4) >>> 0;
117
+ h3 ^= k3;
118
+ }
119
+ if (rem >= 8) k2 ^= bytes[tail + 7] << 24;
120
+ if (rem >= 7) k2 ^= bytes[tail + 6] << 16;
121
+ if (rem >= 6) k2 ^= bytes[tail + 5] << 8;
122
+ if (rem >= 5) {
123
+ k2 ^= bytes[tail + 4];
124
+ k2 = Math.imul(k2, C2) >>> 0;
125
+ k2 = rotl32(k2, 16);
126
+ k2 = Math.imul(k2, C3) >>> 0;
127
+ h2 ^= k2;
128
+ }
129
+ if (rem >= 4) k1 ^= bytes[tail + 3] << 24;
130
+ if (rem >= 3) k1 ^= bytes[tail + 2] << 16;
131
+ if (rem >= 2) k1 ^= bytes[tail + 1] << 8;
132
+ if (rem >= 1) {
133
+ k1 ^= bytes[tail];
134
+ k1 = Math.imul(k1, C1) >>> 0;
135
+ k1 = rotl32(k1, 15);
136
+ k1 = Math.imul(k1, C2) >>> 0;
137
+ h1 ^= k1;
138
+ }
139
+
140
+ h1 ^= len;
141
+ h2 ^= len;
142
+ h3 ^= len;
143
+ h4 ^= len;
144
+
145
+ h1 = (h1 + h2) >>> 0;
146
+ h1 = (h1 + h3) >>> 0;
147
+ h1 = (h1 + h4) >>> 0;
148
+ h2 = (h2 + h1) >>> 0;
149
+ h3 = (h3 + h1) >>> 0;
150
+ h4 = (h4 + h1) >>> 0;
151
+
152
+ h1 = fmix32(h1);
153
+ h2 = fmix32(h2);
154
+ h3 = fmix32(h3);
155
+ h4 = fmix32(h4);
156
+
157
+ h1 = (h1 + h2) >>> 0;
158
+ h1 = (h1 + h3) >>> 0;
159
+ h1 = (h1 + h4) >>> 0;
160
+ h2 = (h2 + h1) >>> 0;
161
+ h3 = (h3 + h1) >>> 0;
162
+ h4 = (h4 + h1) >>> 0;
163
+
164
+ return [h1, h2, h3, h4];
165
+ }
166
+
167
+ // --- Key generation ---
168
+
169
+ function toBase62(num: number, length: number): string {
170
+ let result = "";
171
+ let remaining = num;
172
+ for (let i = 0; i < length; i++) {
173
+ result = BASE62_CHARS[remaining % 62] + result;
174
+ remaining = Math.floor(remaining / 62);
175
+ }
176
+ return result;
177
+ }
178
+
179
+ /**
180
+ * Computes a deterministic short key from source text and optional context.
181
+ *
182
+ * Same source + same context = same key (deterministic).
183
+ * Same source + different context = different key (disambiguation).
184
+ */
185
+ export function computeKey(source: string, context?: string): string {
186
+ const input = context != null && context !== "" ? `${source}\0${context}` : source;
187
+ const bytes = encoder.encode(input.normalize("NFC"));
188
+ const [h1, h2] = murmurhash3_x86_128(bytes);
189
+
190
+ // 48-bit value: h1 (32 bits) + upper 16 bits of h2
191
+ // Safe for JS Number arithmetic (well within 2^53)
192
+ const value = (h1 >>> 0) + (h2 >>> 16) * 0x100000000;
193
+
194
+ return toBase62(value % BASE62_SPACE, KEY_LENGTH);
195
+ }
@@ -0,0 +1,41 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { buildIcuPlural, buildIcuSelect } from "./icu";
3
+
4
+ describe("buildIcuPlural", () => {
5
+ it("builds ICU expression with one/other", () => {
6
+ expect(buildIcuPlural({ one: "# item", other: "# items" })).toBe("{count, plural, one {# item} other {# items}}");
7
+ });
8
+
9
+ it("orders categories by CLDR convention", () => {
10
+ // Even if passed out of order, output follows CLDR: zero, one, two, few, many, other
11
+ const result = buildIcuPlural({ other: "# things", zero: "no things", one: "# thing" });
12
+ expect(result).toBe("{count, plural, zero {no things} one {# thing} other {# things}}");
13
+ });
14
+
15
+ it("handles other-only", () => {
16
+ expect(buildIcuPlural({ other: "# items" })).toBe("{count, plural, other {# items}}");
17
+ });
18
+
19
+ it("handles all six categories", () => {
20
+ const forms = { zero: "0", one: "1", two: "2", few: "few", many: "many", other: "other" };
21
+ expect(buildIcuPlural(forms)).toBe("{count, plural, zero {0} one {1} two {2} few {few} many {many} other {other}}");
22
+ });
23
+ });
24
+
25
+ describe("buildIcuSelect", () => {
26
+ it("builds ICU expression sorted alphabetically", () => {
27
+ expect(buildIcuSelect({ admin: "Admin", other: "Home", user: "Dashboard" })).toBe(
28
+ "{value, select, admin {Admin} other {Home} user {Dashboard}}",
29
+ );
30
+ });
31
+
32
+ it("sorts keys regardless of insertion order", () => {
33
+ const a = buildIcuSelect({ user: "U", admin: "A", other: "O" });
34
+ const b = buildIcuSelect({ admin: "A", other: "O", user: "U" });
35
+ expect(a).toBe(b);
36
+ });
37
+
38
+ it("handles other-only", () => {
39
+ expect(buildIcuSelect({ other: "Default" })).toBe("{value, select, other {Default}}");
40
+ });
41
+ });
package/src/icu.ts ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Canonical ICU MessageFormat expression builders.
3
+ * Shared between runtime (l.plural/l.select) and build tools (extraction).
4
+ *
5
+ * These must produce identical output everywhere so that computeKey()
6
+ * generates matching hash keys at build time and runtime.
7
+ */
8
+
9
+ const CLDR_PLURAL_ORDER = ["zero", "one", "two", "few", "many", "other"];
10
+
11
+ export function buildIcuPlural(forms: Record<string, string>): string {
12
+ const parts = CLDR_PLURAL_ORDER.filter((cat) => forms[cat] !== undefined)
13
+ .map((cat) => `${cat} {${forms[cat]!}}`)
14
+ .join(" ");
15
+ return `{count, plural, ${parts}}`;
16
+ }
17
+
18
+ export function buildIcuSelect(forms: Record<string, string>): string {
19
+ const parts = Object.keys(forms)
20
+ .sort()
21
+ .map((key) => `${key} {${forms[key]}}`)
22
+ .join(" ");
23
+ return `{value, select, ${parts}}`;
24
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { computeKey } from "./hash";
2
+ export { buildIcuPlural, buildIcuSelect } from "./icu";
3
+ export { getActiveEntries, readLocaleFile, type EntryMetadata, type LocaleEntry, type LocaleFile } from "./jsonc";
@@ -0,0 +1,69 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { readLocaleFile } from "./jsonc";
3
+
4
+ describe("readLocaleFile", () => {
5
+ it("reads empty object", () => {
6
+ expect(readLocaleFile("{}")).toEqual({ entries: [] });
7
+ });
8
+
9
+ it("reads simple key-value pairs", () => {
10
+ const result = readLocaleFile('{ "abc": "Hello" }');
11
+ expect(result.entries).toHaveLength(1);
12
+ expect(result.entries[0]).toEqual({ key: "abc", value: "Hello", metadata: {} });
13
+ });
14
+
15
+ it("extracts @context and @src from block comments", () => {
16
+ const jsonc = `{
17
+ /*
18
+ * @context Hero heading
19
+ * @src app/hero.tsx:12
20
+ */
21
+ "mK9xqZ": "Welcome"
22
+ }`;
23
+ const result = readLocaleFile(jsonc);
24
+ expect(result.entries[0].metadata).toEqual({
25
+ context: "Hero heading",
26
+ src: "app/hero.tsx:12",
27
+ });
28
+ });
29
+
30
+ it("detects @orphan marker", () => {
31
+ const jsonc = `{
32
+ /*
33
+ * @orphan
34
+ */
35
+ "old": "Removed"
36
+ }`;
37
+ const result = readLocaleFile(jsonc);
38
+ expect(result.entries[0].metadata.orphan).toBe(true);
39
+ });
40
+
41
+ it("handles trailing commas", () => {
42
+ const jsonc = '{ "a": "one", "b": "two", }';
43
+ const result = readLocaleFile(jsonc);
44
+ expect(result.entries).toHaveLength(2);
45
+ });
46
+
47
+ it("throws on invalid JSONC", () => {
48
+ expect(() => readLocaleFile("{ broken }")).toThrow(/Failed to parse JSONC/);
49
+ });
50
+
51
+ it("reads multiple entries with metadata", () => {
52
+ const jsonc = `{
53
+ /*
54
+ * @context Form button
55
+ * @src checkout.tsx:42
56
+ */
57
+ "aB3dEf": "Save",
58
+
59
+ /*
60
+ * @src hero.tsx:12
61
+ */
62
+ "mK9xqZ": "Welcome"
63
+ }`;
64
+ const result = readLocaleFile(jsonc);
65
+ expect(result.entries).toHaveLength(2);
66
+ expect(result.entries[0].metadata.context).toBe("Form button");
67
+ expect(result.entries[1].metadata.src).toBe("hero.tsx:12");
68
+ });
69
+ });
package/src/jsonc.ts ADDED
@@ -0,0 +1,125 @@
1
+ /**
2
+ * JSONC locale file reader with structured metadata comments.
3
+ * Lives in @lingo.dev/spec so framework adapters can parse locale files
4
+ * without depending on @lingo.dev/cli.
5
+ *
6
+ * File format:
7
+ * ```jsonc
8
+ * {
9
+ * /*
10
+ * * @context Hero heading
11
+ * * @src app/hero.tsx:12
12
+ * *​/
13
+ * "mK9xqZ": "Welcome to Acme"
14
+ * }
15
+ * ```
16
+ */
17
+
18
+ import { parse as parseJsonc, type ParseError, printParseErrorCode } from "jsonc-parser";
19
+
20
+ // --- Types (shared between reader in spec and writer in cli) ---
21
+
22
+ export type EntryMetadata = {
23
+ context?: string;
24
+ src?: string;
25
+ orphan?: boolean;
26
+ };
27
+
28
+ export type LocaleEntry = {
29
+ key: string;
30
+ value: string;
31
+ metadata: EntryMetadata;
32
+ };
33
+
34
+ export type LocaleFile = {
35
+ entries: LocaleEntry[];
36
+ };
37
+
38
+ // --- Helpers ---
39
+
40
+ /** Filters out orphaned entries, returning only active (non-orphaned) ones. */
41
+ export function getActiveEntries(entries: LocaleEntry[]): LocaleEntry[] {
42
+ return entries.filter((e) => !e.metadata.orphan);
43
+ }
44
+
45
+ // --- Reader ---
46
+
47
+ const METADATA_PATTERN = /@(\w+)(?:\s+(.*))?/;
48
+
49
+ function parseMetadataBlock(comment: string): EntryMetadata {
50
+ const metadata: EntryMetadata = {};
51
+ for (const line of comment.split("\n")) {
52
+ const match = line.match(METADATA_PATTERN);
53
+ if (!match) continue;
54
+ const [, tag, value] = match;
55
+ if (tag === "context" && value) metadata.context = value.trim();
56
+ else if (tag === "src" && value) metadata.src = value.trim();
57
+ else if (tag === "orphan") metadata.orphan = true;
58
+ }
59
+ return metadata;
60
+ }
61
+
62
+ function escapeRegex(str: string): string {
63
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
64
+ }
65
+
66
+ /**
67
+ * Scans JSONC source to associate block comments with the key that follows them.
68
+ */
69
+ function extractCommentsForKeys(content: string, keys: string[]): Map<string, EntryMetadata> {
70
+ const result = new Map<string, EntryMetadata>();
71
+ const blockCommentPattern = /\/\*[\s\S]*?\*\//g;
72
+
73
+ const comments: { end: number; text: string }[] = [];
74
+ let match: RegExpExecArray | null;
75
+ while ((match = blockCommentPattern.exec(content)) !== null) {
76
+ comments.push({ end: match.index + match[0].length, text: match[0] });
77
+ }
78
+
79
+ for (const key of keys) {
80
+ const keyPattern = new RegExp(`"${escapeRegex(key)}"\\s*:`);
81
+ const keyMatch = keyPattern.exec(content);
82
+ if (!keyMatch) continue;
83
+
84
+ const keyPos = keyMatch.index;
85
+ const precedingComment = comments.filter((c) => c.end <= keyPos).sort((a, b) => b.end - a.end)[0];
86
+
87
+ if (precedingComment) {
88
+ const between = content.slice(precedingComment.end, keyPos);
89
+ if (/^\s*$/.test(between)) {
90
+ result.set(key, parseMetadataBlock(precedingComment.text));
91
+ }
92
+ }
93
+ }
94
+
95
+ return result;
96
+ }
97
+
98
+ /**
99
+ * Parses a JSONC string into structured locale entries with metadata.
100
+ * Extracts @context, @src, and @orphan from block comments preceding each key.
101
+ * Pure function - no filesystem access. Works in Node.js and edge runtimes.
102
+ */
103
+ export function readLocaleFile(content: string): LocaleFile {
104
+ const errors: ParseError[] = [];
105
+ const data = parseJsonc(content, errors, { allowTrailingComma: true }) as Record<string, string> | undefined;
106
+
107
+ if (errors.length > 0) {
108
+ const msg = errors.map((e) => printParseErrorCode(e.error)).join(", ");
109
+ throw new Error(`Failed to parse JSONC: ${msg}`);
110
+ }
111
+
112
+ if (!data || typeof data !== "object") {
113
+ return { entries: [] };
114
+ }
115
+
116
+ const commentsByKey = extractCommentsForKeys(content, Object.keys(data));
117
+
118
+ const entries: LocaleEntry[] = Object.entries(data).map(([key, value]) => ({
119
+ key,
120
+ value,
121
+ metadata: commentsByKey.get(key) ?? {},
122
+ }));
123
+
124
+ return { entries };
125
+ }
package/dist/index.cjs DELETED
@@ -1,282 +0,0 @@
1
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- let jsonc_parser = require("jsonc-parser");
3
- //#region src/hash.ts
4
- /**
5
- * Deterministic key generation from (source text, context) pairs.
6
- * Shared between runtime (message lookup) and build tools (extraction, JSONC generation).
7
- *
8
- * Uses MurmurHash3 x86_128 on NFC-normalized UTF-8 bytes.
9
- * Outputs an 8-character base62 string (e.g., "aB3dEf9x").
10
- *
11
- * Cross-platform deterministic: JS, Rust, Go, Python produce identical keys
12
- * when given the same (source, context) pair.
13
- *
14
- * 1% collision probability at ~2.4 million messages (48-bit effective entropy).
15
- */
16
- const BASE62_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
17
- const KEY_LENGTH = 8;
18
- const BASE62_SPACE = 62 ** KEY_LENGTH;
19
- const encoder = new TextEncoder();
20
- const C1 = 597399067;
21
- const C2 = 2869860233;
22
- const C3 = 951274213;
23
- const C4 = 2716044179;
24
- function rotl32(x, r) {
25
- return (x << r | x >>> 32 - r) >>> 0;
26
- }
27
- function fmix32(h) {
28
- h ^= h >>> 16;
29
- h = Math.imul(h, 2246822507) >>> 0;
30
- h ^= h >>> 13;
31
- h = Math.imul(h, 3266489909) >>> 0;
32
- h ^= h >>> 16;
33
- return h >>> 0;
34
- }
35
- function readU32LE(bytes, i) {
36
- return (bytes[i] | bytes[i + 1] << 8 | bytes[i + 2] << 16 | bytes[i + 3] << 24) >>> 0;
37
- }
38
- function murmurhash3_x86_128(bytes) {
39
- const len = bytes.length;
40
- const nblocks = len >>> 4;
41
- let h1 = 0;
42
- let h2 = 0;
43
- let h3 = 0;
44
- let h4 = 0;
45
- for (let i = 0; i < nblocks; i++) {
46
- const off = i << 4;
47
- let k1 = readU32LE(bytes, off);
48
- let k2 = readU32LE(bytes, off + 4);
49
- let k3 = readU32LE(bytes, off + 8);
50
- let k4 = readU32LE(bytes, off + 12);
51
- k1 = Math.imul(k1, C1) >>> 0;
52
- k1 = rotl32(k1, 15);
53
- k1 = Math.imul(k1, C2) >>> 0;
54
- h1 ^= k1;
55
- h1 = rotl32(h1, 19);
56
- h1 = h1 + h2 >>> 0;
57
- h1 = Math.imul(h1, 5) + 1444728091 >>> 0;
58
- k2 = Math.imul(k2, C2) >>> 0;
59
- k2 = rotl32(k2, 16);
60
- k2 = Math.imul(k2, C3) >>> 0;
61
- h2 ^= k2;
62
- h2 = rotl32(h2, 17);
63
- h2 = h2 + h3 >>> 0;
64
- h2 = Math.imul(h2, 5) + 197830471 >>> 0;
65
- k3 = Math.imul(k3, C3) >>> 0;
66
- k3 = rotl32(k3, 17);
67
- k3 = Math.imul(k3, C4) >>> 0;
68
- h3 ^= k3;
69
- h3 = rotl32(h3, 15);
70
- h3 = h3 + h4 >>> 0;
71
- h3 = Math.imul(h3, 5) + 2530024501 >>> 0;
72
- k4 = Math.imul(k4, C4) >>> 0;
73
- k4 = rotl32(k4, 18);
74
- k4 = Math.imul(k4, C1) >>> 0;
75
- h4 ^= k4;
76
- h4 = rotl32(h4, 13);
77
- h4 = h4 + h1 >>> 0;
78
- h4 = Math.imul(h4, 5) + 850148119 >>> 0;
79
- }
80
- const tail = nblocks << 4;
81
- let k1 = 0;
82
- let k2 = 0;
83
- let k3 = 0;
84
- let k4 = 0;
85
- const rem = len & 15;
86
- if (rem >= 15) k4 ^= bytes[tail + 14] << 16;
87
- if (rem >= 14) k4 ^= bytes[tail + 13] << 8;
88
- if (rem >= 13) {
89
- k4 ^= bytes[tail + 12];
90
- k4 = Math.imul(k4, C4) >>> 0;
91
- k4 = rotl32(k4, 18);
92
- k4 = Math.imul(k4, C1) >>> 0;
93
- h4 ^= k4;
94
- }
95
- if (rem >= 12) k3 ^= bytes[tail + 11] << 24;
96
- if (rem >= 11) k3 ^= bytes[tail + 10] << 16;
97
- if (rem >= 10) k3 ^= bytes[tail + 9] << 8;
98
- if (rem >= 9) {
99
- k3 ^= bytes[tail + 8];
100
- k3 = Math.imul(k3, C3) >>> 0;
101
- k3 = rotl32(k3, 17);
102
- k3 = Math.imul(k3, C4) >>> 0;
103
- h3 ^= k3;
104
- }
105
- if (rem >= 8) k2 ^= bytes[tail + 7] << 24;
106
- if (rem >= 7) k2 ^= bytes[tail + 6] << 16;
107
- if (rem >= 6) k2 ^= bytes[tail + 5] << 8;
108
- if (rem >= 5) {
109
- k2 ^= bytes[tail + 4];
110
- k2 = Math.imul(k2, C2) >>> 0;
111
- k2 = rotl32(k2, 16);
112
- k2 = Math.imul(k2, C3) >>> 0;
113
- h2 ^= k2;
114
- }
115
- if (rem >= 4) k1 ^= bytes[tail + 3] << 24;
116
- if (rem >= 3) k1 ^= bytes[tail + 2] << 16;
117
- if (rem >= 2) k1 ^= bytes[tail + 1] << 8;
118
- if (rem >= 1) {
119
- k1 ^= bytes[tail];
120
- k1 = Math.imul(k1, C1) >>> 0;
121
- k1 = rotl32(k1, 15);
122
- k1 = Math.imul(k1, C2) >>> 0;
123
- h1 ^= k1;
124
- }
125
- h1 ^= len;
126
- h2 ^= len;
127
- h3 ^= len;
128
- h4 ^= len;
129
- h1 = h1 + h2 >>> 0;
130
- h1 = h1 + h3 >>> 0;
131
- h1 = h1 + h4 >>> 0;
132
- h2 = h2 + h1 >>> 0;
133
- h3 = h3 + h1 >>> 0;
134
- h4 = h4 + h1 >>> 0;
135
- h1 = fmix32(h1);
136
- h2 = fmix32(h2);
137
- h3 = fmix32(h3);
138
- h4 = fmix32(h4);
139
- h1 = h1 + h2 >>> 0;
140
- h1 = h1 + h3 >>> 0;
141
- h1 = h1 + h4 >>> 0;
142
- h2 = h2 + h1 >>> 0;
143
- h3 = h3 + h1 >>> 0;
144
- h4 = h4 + h1 >>> 0;
145
- return [
146
- h1,
147
- h2,
148
- h3,
149
- h4
150
- ];
151
- }
152
- function toBase62(num, length) {
153
- let result = "";
154
- let remaining = num;
155
- for (let i = 0; i < length; i++) {
156
- result = BASE62_CHARS[remaining % 62] + result;
157
- remaining = Math.floor(remaining / 62);
158
- }
159
- return result;
160
- }
161
- /**
162
- * Computes a deterministic short key from source text and optional context.
163
- *
164
- * Same source + same context = same key (deterministic).
165
- * Same source + different context = different key (disambiguation).
166
- */
167
- function computeKey(source, context) {
168
- const input = context != null && context !== "" ? `${source}\0${context}` : source;
169
- const [h1, h2] = murmurhash3_x86_128(encoder.encode(input.normalize("NFC")));
170
- return toBase62(((h1 >>> 0) + (h2 >>> 16) * 4294967296) % BASE62_SPACE, KEY_LENGTH);
171
- }
172
- //#endregion
173
- //#region src/icu.ts
174
- /**
175
- * Canonical ICU MessageFormat expression builders.
176
- * Shared between runtime (l.plural/l.select) and build tools (extraction).
177
- *
178
- * These must produce identical output everywhere so that computeKey()
179
- * generates matching hash keys at build time and runtime.
180
- */
181
- const CLDR_PLURAL_ORDER = [
182
- "zero",
183
- "one",
184
- "two",
185
- "few",
186
- "many",
187
- "other"
188
- ];
189
- function buildIcuPlural(forms) {
190
- return `{count, plural, ${CLDR_PLURAL_ORDER.filter((cat) => forms[cat] !== void 0).map((cat) => `${cat} {${forms[cat]}}`).join(" ")}}`;
191
- }
192
- function buildIcuSelect(forms) {
193
- return `{value, select, ${Object.keys(forms).sort().map((key) => `${key} {${forms[key]}}`).join(" ")}}`;
194
- }
195
- //#endregion
196
- //#region src/jsonc.ts
197
- /**
198
- * JSONC locale file reader with structured metadata comments.
199
- * Lives in @lingo.dev/spec so framework adapters can parse locale files
200
- * without depending on @lingo.dev/cli.
201
- *
202
- * File format:
203
- * ```jsonc
204
- * {
205
- * /*
206
- * * @context Hero heading
207
- * * @src app/hero.tsx:12
208
- * *​/
209
- * "mK9xqZ": "Welcome to Acme"
210
- * }
211
- * ```
212
- */
213
- /** Filters out orphaned entries, returning only active (non-orphaned) ones. */
214
- function getActiveEntries(entries) {
215
- return entries.filter((e) => !e.metadata.orphan);
216
- }
217
- const METADATA_PATTERN = /@(\w+)(?:\s+(.*))?/;
218
- function parseMetadataBlock(comment) {
219
- const metadata = {};
220
- for (const line of comment.split("\n")) {
221
- const match = line.match(METADATA_PATTERN);
222
- if (!match) continue;
223
- const [, tag, value] = match;
224
- if (tag === "context" && value) metadata.context = value.trim();
225
- else if (tag === "src" && value) metadata.src = value.trim();
226
- else if (tag === "orphan") metadata.orphan = true;
227
- }
228
- return metadata;
229
- }
230
- function escapeRegex(str) {
231
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
232
- }
233
- /**
234
- * Scans JSONC source to associate block comments with the key that follows them.
235
- */
236
- function extractCommentsForKeys(content, keys) {
237
- const result = /* @__PURE__ */ new Map();
238
- const blockCommentPattern = /\/\*[\s\S]*?\*\//g;
239
- const comments = [];
240
- let match;
241
- while ((match = blockCommentPattern.exec(content)) !== null) comments.push({
242
- end: match.index + match[0].length,
243
- text: match[0]
244
- });
245
- for (const key of keys) {
246
- const keyMatch = new RegExp(`"${escapeRegex(key)}"\\s*:`).exec(content);
247
- if (!keyMatch) continue;
248
- const keyPos = keyMatch.index;
249
- const precedingComment = comments.filter((c) => c.end <= keyPos).sort((a, b) => b.end - a.end)[0];
250
- if (precedingComment) {
251
- const between = content.slice(precedingComment.end, keyPos);
252
- if (/^\s*$/.test(between)) result.set(key, parseMetadataBlock(precedingComment.text));
253
- }
254
- }
255
- return result;
256
- }
257
- /**
258
- * Parses a JSONC string into structured locale entries with metadata.
259
- * Extracts @context, @src, and @orphan from block comments preceding each key.
260
- * Pure function - no filesystem access. Works in Node.js and edge runtimes.
261
- */
262
- function readLocaleFile(content) {
263
- const errors = [];
264
- const data = (0, jsonc_parser.parse)(content, errors, { allowTrailingComma: true });
265
- if (errors.length > 0) {
266
- const msg = errors.map((e) => (0, jsonc_parser.printParseErrorCode)(e.error)).join(", ");
267
- throw new Error(`Failed to parse JSONC: ${msg}`);
268
- }
269
- if (!data || typeof data !== "object") return { entries: [] };
270
- const commentsByKey = extractCommentsForKeys(content, Object.keys(data));
271
- return { entries: Object.entries(data).map(([key, value]) => ({
272
- key,
273
- value,
274
- metadata: commentsByKey.get(key) ?? {}
275
- })) };
276
- }
277
- //#endregion
278
- exports.buildIcuPlural = buildIcuPlural;
279
- exports.buildIcuSelect = buildIcuSelect;
280
- exports.computeKey = computeKey;
281
- exports.getActiveEntries = getActiveEntries;
282
- exports.readLocaleFile = readLocaleFile;
package/dist/index.d.cts DELETED
@@ -1,77 +0,0 @@
1
- //#region src/hash.d.ts
2
- /**
3
- * Deterministic key generation from (source text, context) pairs.
4
- * Shared between runtime (message lookup) and build tools (extraction, JSONC generation).
5
- *
6
- * Uses MurmurHash3 x86_128 on NFC-normalized UTF-8 bytes.
7
- * Outputs an 8-character base62 string (e.g., "aB3dEf9x").
8
- *
9
- * Cross-platform deterministic: JS, Rust, Go, Python produce identical keys
10
- * when given the same (source, context) pair.
11
- *
12
- * 1% collision probability at ~2.4 million messages (48-bit effective entropy).
13
- */
14
- /**
15
- * Computes a deterministic short key from source text and optional context.
16
- *
17
- * Same source + same context = same key (deterministic).
18
- * Same source + different context = different key (disambiguation).
19
- */
20
- declare function computeKey(source: string, context?: string): string;
21
- //# sourceMappingURL=hash.d.ts.map
22
- //#endregion
23
- //#region src/icu.d.ts
24
- /**
25
- * Canonical ICU MessageFormat expression builders.
26
- * Shared between runtime (l.plural/l.select) and build tools (extraction).
27
- *
28
- * These must produce identical output everywhere so that computeKey()
29
- * generates matching hash keys at build time and runtime.
30
- */
31
- declare function buildIcuPlural(forms: Record<string, string>): string;
32
- declare function buildIcuSelect(forms: Record<string, string>): string;
33
- //# sourceMappingURL=icu.d.ts.map
34
-
35
- //#endregion
36
- //#region src/jsonc.d.ts
37
- /**
38
- * JSONC locale file reader with structured metadata comments.
39
- * Lives in @lingo.dev/spec so framework adapters can parse locale files
40
- * without depending on @lingo.dev/cli.
41
- *
42
- * File format:
43
- * ```jsonc
44
- * {
45
- * /*
46
- * * @context Hero heading
47
- * * @src app/hero.tsx:12
48
- * *​/
49
- * "mK9xqZ": "Welcome to Acme"
50
- * }
51
- * ```
52
- */
53
- type EntryMetadata = {
54
- context?: string;
55
- src?: string;
56
- orphan?: boolean;
57
- };
58
- type LocaleEntry = {
59
- key: string;
60
- value: string;
61
- metadata: EntryMetadata;
62
- };
63
- type LocaleFile = {
64
- entries: LocaleEntry[];
65
- };
66
- /** Filters out orphaned entries, returning only active (non-orphaned) ones. */
67
- declare function getActiveEntries(entries: LocaleEntry[]): LocaleEntry[];
68
- /**
69
- * Parses a JSONC string into structured locale entries with metadata.
70
- * Extracts @context, @src, and @orphan from block comments preceding each key.
71
- * Pure function - no filesystem access. Works in Node.js and edge runtimes.
72
- */
73
- declare function readLocaleFile(content: string): LocaleFile;
74
- //# sourceMappingURL=jsonc.d.ts.map
75
- //#endregion
76
- export { type EntryMetadata, type LocaleEntry, type LocaleFile, buildIcuPlural, buildIcuSelect, computeKey, getActiveEntries, readLocaleFile };
77
- //# sourceMappingURL=index.d.cts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../src/hash.ts","../src/icu.ts","../src/jsonc.ts"],"sourcesContent":[],"mappings":";;AAwLA;;;;AC9KA;AAOA;;;;ACIA;AAMA;AAMA;AAOA;;;;;AA8DgB,iBFkFA,UAAA,CElFiC,MAAA,EAAU,MAAA,EAAA,OAAA,CAAA,EAAA,MAAA,CAAA,EAAA,MAAA;;;;;AFkF3D;;;;AC9KA;AAOA;iBAPgB,cAAA,QAAsB;iBAOtB,cAAA,QAAsB;;;;;;ADuKtC;;;;AC9KA;AAOA;;;;ACIA;AAMA;AAMA;AAOA;;;AAA0D,KAnB9C,aAAA,GAmB8C;EAAW,OAAA,CAAA,EAAA,MAAA;EA8DrD,GAAA,CAAA,EAAA,MAAA;;;KA3EJ,WAAA;;;YAGA;;KAGA,UAAA;WACD;;;iBAMK,gBAAA,UAA0B,gBAAgB;;;;;;iBA8D1C,cAAA,mBAAiC"}
package/dist/index.d.ts DELETED
@@ -1,77 +0,0 @@
1
- //#region src/hash.d.ts
2
- /**
3
- * Deterministic key generation from (source text, context) pairs.
4
- * Shared between runtime (message lookup) and build tools (extraction, JSONC generation).
5
- *
6
- * Uses MurmurHash3 x86_128 on NFC-normalized UTF-8 bytes.
7
- * Outputs an 8-character base62 string (e.g., "aB3dEf9x").
8
- *
9
- * Cross-platform deterministic: JS, Rust, Go, Python produce identical keys
10
- * when given the same (source, context) pair.
11
- *
12
- * 1% collision probability at ~2.4 million messages (48-bit effective entropy).
13
- */
14
- /**
15
- * Computes a deterministic short key from source text and optional context.
16
- *
17
- * Same source + same context = same key (deterministic).
18
- * Same source + different context = different key (disambiguation).
19
- */
20
- declare function computeKey(source: string, context?: string): string;
21
- //# sourceMappingURL=hash.d.ts.map
22
- //#endregion
23
- //#region src/icu.d.ts
24
- /**
25
- * Canonical ICU MessageFormat expression builders.
26
- * Shared between runtime (l.plural/l.select) and build tools (extraction).
27
- *
28
- * These must produce identical output everywhere so that computeKey()
29
- * generates matching hash keys at build time and runtime.
30
- */
31
- declare function buildIcuPlural(forms: Record<string, string>): string;
32
- declare function buildIcuSelect(forms: Record<string, string>): string;
33
- //# sourceMappingURL=icu.d.ts.map
34
-
35
- //#endregion
36
- //#region src/jsonc.d.ts
37
- /**
38
- * JSONC locale file reader with structured metadata comments.
39
- * Lives in @lingo.dev/spec so framework adapters can parse locale files
40
- * without depending on @lingo.dev/cli.
41
- *
42
- * File format:
43
- * ```jsonc
44
- * {
45
- * /*
46
- * * @context Hero heading
47
- * * @src app/hero.tsx:12
48
- * *​/
49
- * "mK9xqZ": "Welcome to Acme"
50
- * }
51
- * ```
52
- */
53
- type EntryMetadata = {
54
- context?: string;
55
- src?: string;
56
- orphan?: boolean;
57
- };
58
- type LocaleEntry = {
59
- key: string;
60
- value: string;
61
- metadata: EntryMetadata;
62
- };
63
- type LocaleFile = {
64
- entries: LocaleEntry[];
65
- };
66
- /** Filters out orphaned entries, returning only active (non-orphaned) ones. */
67
- declare function getActiveEntries(entries: LocaleEntry[]): LocaleEntry[];
68
- /**
69
- * Parses a JSONC string into structured locale entries with metadata.
70
- * Extracts @context, @src, and @orphan from block comments preceding each key.
71
- * Pure function - no filesystem access. Works in Node.js and edge runtimes.
72
- */
73
- declare function readLocaleFile(content: string): LocaleFile;
74
- //# sourceMappingURL=jsonc.d.ts.map
75
- //#endregion
76
- export { type EntryMetadata, type LocaleEntry, type LocaleFile, buildIcuPlural, buildIcuSelect, computeKey, getActiveEntries, readLocaleFile };
77
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/hash.ts","../src/icu.ts","../src/jsonc.ts"],"sourcesContent":[],"mappings":";;AAwLA;;;;AC9KA;AAOA;;;;ACIA;AAMA;AAMA;AAOA;;;;;AA8DgB,iBFkFA,UAAA,CElFiC,MAAA,EAAU,MAAA,EAAA,OAAA,CAAA,EAAA,MAAA,CAAA,EAAA,MAAA;;;;;AFkF3D;;;;AC9KA;AAOA;iBAPgB,cAAA,QAAsB;iBAOtB,cAAA,QAAsB;;;;;;ADuKtC;;;;AC9KA;AAOA;;;;ACIA;AAMA;AAMA;AAOA;;;AAA0D,KAnB9C,aAAA,GAmB8C;EAAW,OAAA,CAAA,EAAA,MAAA;EA8DrD,GAAA,CAAA,EAAA,MAAA;;;KA3EJ,WAAA;;;YAGA;;KAGA,UAAA;WACD;;;iBAMK,gBAAA,UAA0B,gBAAgB;;;;;;iBA8D1C,cAAA,mBAAiC"}
package/dist/index.js DELETED
@@ -1,279 +0,0 @@
1
- import { parse, printParseErrorCode } from "jsonc-parser";
2
- //#region src/hash.ts
3
- /**
4
- * Deterministic key generation from (source text, context) pairs.
5
- * Shared between runtime (message lookup) and build tools (extraction, JSONC generation).
6
- *
7
- * Uses MurmurHash3 x86_128 on NFC-normalized UTF-8 bytes.
8
- * Outputs an 8-character base62 string (e.g., "aB3dEf9x").
9
- *
10
- * Cross-platform deterministic: JS, Rust, Go, Python produce identical keys
11
- * when given the same (source, context) pair.
12
- *
13
- * 1% collision probability at ~2.4 million messages (48-bit effective entropy).
14
- */
15
- const BASE62_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
16
- const KEY_LENGTH = 8;
17
- const BASE62_SPACE = 62 ** KEY_LENGTH;
18
- const encoder = new TextEncoder();
19
- const C1 = 597399067;
20
- const C2 = 2869860233;
21
- const C3 = 951274213;
22
- const C4 = 2716044179;
23
- function rotl32(x, r) {
24
- return (x << r | x >>> 32 - r) >>> 0;
25
- }
26
- function fmix32(h) {
27
- h ^= h >>> 16;
28
- h = Math.imul(h, 2246822507) >>> 0;
29
- h ^= h >>> 13;
30
- h = Math.imul(h, 3266489909) >>> 0;
31
- h ^= h >>> 16;
32
- return h >>> 0;
33
- }
34
- function readU32LE(bytes, i) {
35
- return (bytes[i] | bytes[i + 1] << 8 | bytes[i + 2] << 16 | bytes[i + 3] << 24) >>> 0;
36
- }
37
- function murmurhash3_x86_128(bytes) {
38
- const len = bytes.length;
39
- const nblocks = len >>> 4;
40
- let h1 = 0;
41
- let h2 = 0;
42
- let h3 = 0;
43
- let h4 = 0;
44
- for (let i = 0; i < nblocks; i++) {
45
- const off = i << 4;
46
- let k1 = readU32LE(bytes, off);
47
- let k2 = readU32LE(bytes, off + 4);
48
- let k3 = readU32LE(bytes, off + 8);
49
- let k4 = readU32LE(bytes, off + 12);
50
- k1 = Math.imul(k1, C1) >>> 0;
51
- k1 = rotl32(k1, 15);
52
- k1 = Math.imul(k1, C2) >>> 0;
53
- h1 ^= k1;
54
- h1 = rotl32(h1, 19);
55
- h1 = h1 + h2 >>> 0;
56
- h1 = Math.imul(h1, 5) + 1444728091 >>> 0;
57
- k2 = Math.imul(k2, C2) >>> 0;
58
- k2 = rotl32(k2, 16);
59
- k2 = Math.imul(k2, C3) >>> 0;
60
- h2 ^= k2;
61
- h2 = rotl32(h2, 17);
62
- h2 = h2 + h3 >>> 0;
63
- h2 = Math.imul(h2, 5) + 197830471 >>> 0;
64
- k3 = Math.imul(k3, C3) >>> 0;
65
- k3 = rotl32(k3, 17);
66
- k3 = Math.imul(k3, C4) >>> 0;
67
- h3 ^= k3;
68
- h3 = rotl32(h3, 15);
69
- h3 = h3 + h4 >>> 0;
70
- h3 = Math.imul(h3, 5) + 2530024501 >>> 0;
71
- k4 = Math.imul(k4, C4) >>> 0;
72
- k4 = rotl32(k4, 18);
73
- k4 = Math.imul(k4, C1) >>> 0;
74
- h4 ^= k4;
75
- h4 = rotl32(h4, 13);
76
- h4 = h4 + h1 >>> 0;
77
- h4 = Math.imul(h4, 5) + 850148119 >>> 0;
78
- }
79
- const tail = nblocks << 4;
80
- let k1 = 0;
81
- let k2 = 0;
82
- let k3 = 0;
83
- let k4 = 0;
84
- const rem = len & 15;
85
- if (rem >= 15) k4 ^= bytes[tail + 14] << 16;
86
- if (rem >= 14) k4 ^= bytes[tail + 13] << 8;
87
- if (rem >= 13) {
88
- k4 ^= bytes[tail + 12];
89
- k4 = Math.imul(k4, C4) >>> 0;
90
- k4 = rotl32(k4, 18);
91
- k4 = Math.imul(k4, C1) >>> 0;
92
- h4 ^= k4;
93
- }
94
- if (rem >= 12) k3 ^= bytes[tail + 11] << 24;
95
- if (rem >= 11) k3 ^= bytes[tail + 10] << 16;
96
- if (rem >= 10) k3 ^= bytes[tail + 9] << 8;
97
- if (rem >= 9) {
98
- k3 ^= bytes[tail + 8];
99
- k3 = Math.imul(k3, C3) >>> 0;
100
- k3 = rotl32(k3, 17);
101
- k3 = Math.imul(k3, C4) >>> 0;
102
- h3 ^= k3;
103
- }
104
- if (rem >= 8) k2 ^= bytes[tail + 7] << 24;
105
- if (rem >= 7) k2 ^= bytes[tail + 6] << 16;
106
- if (rem >= 6) k2 ^= bytes[tail + 5] << 8;
107
- if (rem >= 5) {
108
- k2 ^= bytes[tail + 4];
109
- k2 = Math.imul(k2, C2) >>> 0;
110
- k2 = rotl32(k2, 16);
111
- k2 = Math.imul(k2, C3) >>> 0;
112
- h2 ^= k2;
113
- }
114
- if (rem >= 4) k1 ^= bytes[tail + 3] << 24;
115
- if (rem >= 3) k1 ^= bytes[tail + 2] << 16;
116
- if (rem >= 2) k1 ^= bytes[tail + 1] << 8;
117
- if (rem >= 1) {
118
- k1 ^= bytes[tail];
119
- k1 = Math.imul(k1, C1) >>> 0;
120
- k1 = rotl32(k1, 15);
121
- k1 = Math.imul(k1, C2) >>> 0;
122
- h1 ^= k1;
123
- }
124
- h1 ^= len;
125
- h2 ^= len;
126
- h3 ^= len;
127
- h4 ^= len;
128
- h1 = h1 + h2 >>> 0;
129
- h1 = h1 + h3 >>> 0;
130
- h1 = h1 + h4 >>> 0;
131
- h2 = h2 + h1 >>> 0;
132
- h3 = h3 + h1 >>> 0;
133
- h4 = h4 + h1 >>> 0;
134
- h1 = fmix32(h1);
135
- h2 = fmix32(h2);
136
- h3 = fmix32(h3);
137
- h4 = fmix32(h4);
138
- h1 = h1 + h2 >>> 0;
139
- h1 = h1 + h3 >>> 0;
140
- h1 = h1 + h4 >>> 0;
141
- h2 = h2 + h1 >>> 0;
142
- h3 = h3 + h1 >>> 0;
143
- h4 = h4 + h1 >>> 0;
144
- return [
145
- h1,
146
- h2,
147
- h3,
148
- h4
149
- ];
150
- }
151
- function toBase62(num, length) {
152
- let result = "";
153
- let remaining = num;
154
- for (let i = 0; i < length; i++) {
155
- result = BASE62_CHARS[remaining % 62] + result;
156
- remaining = Math.floor(remaining / 62);
157
- }
158
- return result;
159
- }
160
- /**
161
- * Computes a deterministic short key from source text and optional context.
162
- *
163
- * Same source + same context = same key (deterministic).
164
- * Same source + different context = different key (disambiguation).
165
- */
166
- function computeKey(source, context) {
167
- const input = context != null && context !== "" ? `${source}\0${context}` : source;
168
- const [h1, h2] = murmurhash3_x86_128(encoder.encode(input.normalize("NFC")));
169
- return toBase62(((h1 >>> 0) + (h2 >>> 16) * 4294967296) % BASE62_SPACE, KEY_LENGTH);
170
- }
171
- //#endregion
172
- //#region src/icu.ts
173
- /**
174
- * Canonical ICU MessageFormat expression builders.
175
- * Shared between runtime (l.plural/l.select) and build tools (extraction).
176
- *
177
- * These must produce identical output everywhere so that computeKey()
178
- * generates matching hash keys at build time and runtime.
179
- */
180
- const CLDR_PLURAL_ORDER = [
181
- "zero",
182
- "one",
183
- "two",
184
- "few",
185
- "many",
186
- "other"
187
- ];
188
- function buildIcuPlural(forms) {
189
- return `{count, plural, ${CLDR_PLURAL_ORDER.filter((cat) => forms[cat] !== void 0).map((cat) => `${cat} {${forms[cat]}}`).join(" ")}}`;
190
- }
191
- function buildIcuSelect(forms) {
192
- return `{value, select, ${Object.keys(forms).sort().map((key) => `${key} {${forms[key]}}`).join(" ")}}`;
193
- }
194
- //#endregion
195
- //#region src/jsonc.ts
196
- /**
197
- * JSONC locale file reader with structured metadata comments.
198
- * Lives in @lingo.dev/spec so framework adapters can parse locale files
199
- * without depending on @lingo.dev/cli.
200
- *
201
- * File format:
202
- * ```jsonc
203
- * {
204
- * /*
205
- * * @context Hero heading
206
- * * @src app/hero.tsx:12
207
- * *​/
208
- * "mK9xqZ": "Welcome to Acme"
209
- * }
210
- * ```
211
- */
212
- /** Filters out orphaned entries, returning only active (non-orphaned) ones. */
213
- function getActiveEntries(entries) {
214
- return entries.filter((e) => !e.metadata.orphan);
215
- }
216
- const METADATA_PATTERN = /@(\w+)(?:\s+(.*))?/;
217
- function parseMetadataBlock(comment) {
218
- const metadata = {};
219
- for (const line of comment.split("\n")) {
220
- const match = line.match(METADATA_PATTERN);
221
- if (!match) continue;
222
- const [, tag, value] = match;
223
- if (tag === "context" && value) metadata.context = value.trim();
224
- else if (tag === "src" && value) metadata.src = value.trim();
225
- else if (tag === "orphan") metadata.orphan = true;
226
- }
227
- return metadata;
228
- }
229
- function escapeRegex(str) {
230
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
231
- }
232
- /**
233
- * Scans JSONC source to associate block comments with the key that follows them.
234
- */
235
- function extractCommentsForKeys(content, keys) {
236
- const result = /* @__PURE__ */ new Map();
237
- const blockCommentPattern = /\/\*[\s\S]*?\*\//g;
238
- const comments = [];
239
- let match;
240
- while ((match = blockCommentPattern.exec(content)) !== null) comments.push({
241
- end: match.index + match[0].length,
242
- text: match[0]
243
- });
244
- for (const key of keys) {
245
- const keyMatch = new RegExp(`"${escapeRegex(key)}"\\s*:`).exec(content);
246
- if (!keyMatch) continue;
247
- const keyPos = keyMatch.index;
248
- const precedingComment = comments.filter((c) => c.end <= keyPos).sort((a, b) => b.end - a.end)[0];
249
- if (precedingComment) {
250
- const between = content.slice(precedingComment.end, keyPos);
251
- if (/^\s*$/.test(between)) result.set(key, parseMetadataBlock(precedingComment.text));
252
- }
253
- }
254
- return result;
255
- }
256
- /**
257
- * Parses a JSONC string into structured locale entries with metadata.
258
- * Extracts @context, @src, and @orphan from block comments preceding each key.
259
- * Pure function - no filesystem access. Works in Node.js and edge runtimes.
260
- */
261
- function readLocaleFile(content) {
262
- const errors = [];
263
- const data = parse(content, errors, { allowTrailingComma: true });
264
- if (errors.length > 0) {
265
- const msg = errors.map((e) => printParseErrorCode(e.error)).join(", ");
266
- throw new Error(`Failed to parse JSONC: ${msg}`);
267
- }
268
- if (!data || typeof data !== "object") return { entries: [] };
269
- const commentsByKey = extractCommentsForKeys(content, Object.keys(data));
270
- return { entries: Object.entries(data).map(([key, value]) => ({
271
- key,
272
- value,
273
- metadata: commentsByKey.get(key) ?? {}
274
- })) };
275
- }
276
- //#endregion
277
- export { buildIcuPlural, buildIcuSelect, computeKey, getActiveEntries, readLocaleFile };
278
-
279
- //# sourceMappingURL=index.js.map
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","names":["parseJsonc"],"sources":["../src/hash.ts","../src/icu.ts","../src/jsonc.ts"],"sourcesContent":["/**\n * Deterministic key generation from (source text, context) pairs.\n * Shared between runtime (message lookup) and build tools (extraction, JSONC generation).\n *\n * Uses MurmurHash3 x86_128 on NFC-normalized UTF-8 bytes.\n * Outputs an 8-character base62 string (e.g., \"aB3dEf9x\").\n *\n * Cross-platform deterministic: JS, Rust, Go, Python produce identical keys\n * when given the same (source, context) pair.\n *\n * 1% collision probability at ~2.4 million messages (48-bit effective entropy).\n */\n\nconst BASE62_CHARS = \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\";\nconst KEY_LENGTH = 8;\nconst BASE62_SPACE = 62 ** KEY_LENGTH;\n\nconst encoder = new TextEncoder();\n\n// --- MurmurHash3 x86_128 (reference: github.com/aappleby/smhasher) ---\n\nconst C1 = 0x239b961b;\nconst C2 = 0xab0e9789;\nconst C3 = 0x38b34ae5;\nconst C4 = 0xa1e38b93;\n\nfunction rotl32(x: number, r: number): number {\n return ((x << r) | (x >>> (32 - r))) >>> 0;\n}\n\nfunction fmix32(h: number): number {\n h ^= h >>> 16;\n h = Math.imul(h, 0x85ebca6b) >>> 0;\n h ^= h >>> 13;\n h = Math.imul(h, 0xc2b2ae35) >>> 0;\n h ^= h >>> 16;\n return h >>> 0;\n}\n\nfunction readU32LE(bytes: Uint8Array, i: number): number {\n return (bytes[i] | (bytes[i + 1] << 8) | (bytes[i + 2] << 16) | (bytes[i + 3] << 24)) >>> 0;\n}\n\nfunction murmurhash3_x86_128(bytes: Uint8Array): [number, number, number, number] {\n const len = bytes.length;\n const nblocks = len >>> 4;\n\n let h1 = 0;\n let h2 = 0;\n let h3 = 0;\n let h4 = 0;\n\n for (let i = 0; i < nblocks; i++) {\n const off = i << 4;\n let k1 = readU32LE(bytes, off);\n let k2 = readU32LE(bytes, off + 4);\n let k3 = readU32LE(bytes, off + 8);\n let k4 = readU32LE(bytes, off + 12);\n\n k1 = Math.imul(k1, C1) >>> 0;\n k1 = rotl32(k1, 15);\n k1 = Math.imul(k1, C2) >>> 0;\n h1 ^= k1;\n h1 = rotl32(h1, 19);\n h1 = (h1 + h2) >>> 0;\n h1 = (Math.imul(h1, 5) + 0x561ccd1b) >>> 0;\n\n k2 = Math.imul(k2, C2) >>> 0;\n k2 = rotl32(k2, 16);\n k2 = Math.imul(k2, C3) >>> 0;\n h2 ^= k2;\n h2 = rotl32(h2, 17);\n h2 = (h2 + h3) >>> 0;\n h2 = (Math.imul(h2, 5) + 0x0bcaa747) >>> 0;\n\n k3 = Math.imul(k3, C3) >>> 0;\n k3 = rotl32(k3, 17);\n k3 = Math.imul(k3, C4) >>> 0;\n h3 ^= k3;\n h3 = rotl32(h3, 15);\n h3 = (h3 + h4) >>> 0;\n h3 = (Math.imul(h3, 5) + 0x96cd1c35) >>> 0;\n\n k4 = Math.imul(k4, C4) >>> 0;\n k4 = rotl32(k4, 18);\n k4 = Math.imul(k4, C1) >>> 0;\n h4 ^= k4;\n h4 = rotl32(h4, 13);\n h4 = (h4 + h1) >>> 0;\n h4 = (Math.imul(h4, 5) + 0x32ac3b17) >>> 0;\n }\n\n const tail = nblocks << 4;\n let k1 = 0;\n let k2 = 0;\n let k3 = 0;\n let k4 = 0;\n const rem = len & 15;\n\n if (rem >= 15) k4 ^= bytes[tail + 14] << 16;\n if (rem >= 14) k4 ^= bytes[tail + 13] << 8;\n if (rem >= 13) {\n k4 ^= bytes[tail + 12];\n k4 = Math.imul(k4, C4) >>> 0;\n k4 = rotl32(k4, 18);\n k4 = Math.imul(k4, C1) >>> 0;\n h4 ^= k4;\n }\n if (rem >= 12) k3 ^= bytes[tail + 11] << 24;\n if (rem >= 11) k3 ^= bytes[tail + 10] << 16;\n if (rem >= 10) k3 ^= bytes[tail + 9] << 8;\n if (rem >= 9) {\n k3 ^= bytes[tail + 8];\n k3 = Math.imul(k3, C3) >>> 0;\n k3 = rotl32(k3, 17);\n k3 = Math.imul(k3, C4) >>> 0;\n h3 ^= k3;\n }\n if (rem >= 8) k2 ^= bytes[tail + 7] << 24;\n if (rem >= 7) k2 ^= bytes[tail + 6] << 16;\n if (rem >= 6) k2 ^= bytes[tail + 5] << 8;\n if (rem >= 5) {\n k2 ^= bytes[tail + 4];\n k2 = Math.imul(k2, C2) >>> 0;\n k2 = rotl32(k2, 16);\n k2 = Math.imul(k2, C3) >>> 0;\n h2 ^= k2;\n }\n if (rem >= 4) k1 ^= bytes[tail + 3] << 24;\n if (rem >= 3) k1 ^= bytes[tail + 2] << 16;\n if (rem >= 2) k1 ^= bytes[tail + 1] << 8;\n if (rem >= 1) {\n k1 ^= bytes[tail];\n k1 = Math.imul(k1, C1) >>> 0;\n k1 = rotl32(k1, 15);\n k1 = Math.imul(k1, C2) >>> 0;\n h1 ^= k1;\n }\n\n h1 ^= len;\n h2 ^= len;\n h3 ^= len;\n h4 ^= len;\n\n h1 = (h1 + h2) >>> 0;\n h1 = (h1 + h3) >>> 0;\n h1 = (h1 + h4) >>> 0;\n h2 = (h2 + h1) >>> 0;\n h3 = (h3 + h1) >>> 0;\n h4 = (h4 + h1) >>> 0;\n\n h1 = fmix32(h1);\n h2 = fmix32(h2);\n h3 = fmix32(h3);\n h4 = fmix32(h4);\n\n h1 = (h1 + h2) >>> 0;\n h1 = (h1 + h3) >>> 0;\n h1 = (h1 + h4) >>> 0;\n h2 = (h2 + h1) >>> 0;\n h3 = (h3 + h1) >>> 0;\n h4 = (h4 + h1) >>> 0;\n\n return [h1, h2, h3, h4];\n}\n\n// --- Key generation ---\n\nfunction toBase62(num: number, length: number): string {\n let result = \"\";\n let remaining = num;\n for (let i = 0; i < length; i++) {\n result = BASE62_CHARS[remaining % 62] + result;\n remaining = Math.floor(remaining / 62);\n }\n return result;\n}\n\n/**\n * Computes a deterministic short key from source text and optional context.\n *\n * Same source + same context = same key (deterministic).\n * Same source + different context = different key (disambiguation).\n */\nexport function computeKey(source: string, context?: string): string {\n const input = context != null && context !== \"\" ? `${source}\\0${context}` : source;\n const bytes = encoder.encode(input.normalize(\"NFC\"));\n const [h1, h2] = murmurhash3_x86_128(bytes);\n\n // 48-bit value: h1 (32 bits) + upper 16 bits of h2\n // Safe for JS Number arithmetic (well within 2^53)\n const value = (h1 >>> 0) + (h2 >>> 16) * 0x100000000;\n\n return toBase62(value % BASE62_SPACE, KEY_LENGTH);\n}\n","/**\n * Canonical ICU MessageFormat expression builders.\n * Shared between runtime (l.plural/l.select) and build tools (extraction).\n *\n * These must produce identical output everywhere so that computeKey()\n * generates matching hash keys at build time and runtime.\n */\n\nconst CLDR_PLURAL_ORDER = [\"zero\", \"one\", \"two\", \"few\", \"many\", \"other\"];\n\nexport function buildIcuPlural(forms: Record<string, string>): string {\n const parts = CLDR_PLURAL_ORDER.filter((cat) => forms[cat] !== undefined)\n .map((cat) => `${cat} {${forms[cat]!}}`)\n .join(\" \");\n return `{count, plural, ${parts}}`;\n}\n\nexport function buildIcuSelect(forms: Record<string, string>): string {\n const parts = Object.keys(forms)\n .sort()\n .map((key) => `${key} {${forms[key]}}`)\n .join(\" \");\n return `{value, select, ${parts}}`;\n}\n","/**\n * JSONC locale file reader with structured metadata comments.\n * Lives in @lingo.dev/spec so framework adapters can parse locale files\n * without depending on @lingo.dev/cli.\n *\n * File format:\n * ```jsonc\n * {\n * /*\n * * @context Hero heading\n * * @src app/hero.tsx:12\n * *​/\n * \"mK9xqZ\": \"Welcome to Acme\"\n * }\n * ```\n */\n\nimport { parse as parseJsonc, type ParseError, printParseErrorCode } from \"jsonc-parser\";\n\n// --- Types (shared between reader in spec and writer in cli) ---\n\nexport type EntryMetadata = {\n context?: string;\n src?: string;\n orphan?: boolean;\n};\n\nexport type LocaleEntry = {\n key: string;\n value: string;\n metadata: EntryMetadata;\n};\n\nexport type LocaleFile = {\n entries: LocaleEntry[];\n};\n\n// --- Helpers ---\n\n/** Filters out orphaned entries, returning only active (non-orphaned) ones. */\nexport function getActiveEntries(entries: LocaleEntry[]): LocaleEntry[] {\n return entries.filter((e) => !e.metadata.orphan);\n}\n\n// --- Reader ---\n\nconst METADATA_PATTERN = /@(\\w+)(?:\\s+(.*))?/;\n\nfunction parseMetadataBlock(comment: string): EntryMetadata {\n const metadata: EntryMetadata = {};\n for (const line of comment.split(\"\\n\")) {\n const match = line.match(METADATA_PATTERN);\n if (!match) continue;\n const [, tag, value] = match;\n if (tag === \"context\" && value) metadata.context = value.trim();\n else if (tag === \"src\" && value) metadata.src = value.trim();\n else if (tag === \"orphan\") metadata.orphan = true;\n }\n return metadata;\n}\n\nfunction escapeRegex(str: string): string {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n}\n\n/**\n * Scans JSONC source to associate block comments with the key that follows them.\n */\nfunction extractCommentsForKeys(content: string, keys: string[]): Map<string, EntryMetadata> {\n const result = new Map<string, EntryMetadata>();\n const blockCommentPattern = /\\/\\*[\\s\\S]*?\\*\\//g;\n\n const comments: { end: number; text: string }[] = [];\n let match: RegExpExecArray | null;\n while ((match = blockCommentPattern.exec(content)) !== null) {\n comments.push({ end: match.index + match[0].length, text: match[0] });\n }\n\n for (const key of keys) {\n const keyPattern = new RegExp(`\"${escapeRegex(key)}\"\\\\s*:`);\n const keyMatch = keyPattern.exec(content);\n if (!keyMatch) continue;\n\n const keyPos = keyMatch.index;\n const precedingComment = comments.filter((c) => c.end <= keyPos).sort((a, b) => b.end - a.end)[0];\n\n if (precedingComment) {\n const between = content.slice(precedingComment.end, keyPos);\n if (/^\\s*$/.test(between)) {\n result.set(key, parseMetadataBlock(precedingComment.text));\n }\n }\n }\n\n return result;\n}\n\n/**\n * Parses a JSONC string into structured locale entries with metadata.\n * Extracts @context, @src, and @orphan from block comments preceding each key.\n * Pure function - no filesystem access. Works in Node.js and edge runtimes.\n */\nexport function readLocaleFile(content: string): LocaleFile {\n const errors: ParseError[] = [];\n const data = parseJsonc(content, errors, { allowTrailingComma: true }) as Record<string, string> | undefined;\n\n if (errors.length > 0) {\n const msg = errors.map((e) => printParseErrorCode(e.error)).join(\", \");\n throw new Error(`Failed to parse JSONC: ${msg}`);\n }\n\n if (!data || typeof data !== \"object\") {\n return { entries: [] };\n }\n\n const commentsByKey = extractCommentsForKeys(content, Object.keys(data));\n\n const entries: LocaleEntry[] = Object.entries(data).map(([key, value]) => ({\n key,\n value,\n metadata: commentsByKey.get(key) ?? {},\n }));\n\n return { entries };\n}\n"],"mappings":"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"}