@linguanahq/catalog 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +25 -0
  2. package/src/index.ts +219 -0
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@linguanahq/catalog",
3
+ "version": "0.1.0",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "type": "module",
8
+ "exports": {
9
+ ".": "./src/index.ts"
10
+ },
11
+ "scripts": {
12
+ "check-types": "tsc --noEmit",
13
+ "test": "bun test"
14
+ },
15
+ "dependencies": {
16
+ "@formatjs/icu-messageformat-parser": "^2.11.4",
17
+ "@noble/hashes": "^2.0.1",
18
+ "zod": "^4.4.3"
19
+ },
20
+ "devDependencies": {
21
+ "@linguana/config": "0.0.0",
22
+ "@types/bun": "^1.3.14",
23
+ "typescript": "^6"
24
+ }
25
+ }
package/src/index.ts ADDED
@@ -0,0 +1,219 @@
1
+ import {
2
+ type MessageFormatElement,
3
+ parse,
4
+ TYPE,
5
+ } from "@formatjs/icu-messageformat-parser";
6
+ import { sha256 } from "@noble/hashes/sha2.js";
7
+ import { bytesToHex, utf8ToBytes } from "@noble/hashes/utils.js";
8
+ import { z } from "zod";
9
+
10
+ export const SCHEMA_VERSION = "1.0" as const;
11
+ export const MANIFEST_SCHEMA_VERSION = "2.0" as const;
12
+
13
+ export const messageLocationSchema = z.object({
14
+ file: z.string().min(1),
15
+ line: z.number().int().positive(),
16
+ column: z.number().int().nonnegative(),
17
+ });
18
+
19
+ export const placeholderSchema = z.object({
20
+ name: z.string().min(1),
21
+ kind: z.enum([
22
+ "value",
23
+ "component",
24
+ "plural",
25
+ "select",
26
+ "number",
27
+ "date",
28
+ "time",
29
+ ]),
30
+ });
31
+
32
+ export const messageContextSchema = z.object({
33
+ kind: z.enum(["text", "attribute", "explicit"]),
34
+ element: z.string().optional(),
35
+ attribute: z.string().optional(),
36
+ developer: z.string().optional(),
37
+ });
38
+
39
+ export const manifestMessageSchema = z.object({
40
+ id: z.string().min(1),
41
+ source: z.string().min(1),
42
+ normalized: z.string().min(1),
43
+ contentHash: z.string().length(64),
44
+ context: messageContextSchema,
45
+ placeholders: z.array(placeholderSchema).default([]),
46
+ confidence: z.enum(["high", "explicit"]),
47
+ locations: z.array(messageLocationSchema).min(1),
48
+ });
49
+
50
+ export const diagnosticSchema = z.object({
51
+ code: z.string().min(1),
52
+ severity: z.enum(["info", "warning", "error"]),
53
+ message: z.string().min(1),
54
+ location: messageLocationSchema.optional(),
55
+ remediation: z.string().optional(),
56
+ });
57
+
58
+ export const manifestIntegrationSchema = z.object({
59
+ tool: z.literal("vite"),
60
+ toolVersion: z.string().min(1),
61
+ framework: z.enum(["react", "vue", "svelte", "solid"]),
62
+ adapterVersion: z.string().min(1),
63
+ ssr: z.boolean(),
64
+ });
65
+
66
+ export const manifestSchema = z
67
+ .object({
68
+ schemaVersion: z.enum([SCHEMA_VERSION, MANIFEST_SCHEMA_VERSION]),
69
+ compilerVersion: z.string().min(1),
70
+ projectId: z.string().min(1),
71
+ environment: z.string().min(1),
72
+ sourceLocale: z.string().min(2),
73
+ sourceRevision: z.string().optional(),
74
+ integration: manifestIntegrationSchema.optional(),
75
+ messages: z.array(manifestMessageSchema),
76
+ diagnostics: z.array(diagnosticSchema).default([]),
77
+ })
78
+ .superRefine((manifest, context) => {
79
+ if (
80
+ manifest.schemaVersion === MANIFEST_SCHEMA_VERSION &&
81
+ !manifest.integration
82
+ ) {
83
+ context.addIssue({
84
+ code: "custom",
85
+ path: ["integration"],
86
+ message: "Manifest schema 2.0 requires integration metadata.",
87
+ });
88
+ }
89
+ });
90
+
91
+ export const catalogSchema = z.object({
92
+ schemaVersion: z.literal(SCHEMA_VERSION),
93
+ projectId: z.string().min(1),
94
+ environment: z.string().min(1),
95
+ sourceLocale: z.string().min(2),
96
+ locale: z.string().min(2),
97
+ manifestHash: z.string().length(64),
98
+ version: z.number().int().positive(),
99
+ messages: z.record(z.string(), z.string()),
100
+ integrity: z.string().length(64),
101
+ });
102
+
103
+ export type MessageLocation = z.infer<typeof messageLocationSchema>;
104
+ export type Placeholder = z.infer<typeof placeholderSchema>;
105
+ export type MessageContext = z.infer<typeof messageContextSchema>;
106
+ export type ManifestMessage = z.infer<typeof manifestMessageSchema>;
107
+ export type Diagnostic = z.infer<typeof diagnosticSchema>;
108
+ export type Manifest = z.infer<typeof manifestSchema>;
109
+ export type ManifestIntegration = z.infer<typeof manifestIntegrationSchema>;
110
+ export type Catalog = z.infer<typeof catalogSchema>;
111
+
112
+ function sortValue(value: unknown): unknown {
113
+ if (Array.isArray(value)) return value.map(sortValue);
114
+ if (value && typeof value === "object") {
115
+ return Object.fromEntries(
116
+ Object.entries(value)
117
+ .sort(([left], [right]) => left.localeCompare(right))
118
+ .map(([key, item]) => [key, sortValue(item)]),
119
+ );
120
+ }
121
+ return value;
122
+ }
123
+
124
+ export function canonicalJson(value: unknown): string {
125
+ return JSON.stringify(sortValue(value));
126
+ }
127
+
128
+ export function contentHash(value: unknown): string {
129
+ const input = typeof value === "string" ? value : canonicalJson(value);
130
+ return bytesToHex(sha256(utf8ToBytes(input)));
131
+ }
132
+
133
+ export function normalizeSource(source: string): string {
134
+ return source.replace(/\s+/g, " ").trim();
135
+ }
136
+
137
+ export function parseMessagePlaceholders(source: string): Placeholder[] {
138
+ const descriptors = new Map<string, Placeholder>();
139
+ const priority: Record<Placeholder["kind"], number> = {
140
+ value: 0,
141
+ number: 1,
142
+ date: 1,
143
+ time: 1,
144
+ select: 2,
145
+ plural: 3,
146
+ component: 4,
147
+ };
148
+ const add = (name: string, kind: Placeholder["kind"]) => {
149
+ const existing = descriptors.get(name);
150
+ if (!existing || priority[kind] > priority[existing.kind])
151
+ descriptors.set(name, { name, kind });
152
+ };
153
+ const visit = (elements: MessageFormatElement[]) => {
154
+ for (const element of elements) {
155
+ switch (element.type) {
156
+ case TYPE.argument:
157
+ add(element.value, "value");
158
+ break;
159
+ case TYPE.number:
160
+ add(element.value, "number");
161
+ break;
162
+ case TYPE.date:
163
+ add(element.value, "date");
164
+ break;
165
+ case TYPE.time:
166
+ add(element.value, "time");
167
+ break;
168
+ case TYPE.select:
169
+ add(element.value, "select");
170
+ for (const option of Object.values(element.options))
171
+ visit(option.value);
172
+ break;
173
+ case TYPE.plural:
174
+ add(element.value, "plural");
175
+ for (const option of Object.values(element.options))
176
+ visit(option.value);
177
+ break;
178
+ case TYPE.tag:
179
+ add(element.value, "component");
180
+ visit(element.children);
181
+ break;
182
+ }
183
+ }
184
+ };
185
+ visit(parse(source, { captureLocation: false, ignoreTag: false }));
186
+ return [...descriptors.values()].sort((left, right) =>
187
+ left.name.localeCompare(right.name),
188
+ );
189
+ }
190
+
191
+ export function stableMessageId(input: {
192
+ sourceLocale: string;
193
+ source: string;
194
+ context: MessageContext;
195
+ explicitId?: string;
196
+ }): string {
197
+ if (input.explicitId) return input.explicitId;
198
+ const descriptor = {
199
+ version: 1,
200
+ sourceLocale: input.sourceLocale,
201
+ source: normalizeSource(input.source),
202
+ context: input.context,
203
+ };
204
+ return `lna_${contentHash(descriptor).slice(0, 24)}`;
205
+ }
206
+
207
+ export function manifestHash(manifest: Manifest): string {
208
+ return contentHash(manifestSchema.parse(manifest));
209
+ }
210
+
211
+ export function createCatalog(input: Omit<Catalog, "integrity">): Catalog {
212
+ const integrity = contentHash(input);
213
+ return catalogSchema.parse({ ...input, integrity });
214
+ }
215
+
216
+ export function verifyCatalog(catalog: Catalog): boolean {
217
+ const { integrity, ...payload } = catalogSchema.parse(catalog);
218
+ return contentHash(payload) === integrity;
219
+ }