@vouchington/localization 0.0.1 → 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.
package/README.md CHANGED
@@ -2,15 +2,20 @@
2
2
 
3
3
  Browser-safe localization contracts for Node 24+ and browsers. The package owns locale
4
4
  normalization (`en` aliases `en-US`), exact and terminal-prefix selector validation, ordered
5
- fallback, consumer membership, and deterministic catalog serialization. It does not load catalogs,
6
- open SQLite, or interpolate message text.
5
+ fallback, and deterministic catalog-table serialization. It does not load catalogs, open SQLite,
6
+ or interpolate message text.
7
7
 
8
- Catalog shards on disk are a JSON array with **one compact message object per line**. `id` is the
9
- first key so git and line editors can add, remove, or update a message without parsing the file.
10
- `parseCatalogShardText` rejects pretty-printed JSON, `{ messages }` wrappers, and unsorted ids.
8
+ Catalog source separates reusable copy from where each consumer renders it:
11
9
 
12
- `@vouchington/localization-compiler` compiles those shards into an immutable SQLite artifact,
13
- resolves the same selectors locally, and ships `upsert` / `remove` / `git-merge` for the line
14
- format. `git-merge` is 3-way: union independent ids, then merge consumers, descriptor, and
15
- each locale. Same-locale edits conflict and write `<<<<<<< ours` markers; adding `es` on one
16
- side and `fr` on the other does not.
10
+ - `copies.json`: `{ id, descriptor }` rows.
11
+ - `aliases.json`: `{ consumer, alias, copyId }` rows.
12
+ - `translations/<locale>.json`: `{ id, value }` rows.
13
+ - `routes.json` (generated): `{ consumer, selectorId, alias }` rows.
14
+
15
+ A route selector resolves its generated membership independently of alias spelling, then returns
16
+ the existing v1 alias-keyed wire response. Multiple aliases and consumers can therefore share one
17
+ copy and its full translation variants.
18
+
19
+ `@vouchington/localization-compiler` compiles those tables into an immutable SQLite artifact and
20
+ resolves the same selectors locally. Its CLI owns table updates, formatting, CSV interchange, and
21
+ three-way merges so catalog edits remain deterministic.
package/dist/index.d.mts CHANGED
@@ -1,4 +1,5 @@
1
- export type { CatalogMessage, ExactSelector, LocalizationBatch, LocalizationBounds, LocalizationConsumer, LocalizationLeaf, LocalizationRequest, LocalizationSelector, LocalizationWireContract, MessageDescriptor, NormalizedLocalizationRequest, PluralCategory, PluralDescriptor, PluralForms, PrefixSelector, PublicLocalizationConsumer, SelectPluralCases, SelectPluralDescriptor, TranslationValue, } from './types.mts';
1
+ export type { CatalogMessage, CatalogCopy, ConsumerAlias, ExactSelector, LocalizationBatch, LocalizationBounds, LocalizationConsumer, LocalizationLeaf, LocalizationRequest, LocalizationSelector, LocalizationWireContract, MessageDescriptor, NormalizedLocalizationRequest, PluralCategory, PluralDescriptor, PluralForms, PrefixSelector, PublicLocalizationConsumer, SelectPluralCases, SelectPluralDescriptor, TranslationValue, TranslationRow, RouteSelectorMembership, LocalizationCatalog, } from './types.mts';
2
+ export { catalogFromMessages, catalogCopyFromRecord, consumerAliasFromRecord, translationRowFromRecord, routeSelectorMembershipFromRecord, serializeCatalogTable, } from './tables.mts';
2
3
  export { CANONICAL_SOURCE_LOCALE, ENGLISH_LOCALE_ALIAS, LOCALIZATION_CONSUMERS, LOCALIZATION_WIRE_CONTRACT, PLURAL_CATEGORIES, PUBLIC_LOCALIZATION_CONSUMERS, } from './types.mts';
3
4
  export { DEFAULT_LOCALIZATION_BOUNDS, LocalizationBoundError } from './bounds.mts';
4
5
  export { assertLocalizationConsumer, assertPublicLocalizationConsumer, isLocalizationConsumer, isPublicLocalizationConsumer, uniqueConsumers, } from './consumers.mts';
package/dist/index.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ export { catalogFromMessages, catalogCopyFromRecord, consumerAliasFromRecord, translationRowFromRecord, routeSelectorMembershipFromRecord, serializeCatalogTable, } from './tables.mjs';
1
2
  export { CANONICAL_SOURCE_LOCALE, ENGLISH_LOCALE_ALIAS, LOCALIZATION_CONSUMERS, LOCALIZATION_WIRE_CONTRACT, PLURAL_CATEGORIES, PUBLIC_LOCALIZATION_CONSUMERS, } from './types.mjs';
2
3
  export { DEFAULT_LOCALIZATION_BOUNDS, LocalizationBoundError } from './bounds.mjs';
3
4
  export { assertLocalizationConsumer, assertPublicLocalizationConsumer, isLocalizationConsumer, isPublicLocalizationConsumer, uniqueConsumers, } from './consumers.mjs';
