@lingo.dev/spec 1.0.1 → 1.0.2
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/dist/index.cjs +282 -0
- package/dist/index.d.cts +77 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +279 -0
- package/dist/index.js.map +1 -0
- package/package.json +1 -1
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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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"}
|