@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 +29 -0
- package/dist/bin.mjs +0 -0
- package/dist/cli-shard.d.mts +2 -0
- package/dist/cli-shard.mjs +82 -0
- package/dist/cli.mjs +11 -0
- package/dist/compile.mjs +3 -3
- package/dist/load.mjs +5 -5
- package/package.json +6 -7
- package/dist/test-helpers.d.mts +0 -3
- package/dist/test-helpers.mjs +0 -49
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,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,
|
|
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
|
|
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 {
|
|
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
|
|
14
|
+
const text = await readFile(join(directory, name), 'utf8');
|
|
15
15
|
if (name === 'tags.json') {
|
|
16
|
-
tags = parseTags(
|
|
16
|
+
tags = parseTags(JSON.parse(text));
|
|
17
17
|
continue;
|
|
18
18
|
}
|
|
19
|
-
messages.push(...
|
|
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.
|
|
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.
|
|
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
|
+
}
|
package/dist/test-helpers.d.mts
DELETED
package/dist/test-helpers.mjs
DELETED
|
@@ -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
|
-
}
|