@@ -0,0 +1,7 @@
1
+ import type { CatalogCopy, CatalogMessage, ConsumerAlias, LocalizationCatalog, RouteSelectorMembership, TranslationRow } from './types.mts';
2
+ export declare function catalogCopyFromRecord(value: unknown): CatalogCopy;
3
+ export declare function consumerAliasFromRecord(value: unknown): ConsumerAlias;
4
+ export declare function translationRowFromRecord(value: unknown): TranslationRow;
5
+ export declare function routeSelectorMembershipFromRecord(value: unknown): RouteSelectorMembership;
6
+ export declare function catalogFromMessages(messages: readonly CatalogMessage[]): LocalizationCatalog;
7
+ export declare function serializeCatalogTable(rows: readonly unknown[]): string;
@@ -0,0 +1,70 @@
1
+ import { uniqueConsumers } from './consumers.mjs';
2
+ import { parseDescriptor } from './descriptors.mjs';
3
+ import { compareCodePoints } from './compare.mjs';
4
+ import { canonicalJson } from './serialize.mjs';
5
+ import { isMessageId } from './selectors.mjs';
6
+ export function catalogCopyFromRecord(value) {
7
+ if (!object(value) || typeof value.id !== 'string' || !isMessageId(value.id)) {
8
+ throw new TypeError('Copy is missing a valid id');
9
+ }
10
+ return { id: value.id, descriptor: parseDescriptor(value.descriptor ?? null) };
11
+ }
12
+ export function consumerAliasFromRecord(value) {
13
+ if (!object(value) || typeof value.alias !== 'string' || !isMessageId(value.alias)) {
14
+ throw new TypeError('Alias is missing a valid alias');
15
+ }
16
+ if (typeof value.copyId !== 'string' || !isMessageId(value.copyId)) {
17
+ throw new TypeError(`Alias "${value.alias}" is missing a valid copyId`);
18
+ }
19
+ return {
20
+ consumer: uniqueConsumers([String(value.consumer)])[0],
21
+ alias: value.alias,
22
+ copyId: value.copyId,
23
+ };
24
+ }
25
+ export function translationRowFromRecord(value) {
26
+ if (!object(value) ||
27
+ typeof value.id !== 'string' ||
28
+ !isMessageId(value.id) ||
29
+ !('value' in value)) {
30
+ throw new TypeError('Translation is missing a valid id or value');
31
+ }
32
+ return { id: value.id, value: value.value };
33
+ }
34
+ export function routeSelectorMembershipFromRecord(value) {
35
+ if (!object(value) || typeof value.selectorId !== 'string' || !isMessageId(value.selectorId)) {
36
+ throw new TypeError('Route membership is missing a valid selectorId');
37
+ }
38
+ if (typeof value.alias !== 'string' || !isMessageId(value.alias)) {
39
+ throw new TypeError('Route membership is missing a valid alias');
40
+ }
41
+ return {
42
+ consumer: uniqueConsumers([String(value.consumer)])[0],
43
+ selectorId: value.selectorId,
44
+ alias: value.alias,
45
+ };
46
+ }
47
+ export function catalogFromMessages(messages) {
48
+ const copies = messages.map(({ id, descriptor }) => ({ id, descriptor }));
49
+ const aliases = messages.flatMap(({ id, consumers }) => consumers.map((consumer) => ({ consumer, alias: id, copyId: id })));
50
+ const translations = {};
51
+ for (const message of messages)
52
+ for (const [locale, value] of Object.entries(message.translations)) {
53
+ ;
54
+ (translations[locale] ??= []).push({ id: message.id, value });
55
+ }
56
+ return { copies, aliases, translations };
57
+ }
58
+ export function serializeCatalogTable(rows) {
59
+ return `${canonicalJson([...rows].toSorted((a, b) => compareCodePoints(rowKey(a), rowKey(b))))}\n`;
60
+ }
61
+ function rowKey(value) {
62
+ if (!object(value))
63
+ return '';
64
+ return ['consumer', 'selectorId', 'alias', 'id']
65
+ .map((key) => (typeof value[key] === 'string' ? value[key] : ''))
66
+ .join('\t');
67
+ }
68
+ function object(value) {
69
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
70
+ }
package/dist/types.d.mts CHANGED
@@ -41,6 +41,36 @@ export type CatalogMessage = Readonly<{
41
41
  consumers: readonly LocalizationConsumer[];
42
42
  translations: Readonly<Record<string, TranslationValue>>;
43
43
  }>;
44
+ /** Canonical, reusable copy. Its id is deliberately unrelated to any rendered slot. */
45
+ export type CatalogCopy = Readonly<{
46
+ id: string;
47
+ descriptor: MessageDescriptor | null;
48
+ }>;
49
+ /** A consumer-owned rendered slot pointing at canonical copy. */
50
+ export type ConsumerAlias = Readonly<{
51
+ consumer: LocalizationConsumer;
52
+ alias: string;
53
+ copyId: string;
54
+ }>;
55
+ /** One locale value for one canonical copy. */
56
+ export type TranslationRow = Readonly<{
57
+ id: string;
58
+ value: TranslationValue;
59
+ }>;
60
+ /** Generated route closure membership. selectorId is not required to resemble an alias. */
61
+ export type RouteSelectorMembership = Readonly<{
62
+ consumer: LocalizationConsumer;
63
+ selectorId: string;
64
+ alias: string;
65
+ }>;
66
+ /** The three source tables plus optional generated route membership and editorial tags. */
67
+ export type LocalizationCatalog = Readonly<{
68
+ copies: readonly CatalogCopy[];
69
+ aliases: readonly ConsumerAlias[];
70
+ translations: Readonly<Record<string, readonly TranslationRow[]>>;
71
+ routeMembership?: readonly RouteSelectorMembership[];
72
+ tags?: Readonly<Record<string, readonly string[]>>;
73
+ }>;
44
74
  export type LocalizationRequest = Readonly<{
45
75
  consumer: LocalizationConsumer;
46
76
  locales: readonly string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vouchington/localization",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "description": "Browser-safe localization catalog contracts, locale fallback, and selector validation.",
5
5
  "homepage": "https://github.com/vouchington/vouchington-platform/tree/main/packages/localization#readme",
6
6
  "bugs": {