@vouchington/localization 0.0.2 → 0.1.1
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 +15 -10
- package/dist/bounds.d.mts +2 -2
- package/dist/bounds.mjs +2 -2
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +1 -0
- package/dist/tables.d.mts +7 -0
- package/dist/tables.mjs +70 -0
- package/dist/types.d.mts +30 -0
- package/package.json +1 -1
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,
|
|
6
|
-
|
|
5
|
+
fallback, and deterministic catalog-table serialization. It does not load catalogs, open SQLite,
|
|
6
|
+
or interpolate message text.
|
|
7
7
|
|
|
8
|
-
Catalog
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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/bounds.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export declare const DEFAULT_LOCALIZATION_BOUNDS: {
|
|
2
2
|
readonly maxLocales: 8;
|
|
3
|
-
readonly maxSelectors:
|
|
4
|
-
readonly maxMessages:
|
|
3
|
+
readonly maxSelectors: 32;
|
|
4
|
+
readonly maxMessages: 2000;
|
|
5
5
|
readonly maxBytes: number;
|
|
6
6
|
};
|
|
7
7
|
export declare class LocalizationBoundError extends RangeError {
|
package/dist/bounds.mjs
CHANGED
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;
|
package/dist/tables.mjs
ADDED
|
@@ -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.
|
|
3
|
+
"version": "0.1.1",
|
|
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": {
|