@vouchington/localization-compiler 0.0.0 → 0.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/README.md CHANGED
@@ -4,6 +4,35 @@ Node-only compiler for `@vouchington/localization`. It validates namespace-shard
4
4
  requires complete `en-US` with sparse partial locales, emits an immutable read-only SQLite
5
5
  artifact, and exposes the same consumer/locale/selector resolver used by application CLIs.
6
6
 
7
+ Catalog shards must be a JSON array with one compact message per line (`id` first, sorted).
8
+ `compile` rejects any other layout. Line tools never parse the shard as a JSON document:
9
+
10
+ ```bash
11
+ vouchington-localization upsert --file localization/catalog/common.json --message '{"id":"common.ok","consumers":["web"],"descriptor":null,"translations":{"en-US":"OK"}}'
12
+ vouchington-localization remove --file localization/catalog/common.json --id common.ok
13
+ vouchington-localization format --source localization/catalog
14
+ ```
15
+
16
+ Git merge is 3-way by message id, then by field (configure once per clone). Adding `es` on one
17
+ branch and `fr` on the other auto-merges when both sides keep a compile-valid shape. An empty
18
+ `%O` (add/add of a new shard) is treated as `[]`. Both sides changing the same locale,
19
+ descriptor, or deleting vs editing the same id is a conflict.
20
+
21
+ ```gitattributes
22
+ localization/catalog/*.json merge=vouchington-localization text eol=lf
23
+ localization/catalog/tags.json merge=text
24
+ ```
25
+
26
+ ```gitconfig
27
+ [merge "vouchington-localization"]
28
+ name = Merge localization catalog shards by message id and locale
29
+ driver = vouchington-localization git-merge %O %A %B
30
+ ```
31
+
32
+ On conflict the driver writes conflict markers into `%A` (`<<<<<<< ours` / `=======` /
33
+ `>>>>>>> theirs`) and exits non-zero. Resolve the markers, then `format` or `compile` — a
34
+ conflicted shard is not canonical.
35
+
7
36
  CSV import/export is interchange only: never source of truth and never compiled directly to
8
37
  SQLite. Native resource helpers emit strings, RESX, and typed key/descriptor files from the
9
38
  same resolved catalog without product path assumptions.
package/dist/bin.mjs CHANGED
File without changes
@@ -0,0 +1,2 @@
1
+ export declare function runShardCli(command: string, args: readonly string[]): string | undefined;
2
+ export declare function shardUsage(): string;
@@ -0,0 +1,82 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { CatalogMergeConflict, mergeCatalogShards, parseCatalogShardText, removeCatalogLine, serializeCatalogShard, upsertCatalogLine, } from '@vouchington/localization';
4
+ import { parseCatalogFile } from './validate.mjs';
5
+ export function runShardCli(command, args) {
6
+ if (command === 'upsert') {
7
+ const path = requiredPath(args, '--file');
8
+ mkdirSync(dirname(path), { recursive: true });
9
+ writeFileSync(path, upsertCatalogLine(readShard(path), required(args, '--message')));
10
+ return undefined;
11
+ }
12
+ if (command === 'remove') {
13
+ const path = requiredPath(args, '--file');
14
+ writeFileSync(path, removeCatalogLine(readFileSync(path, 'utf8'), required(args, '--id')));
15
+ return undefined;
16
+ }
17
+ if (command === 'git-merge') {
18
+ const [ancestor, ours, theirs] = args;
19
+ if (ancestor === undefined || ours === undefined || theirs === undefined) {
20
+ throw new TypeError(shardUsage());
21
+ }
22
+ try {
23
+ writeFileSync(ours, mergeCatalogShards(readFileSync(ancestor, 'utf8'), readFileSync(ours, 'utf8'), readFileSync(theirs, 'utf8')));
24
+ }
25
+ catch (error) {
26
+ if (error instanceof CatalogMergeConflict)
27
+ writeFileSync(ours, error.text);
28
+ throw error;
29
+ }
30
+ return undefined;
31
+ }
32
+ if (command === 'format')
33
+ return formatCatalogDirectory(required(args, '--source'));
34
+ throw new TypeError(shardUsage());
35
+ }
36
+ export function shardUsage() {
37
+ return [
38
+ 'Usage: vouchington-localization upsert --file <file> --message <json>',
39
+ 'Usage: vouchington-localization remove --file <file> --id <id>',
40
+ 'Usage: vouchington-localization git-merge <ancestor> <ours> <theirs>',
41
+ 'Usage: vouchington-localization format --source <dir>',
42
+ ].join('\n');
43
+ }
44
+ function formatCatalogDirectory(directory) {
45
+ const names = readdirSync(directory).filter((name) => name.endsWith('.json') && name !== 'tags.json');
46
+ for (const name of names) {
47
+ const path = join(directory, name);
48
+ try {
49
+ writeFileSync(path, serializeCatalogShard(messagesFromUnknownText(readFileSync(path, 'utf8'))));
50
+ }
51
+ catch (error) {
52
+ throw new TypeError(`${path}: ${error.message}`);
53
+ }
54
+ }
55
+ return `${names.length} files`;
56
+ }
57
+ function required(args, flag) {
58
+ const index = args.indexOf(flag);
59
+ const value = index === -1 ? undefined : args[index + 1];
60
+ if (value === undefined || value.startsWith('--'))
61
+ throw new TypeError(shardUsage());
62
+ return value;
63
+ }
64
+ function requiredPath(args, flag) {
65
+ return resolve(required(args, flag));
66
+ }
67
+ function messagesFromUnknownText(text) {
68
+ try {
69
+ return parseCatalogShardText(text);
70
+ }
71
+ catch (shardError) {
72
+ try {
73
+ return parseCatalogFile(JSON.parse(text));
74
+ }
75
+ catch {
76
+ throw shardError;
77
+ }
78
+ }
79
+ }
80
+ function readShard(path) {
81
+ return existsSync(path) ? readFileSync(path, 'utf8') : '[]\n';
82
+ }
package/dist/cli.mjs CHANGED
@@ -7,8 +7,18 @@ import { exportLocalizationCsv, importLocalizationCsv } from './csv.mjs';
7
7
  import { loadCatalogDirectory } from './load.mjs';
8
8
  import { openLocalizationDatabase } from './open.mjs';
9
9
  import { explainLocalizationPlan, resolveLocalizationBatch } from './resolve.mjs';
10
+ import { runShardCli, shardUsage } from './cli-shard.mjs';
10
11
  export async function runLocalizationCli(argv, write = console.log) {
11
12
  const [command, ...rest] = argv;
13
+ if (command === 'upsert' ||
14
+ command === 'remove' ||
15
+ command === 'git-merge' ||
16
+ command === 'format') {
17
+ const result = runShardCli(command, rest);
18
+ if (typeof result === 'string')
19
+ write(result);
20
+ return;
21
+ }
12
22
  if (command === 'compile') {
13
23
  const loaded = await loadCatalogDirectory(required(rest, '--source'));
14
24
  write(compileLocalizationSqlite(loaded.messages, required(rest, '--output'), loaded.tags));
@@ -75,5 +85,6 @@ function usage() {
75
85
  'Usage: vouchington-localization inspect --db <file>',
76
86
  'Usage: vouchington-localization csv-export --source <dir> [--output <file>]',
77
87
  'Usage: vouchington-localization csv-import --input <file> --output <dir>',
88
+ shardUsage(),
78
89
  ].join('\n');
79
90
  }
package/dist/compile.mjs CHANGED
@@ -2,7 +2,7 @@ import { mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync } from 'node:
2
2
  import { tmpdir } from 'node:os';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { DatabaseSync } from 'node:sqlite';
5
- import { canonicalJson, compareCodePoints, LOCALIZATION_WIRE_CONTRACT, } from '@vouchington/localization';
5
+ import { canonicalJson, compareCodePoints, LOCALIZATION_WIRE_CONTRACT, serializeCatalogShard, } from '@vouchington/localization';
6
6
  import { assertSqliteIntegrity } from './integrity.mjs';
7
7
  import { catalogRevision } from './revision.mjs';
8
8
  import { SQLITE_SCHEMA } from './schema.mjs';
@@ -31,7 +31,7 @@ export function compileLocalizationSqlite(messages, outputPath, tags = {}) {
31
31
  return revision;
32
32
  }
33
33
  export function writeJsonCatalog(messages, path) {
34
- writeFileSync(path, `${JSON.stringify({ messages }, null, 2)}\n`);
34
+ writeFileSync(path, serializeCatalogShard(messages));
35
35
  }
36
36
  function insertMetadata(database, revision) {
37
37
  const insert = database.prepare('INSERT INTO metadata (key, value) VALUES (?, ?)');
@@ -42,7 +42,7 @@ function insertMessages(database, messages) {
42
42
  const insertMessage = database.prepare('INSERT INTO messages (id, descriptor_json) VALUES (?, ?)');
43
43
  const insertTranslation = database.prepare('INSERT INTO translations (locale, message_id, value_json) VALUES (?, ?, ?)');
44
44
  const insertConsumer = database.prepare('INSERT INTO consumer_membership (consumer, message_id) VALUES (?, ?)');
45
- for (const message of [...messages].toSorted((left, right) => (left.id < right.id ? -1 : 1))) {
45
+ for (const message of [...messages].toSorted((left, right) => compareCodePoints(left.id, right.id))) {
46
46
  insertMessage.run(message.id, canonicalJson(message.descriptor));
47
47
  for (const consumer of message.consumers)
48
48
  insertConsumer.run(consumer, message.id);
package/dist/load.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readdir, readFile } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
- import { compareCodePoints } from '@vouchington/localization';
4
- import { parseCatalogFile, validateCatalogMessages } from './validate.mjs';
3
+ import { compareCodePoints, parseCatalogShardText, } from '@vouchington/localization';
4
+ import { validateCatalogMessages } from './validate.mjs';
5
5
  export async function loadCatalogDirectory(directory) {
6
6
  const names = (await readdir(directory))
7
7
  .filter((name) => name.endsWith('.json'))
@@ -11,12 +11,12 @@ export async function loadCatalogDirectory(directory) {
11
11
  const messages = [];
12
12
  let tags = {};
13
13
  for (const name of names) {
14
- const parsed = JSON.parse(await readFile(join(directory, name), 'utf8'));
14
+ const text = await readFile(join(directory, name), 'utf8');
15
15
  if (name === 'tags.json') {
16
- tags = parseTags(parsed);
16
+ tags = parseTags(JSON.parse(text));
17
17
  continue;
18
18
  }
19
- messages.push(...parseCatalogFile(parsed));
19
+ messages.push(...parseCatalogShardText(text));
20
20
  }
21
21
  if (messages.length === 0)
22
22
  throw new TypeError(`No catalog messages in "${directory}"`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vouchington/localization-compiler",
3
- "version": "0.0.0",
3
+ "version": "0.0.2",
4
4
  "description": "Node compiler for localization JSON catalogs, immutable SQLite artifacts, and CLI resolution.",
5
5
  "homepage": "https://github.com/vouchington/vouchington-platform/tree/main/packages/localization-compiler#readme",
6
6
  "bugs": {
@@ -34,15 +34,14 @@
34
34
  "publishConfig": {
35
35
  "access": "public"
36
36
  },
37
- "scripts": {
38
- "build": "tsc --project tsconfig.build.json",
39
- "prepack": "pnpm run build"
40
- },
41
37
  "dependencies": {
42
38
  "@vouchington/csv": "^0.0.1",
43
- "@vouchington/localization": "^0.0.0"
39
+ "@vouchington/localization": "^0.0.1"
44
40
  },
45
41
  "engines": {
46
42
  "node": ">=24.0.0"
43
+ },
44
+ "scripts": {
45
+ "build": "tsc --project tsconfig.build.json"
47
46
  }
48
- }
47
+ }
@@ -1,3 +0,0 @@
1
- import type { CatalogMessage } from '@vouchington/localization';
2
- export declare function sampleMessages(): CatalogMessage[];
3
- export declare function writeCatalog(files: Record<string, unknown>): string;
@@ -1,49 +0,0 @@
1
- import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
2
- import { tmpdir } from 'node:os';
3
- import { join } from 'node:path';
4
- export function sampleMessages() {
5
- return [
6
- {
7
- id: 'nav.home',
8
- descriptor: null,
9
- consumers: ['web', 'swift'],
10
- translations: { 'en-US': 'Home', es: 'Inicio' },
11
- },
12
- {
13
- id: 'common.save',
14
- descriptor: null,
15
- consumers: ['web'],
16
- translations: { 'en-US': 'Save "{name}"' },
17
- },
18
- {
19
- id: 'settings.count',
20
- descriptor: { kind: 'plural', valueParameter: 'count' },
21
- consumers: ['web', 'dotnet'],
22
- translations: {
23
- 'en-US': { one: '{count} item', other: '{count} items' },
24
- es: { one: '{count} artículo', other: '{count} artículos' },
25
- },
26
- },
27
- {
28
- id: 'settings.ago',
29
- descriptor: {
30
- kind: 'select-plural',
31
- valueParameter: 'value',
32
- selectParameter: 'unit',
33
- cases: ['day'],
34
- },
35
- consumers: ['email'],
36
- translations: {
37
- 'en-US': { day: { one: '{value} day ago', other: '{value} days ago' } },
38
- },
39
- },
40
- ];
41
- }
42
- export function writeCatalog(files) {
43
- const directory = mkdtempSync(join(tmpdir(), 'catalog-'));
44
- mkdirSync(directory, { recursive: true });
45
- for (const [name, value] of Object.entries(files)) {
46
- writeFileSync(join(directory, name), `${JSON.stringify(value, null, 2)}\n`);
47
- }
48
- return directory;
49
- }