@_linked/translation 0.2.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.
Files changed (153) hide show
  1. package/README.md +102 -0
  2. package/lib/cjs/data/translation.json +8 -0
  3. package/lib/esm/archive.d.ts +133 -0
  4. package/lib/esm/archive.js +567 -0
  5. package/lib/esm/archive.js.map +1 -0
  6. package/lib/esm/authorization.d.ts +12 -0
  7. package/lib/esm/authorization.js +10 -0
  8. package/lib/esm/authorization.js.map +1 -0
  9. package/lib/esm/backend.d.ts +1 -0
  10. package/lib/esm/backend.js +3 -0
  11. package/lib/esm/backend.js.map +1 -0
  12. package/lib/esm/cdn-loader.d.ts +40 -0
  13. package/lib/esm/cdn-loader.js +200 -0
  14. package/lib/esm/cdn-loader.js.map +1 -0
  15. package/lib/esm/checks.d.ts +56 -0
  16. package/lib/esm/checks.js +288 -0
  17. package/lib/esm/checks.js.map +1 -0
  18. package/lib/esm/compile.d.ts +64 -0
  19. package/lib/esm/compile.js +99 -0
  20. package/lib/esm/compile.js.map +1 -0
  21. package/lib/esm/core/messages.d.ts +25 -0
  22. package/lib/esm/core/messages.js +76 -0
  23. package/lib/esm/core/messages.js.map +1 -0
  24. package/lib/esm/data/translation.json +8 -0
  25. package/lib/esm/discovery.d.ts +97 -0
  26. package/lib/esm/discovery.js +365 -0
  27. package/lib/esm/discovery.js.map +1 -0
  28. package/lib/esm/exchange.d.ts +100 -0
  29. package/lib/esm/exchange.js +223 -0
  30. package/lib/esm/exchange.js.map +1 -0
  31. package/lib/esm/export.d.ts +28 -0
  32. package/lib/esm/export.js +92 -0
  33. package/lib/esm/export.js.map +1 -0
  34. package/lib/esm/formats/csv.d.ts +8 -0
  35. package/lib/esm/formats/csv.js +397 -0
  36. package/lib/esm/formats/csv.js.map +1 -0
  37. package/lib/esm/formats/json.d.ts +30 -0
  38. package/lib/esm/formats/json.js +459 -0
  39. package/lib/esm/formats/json.js.map +1 -0
  40. package/lib/esm/formats/xliff.d.ts +12 -0
  41. package/lib/esm/formats/xliff.js +577 -0
  42. package/lib/esm/formats/xliff.js.map +1 -0
  43. package/lib/esm/import.d.ts +197 -0
  44. package/lib/esm/import.js +391 -0
  45. package/lib/esm/import.js.map +1 -0
  46. package/lib/esm/index.d.ts +42 -0
  47. package/lib/esm/index.js +34 -0
  48. package/lib/esm/index.js.map +1 -0
  49. package/lib/esm/key-sync-node.d.ts +29 -0
  50. package/lib/esm/key-sync-node.js +174 -0
  51. package/lib/esm/key-sync-node.js.map +1 -0
  52. package/lib/esm/key-sync.d.ts +46 -0
  53. package/lib/esm/key-sync.js +114 -0
  54. package/lib/esm/key-sync.js.map +1 -0
  55. package/lib/esm/key-version.d.ts +50 -0
  56. package/lib/esm/key-version.js +134 -0
  57. package/lib/esm/key-version.js.map +1 -0
  58. package/lib/esm/languages.d.ts +18 -0
  59. package/lib/esm/languages.js +74 -0
  60. package/lib/esm/languages.js.map +1 -0
  61. package/lib/esm/linked-discovery.d.ts +44 -0
  62. package/lib/esm/linked-discovery.js +267 -0
  63. package/lib/esm/linked-discovery.js.map +1 -0
  64. package/lib/esm/message-format.d.ts +41 -0
  65. package/lib/esm/message-format.js +133 -0
  66. package/lib/esm/message-format.js.map +1 -0
  67. package/lib/esm/mt.d.ts +111 -0
  68. package/lib/esm/mt.js +142 -0
  69. package/lib/esm/mt.js.map +1 -0
  70. package/lib/esm/ontologies/translation.d.ts +203 -0
  71. package/lib/esm/ontologies/translation.js +219 -0
  72. package/lib/esm/ontologies/translation.js.map +1 -0
  73. package/lib/esm/package.d.ts +4 -0
  74. package/lib/esm/package.js +11 -0
  75. package/lib/esm/package.js.map +1 -0
  76. package/lib/esm/react.d.ts +55 -0
  77. package/lib/esm/react.js +127 -0
  78. package/lib/esm/react.js.map +1 -0
  79. package/lib/esm/records.d.ts +151 -0
  80. package/lib/esm/records.js +2 -0
  81. package/lib/esm/records.js.map +1 -0
  82. package/lib/esm/release.d.ts +198 -0
  83. package/lib/esm/release.js +435 -0
  84. package/lib/esm/release.js.map +1 -0
  85. package/lib/esm/shape-catalog.d.ts +79 -0
  86. package/lib/esm/shape-catalog.js +118 -0
  87. package/lib/esm/shape-catalog.js.map +1 -0
  88. package/lib/esm/shapes/GlossaryTerm.d.ts +28 -0
  89. package/lib/esm/shapes/GlossaryTerm.js +94 -0
  90. package/lib/esm/shapes/GlossaryTerm.js.map +1 -0
  91. package/lib/esm/shapes/TranslationInventoryOrigin.d.ts +33 -0
  92. package/lib/esm/shapes/TranslationInventoryOrigin.js +209 -0
  93. package/lib/esm/shapes/TranslationInventoryOrigin.js.map +1 -0
  94. package/lib/esm/shapes/TranslationKey.d.ts +92 -0
  95. package/lib/esm/shapes/TranslationKey.js +194 -0
  96. package/lib/esm/shapes/TranslationKey.js.map +1 -0
  97. package/lib/esm/shapes/TranslationKeyVersion.d.ts +19 -0
  98. package/lib/esm/shapes/TranslationKeyVersion.js +125 -0
  99. package/lib/esm/shapes/TranslationKeyVersion.js.map +1 -0
  100. package/lib/esm/shapes/TranslationLanguage.d.ts +15 -0
  101. package/lib/esm/shapes/TranslationLanguage.js +102 -0
  102. package/lib/esm/shapes/TranslationLanguage.js.map +1 -0
  103. package/lib/esm/shapes/TranslationProvider.d.ts +409 -0
  104. package/lib/esm/shapes/TranslationProvider.js +1376 -0
  105. package/lib/esm/shapes/TranslationProvider.js.map +1 -0
  106. package/lib/esm/shapes/TranslationRelease.d.ts +36 -0
  107. package/lib/esm/shapes/TranslationRelease.js +149 -0
  108. package/lib/esm/shapes/TranslationRelease.js.map +1 -0
  109. package/lib/esm/shapes/TranslationRevision.d.ts +39 -0
  110. package/lib/esm/shapes/TranslationRevision.js +159 -0
  111. package/lib/esm/shapes/TranslationRevision.js.map +1 -0
  112. package/lib/esm/shapes/TranslationUnit.d.ts +26 -0
  113. package/lib/esm/shapes/TranslationUnit.js +111 -0
  114. package/lib/esm/shapes/TranslationUnit.js.map +1 -0
  115. package/package.json +134 -0
  116. package/src/archive.ts +894 -0
  117. package/src/authorization.ts +33 -0
  118. package/src/backend.ts +2 -0
  119. package/src/cdn-loader.ts +264 -0
  120. package/src/checks.ts +413 -0
  121. package/src/compile.ts +168 -0
  122. package/src/core/messages.ts +106 -0
  123. package/src/data/translation.json +8 -0
  124. package/src/discovery.ts +594 -0
  125. package/src/exchange.ts +428 -0
  126. package/src/export.ts +135 -0
  127. package/src/formats/csv.ts +459 -0
  128. package/src/formats/json.ts +587 -0
  129. package/src/formats/xliff.ts +708 -0
  130. package/src/import.ts +648 -0
  131. package/src/index.ts +207 -0
  132. package/src/key-sync-node.ts +269 -0
  133. package/src/key-sync.ts +168 -0
  134. package/src/key-version.ts +206 -0
  135. package/src/languages.ts +102 -0
  136. package/src/linked-discovery.ts +385 -0
  137. package/src/message-format.ts +214 -0
  138. package/src/mt.ts +248 -0
  139. package/src/ontologies/translation.ts +234 -0
  140. package/src/package.ts +19 -0
  141. package/src/react.tsx +250 -0
  142. package/src/records.ts +180 -0
  143. package/src/release.ts +726 -0
  144. package/src/shape-catalog.ts +198 -0
  145. package/src/shapes/GlossaryTerm.ts +61 -0
  146. package/src/shapes/TranslationInventoryOrigin.ts +131 -0
  147. package/src/shapes/TranslationKey.ts +174 -0
  148. package/src/shapes/TranslationKeyVersion.ts +77 -0
  149. package/src/shapes/TranslationLanguage.ts +51 -0
  150. package/src/shapes/TranslationProvider.ts +1755 -0
  151. package/src/shapes/TranslationRelease.ts +114 -0
  152. package/src/shapes/TranslationRevision.ts +105 -0
  153. package/src/shapes/TranslationUnit.ts +72 -0
@@ -0,0 +1,214 @@
1
+ import {
2
+ isArgumentElement,
3
+ isDateElement,
4
+ isNumberElement,
5
+ isPluralElement,
6
+ isSelectElement,
7
+ isTagElement,
8
+ isTimeElement,
9
+ parse,
10
+ type MessageFormatElement,
11
+ } from '@formatjs/icu-messageformat-parser';
12
+ import type { MessageFormat } from './core/messages.js';
13
+
14
+ export type MessageArgumentKind =
15
+ | 'argument'
16
+ | 'number'
17
+ | 'date'
18
+ | 'time'
19
+ | 'select'
20
+ | 'plural'
21
+ | 'selectordinal';
22
+
23
+ export interface MessageArgument {
24
+ name: string;
25
+ kinds: MessageArgumentKind[];
26
+ }
27
+
28
+ export interface MessageAnalysis {
29
+ valid: boolean;
30
+ arguments: MessageArgument[];
31
+ error?: string;
32
+ }
33
+
34
+ export interface MessageArgumentComparison {
35
+ valid: boolean;
36
+ missing: string[];
37
+ extra: string[];
38
+ typeMismatches: Array<{
39
+ name: string;
40
+ source: MessageArgumentKind[];
41
+ target: MessageArgumentKind[];
42
+ }>;
43
+ sourceError?: string;
44
+ targetError?: string;
45
+ }
46
+
47
+ export interface LegacyPluralSuffix {
48
+ countArgument: string;
49
+ suffixArgument: string;
50
+ }
51
+
52
+ /**
53
+ * FormatSimple remains intentionally looser than ICU identifiers: existing
54
+ * apps may use positional (`{0}`), dotted (`{user.name}`), hyphenated
55
+ * (`{item-count}`), or Unicode (`{名前}`) variable names.
56
+ */
57
+ const SIMPLE_ARGUMENT_RE =
58
+ /\{\s*([\p{L}\p{N}_][\p{L}\p{N}\p{M}_.-]*)\s*\}/gu;
59
+
60
+ function argumentKind(element: MessageFormatElement): MessageArgumentKind | null {
61
+ if (isArgumentElement(element)) return 'argument';
62
+ if (isNumberElement(element)) return 'number';
63
+ if (isDateElement(element)) return 'date';
64
+ if (isTimeElement(element)) return 'time';
65
+ if (isSelectElement(element)) return 'select';
66
+ if (isPluralElement(element)) {
67
+ return element.pluralType === 'ordinal' ? 'selectordinal' : 'plural';
68
+ }
69
+ return null;
70
+ }
71
+
72
+ function collectIcuArguments(
73
+ elements: MessageFormatElement[],
74
+ found: Map<string, Set<MessageArgumentKind>>,
75
+ ): void {
76
+ for (const element of elements) {
77
+ const kind = argumentKind(element);
78
+ if (kind && 'value' in element) {
79
+ const kinds = found.get(element.value) ?? new Set<MessageArgumentKind>();
80
+ kinds.add(kind);
81
+ found.set(element.value, kinds);
82
+ }
83
+ if (isPluralElement(element) || isSelectElement(element)) {
84
+ for (const option of Object.values(element.options)) {
85
+ collectIcuArguments(option.value, found);
86
+ }
87
+ } else if (isTagElement(element)) {
88
+ collectIcuArguments(element.children, found);
89
+ }
90
+ }
91
+ }
92
+
93
+ const toArguments = (
94
+ found: Map<string, Set<MessageArgumentKind>>,
95
+ ): MessageArgument[] =>
96
+ [...found.entries()]
97
+ .map(([name, kinds]) => ({ name, kinds: [...kinds].sort() }))
98
+ .sort((a, b) => a.name.localeCompare(b.name));
99
+
100
+ /** Parse the variables a translator must preserve in a simple or ICU message. */
101
+ export function analyzeMessage(
102
+ text: string,
103
+ format: MessageFormat = 'simple',
104
+ ): MessageAnalysis {
105
+ const found = new Map<string, Set<MessageArgumentKind>>();
106
+ if (format === 'simple') {
107
+ for (const match of text.matchAll(SIMPLE_ARGUMENT_RE)) {
108
+ found.set(match[1], new Set(['argument']));
109
+ }
110
+ return { valid: true, arguments: toArguments(found) };
111
+ }
112
+
113
+ try {
114
+ collectIcuArguments(
115
+ parse(text, { ignoreTag: true, requiresOtherClause: true }),
116
+ found,
117
+ );
118
+ return { valid: true, arguments: toArguments(found) };
119
+ } catch (cause) {
120
+ return {
121
+ valid: false,
122
+ arguments: [],
123
+ error: cause instanceof Error ? cause.message : String(cause),
124
+ };
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Compare runtime arguments without requiring identical word order or plural
130
+ * categories. Categories legitimately differ by language; variable names and
131
+ * their ICU roles may not.
132
+ */
133
+ export function compareMessageArguments(
134
+ source: string,
135
+ target: string,
136
+ format: MessageFormat = 'simple',
137
+ ): MessageArgumentComparison {
138
+ const sourceAnalysis = analyzeMessage(source, format);
139
+ const targetAnalysis = analyzeMessage(target, format);
140
+ const sourceByName = new Map(
141
+ sourceAnalysis.arguments.map((argument) => [argument.name, argument]),
142
+ );
143
+ const targetByName = new Map(
144
+ targetAnalysis.arguments.map((argument) => [argument.name, argument]),
145
+ );
146
+ const optionalLegacySuffixes =
147
+ format === 'simple'
148
+ ? new Set(
149
+ detectLegacyPluralSuffixes(source).map(
150
+ ({ suffixArgument }) => suffixArgument,
151
+ ),
152
+ )
153
+ : new Set<string>();
154
+ const missing = [...sourceByName.keys()]
155
+ .filter((name) => !targetByName.has(name) && !optionalLegacySuffixes.has(name))
156
+ .sort();
157
+ const extra = [...targetByName.keys()]
158
+ .filter((name) => !sourceByName.has(name))
159
+ .sort();
160
+ const typeMismatches = [...sourceByName.keys()]
161
+ .filter((name) => targetByName.has(name))
162
+ .flatMap((name) => {
163
+ const sourceKinds = sourceByName.get(name)!.kinds;
164
+ const targetKinds = targetByName.get(name)!.kinds;
165
+ return sourceKinds.length === targetKinds.length &&
166
+ sourceKinds.every((kind, index) => kind === targetKinds[index])
167
+ ? []
168
+ : [{ name, source: sourceKinds, target: targetKinds }];
169
+ });
170
+
171
+ return {
172
+ valid:
173
+ sourceAnalysis.valid &&
174
+ targetAnalysis.valid &&
175
+ missing.length === 0 &&
176
+ extra.length === 0 &&
177
+ typeMismatches.length === 0,
178
+ missing,
179
+ extra,
180
+ typeMismatches,
181
+ sourceError: sourceAnalysis.error,
182
+ targetError: targetAnalysis.error,
183
+ };
184
+ }
185
+
186
+ /**
187
+ * Detect the legacy English suffix idiom (`{n} mission{s}`). `{s}` is not a
188
+ * plural operator; callers merely pass either "s" or "". Other languages may
189
+ * omit it, and the key should eventually be rewritten as an ICU plural.
190
+ */
191
+ export function detectLegacyPluralSuffixes(
192
+ text: string,
193
+ ): LegacyPluralSuffix[] {
194
+ const countArguments = [
195
+ ...text.matchAll(
196
+ /\{\s*([\p{L}\p{N}_][\p{L}\p{N}\p{M}_.-]*)\s*\}/gu,
197
+ ),
198
+ ].map((match) => match[1]);
199
+ if (countArguments.length < 2) return [];
200
+ const suffixes = [
201
+ ...text.matchAll(
202
+ /[\p{L}\p{N}]\{\s*([\p{L}\p{N}_][\p{L}\p{N}\p{M}_.-]*)\s*\}/gu,
203
+ ),
204
+ ].map((match) => match[1]);
205
+ if (suffixes.length === 0) return [];
206
+ const countArgument = countArguments.find(
207
+ (name) => !suffixes.includes(name),
208
+ );
209
+ if (!countArgument) return [];
210
+ return [...new Set(suffixes)].map((suffixArgument) => ({
211
+ countArgument,
212
+ suffixArgument,
213
+ }));
214
+ }
package/src/mt.ts ADDED
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Machine-translation provider registry (Plan 014 AD-L / Plan 017 Slice B).
3
+ * Providers implement one method — translate a batch of UI strings — and
4
+ * register by key ('claude', 'deepl', …). Resolution is PER TARGET LANGUAGE
5
+ * with a fallback chain: not every provider covers every language (DeepL has
6
+ * no Swahili or Urdu), so the preferred provider is used where it supports
7
+ * the target and the chain falls through otherwise. Pure module: no I/O, no
8
+ * SDK imports — concrete providers live with the host (Create Now) and are
9
+ * registered at boot.
10
+ */
11
+ import {
12
+ analyzeMessage,
13
+ compareMessageArguments,
14
+ } from './message-format.js';
15
+ import type { MessageFormat } from './core/messages.js';
16
+
17
+ export interface MtItem {
18
+ key: string;
19
+ sourceText: string;
20
+ /** Authoring context shown to the translator/model. */
21
+ description?: string;
22
+ format?: 'simple' | 'icu';
23
+ }
24
+
25
+ export interface MtGlossaryEntry {
26
+ term: string;
27
+ /** prefer (default) = translate as `translation`; keep = do-not-translate; forbid = never use. */
28
+ termType?: 'prefer' | 'require' | 'keep' | 'forbid';
29
+ /** Preferred translation (prefer entries); empty/undefined = keep the term as-is. */
30
+ translation?: string;
31
+ /** Replacement wording (forbid entries). */
32
+ useInstead?: string;
33
+ /** Target language this applies to; empty = all languages. */
34
+ language?: string;
35
+ /** Exact-casing matching (proper nouns / brand styling). */
36
+ caseSensitive?: boolean;
37
+ description?: string;
38
+ }
39
+
40
+ /** Register/tone controls (Plan 017 §7.4b) — NOT sampling temperature. */
41
+ export interface MtStyle {
42
+ formality?: 'formal' | 'informal';
43
+ /** Free-text tone guidance, e.g. "friendly, energetic, no bureaucratese". */
44
+ note?: string;
45
+ }
46
+
47
+ /** An established translation the provider should stay consistent with. */
48
+ export interface MtReference {
49
+ sourceText: string;
50
+ text: string;
51
+ }
52
+
53
+ export interface MtBatchRequest {
54
+ sourceLanguage: string;
55
+ targetLanguage: string;
56
+ items: MtItem[];
57
+ glossary: MtGlossaryEntry[];
58
+ style?: MtStyle;
59
+ /** Translation-memory context: reviewed units for consistency (Plan 017 §7.4c). */
60
+ references?: MtReference[];
61
+ }
62
+
63
+ export interface MtBatchResult {
64
+ key: string;
65
+ text: string;
66
+ }
67
+
68
+ export interface TranslationMtProvider {
69
+ readonly key: string;
70
+ /** Absent ⇒ the provider claims every language. */
71
+ supportsLanguage?(language: string): boolean;
72
+ translateBatch(request: MtBatchRequest): Promise<MtBatchResult[]>;
73
+ }
74
+
75
+ const providers = new Map<string, TranslationMtProvider>();
76
+
77
+ export function registerMtProvider(provider: TranslationMtProvider): void {
78
+ providers.set(provider.key, provider);
79
+ }
80
+
81
+ export function getMtProvider(key: string): TranslationMtProvider | undefined {
82
+ return providers.get(key);
83
+ }
84
+
85
+ export function listMtProviders(): TranslationMtProvider[] {
86
+ return [...providers.values()];
87
+ }
88
+
89
+ /** Test seam. */
90
+ export function clearMtProviders(): void {
91
+ providers.clear();
92
+ }
93
+
94
+ /**
95
+ * Ordered provider chain for one target language: the preferred provider
96
+ * first when it exists AND supports the language, then every other provider
97
+ * that supports it, registration order.
98
+ */
99
+ export function resolveMtProviders(
100
+ targetLanguage: string,
101
+ preferred?: string,
102
+ ): TranslationMtProvider[] {
103
+ const supports = (p: TranslationMtProvider) =>
104
+ !p.supportsLanguage || p.supportsLanguage(targetLanguage);
105
+ const chain = listMtProviders().filter(supports);
106
+ if (!preferred) return chain;
107
+ const first = chain.find((p) => p.key === preferred);
108
+ if (!first) return chain;
109
+ return [first, ...chain.filter((p) => p !== first)];
110
+ }
111
+
112
+ /** Unique `{placeholder}` tokens in a message, sorted for set comparison. */
113
+ export function extractPlaceholders(
114
+ text: string,
115
+ format: MessageFormat = 'simple',
116
+ ): string[] {
117
+ return analyzeMessage(text, format).arguments.map(({ name }) => name);
118
+ }
119
+
120
+ /**
121
+ * A translation is only usable if it preserves the source's placeholder SET
122
+ * exactly — a dropped or renamed `{param}` breaks interpolation at runtime.
123
+ */
124
+ export function placeholdersIntact(
125
+ source: string,
126
+ translated: string,
127
+ format: MessageFormat = 'simple',
128
+ ): boolean {
129
+ return compareMessageArguments(source, translated, format).valid;
130
+ }
131
+
132
+ /** Glossary entries applying to one target language (global + language-specific). */
133
+ export function glossaryForLanguage(
134
+ glossary: MtGlossaryEntry[],
135
+ language: string,
136
+ ): MtGlossaryEntry[] {
137
+ return glossary.filter((g) => !g.language || g.language === language);
138
+ }
139
+
140
+ export function chunkItems<T>(items: T[], size: number): T[][] {
141
+ if (size < 1) throw new Error('chunk size must be >= 1');
142
+ const chunks: T[][] = [];
143
+ for (let i = 0; i < items.length; i += size) chunks.push(items.slice(i, i + size));
144
+ return chunks;
145
+ }
146
+
147
+ /**
148
+ * Resolve the formality for one language from the encoded config entries
149
+ * ("formal" = app default; "de:informal" = per-language override; override wins).
150
+ */
151
+ export function formalityForLanguage(
152
+ entries: string[],
153
+ language: string,
154
+ ): 'formal' | 'informal' | undefined {
155
+ let result: 'formal' | 'informal' | undefined;
156
+ for (const entry of entries ?? []) {
157
+ const i = entry.indexOf(':');
158
+ const value = i === -1 ? entry : entry.slice(i + 1);
159
+ if (value !== 'formal' && value !== 'informal') continue;
160
+ if (i === -1) {
161
+ if (!result) result = value;
162
+ } else if (entry.slice(0, i) === language) {
163
+ return value;
164
+ }
165
+ }
166
+ return result;
167
+ }
168
+
169
+ // ── Consistency (Plan 017 §7.4c) ────────────────────────────────────────
170
+
171
+ /** Minimal entry view the consistency helpers need (client-safe). */
172
+ export interface ConsistencyEntry {
173
+ key: string;
174
+ namespace?: string;
175
+ sourceText: string;
176
+ format?: 'simple' | 'icu';
177
+ units: Record<string, { text: string; state: string } | undefined>;
178
+ }
179
+
180
+ /** Identical English source translated DIFFERENTLY within one language. */
181
+ export interface ConsistencyConflict {
182
+ sourceText: string;
183
+ language: string;
184
+ /** One entry per distinct translation: the text + the keys carrying it. */
185
+ variants: Array<{ text: string; keys: string[] }>;
186
+ }
187
+
188
+ /**
189
+ * Find keys whose IDENTICAL source text has diverging translations in a
190
+ * language — the core "same words, same translation" consistency check.
191
+ * Empty/untranslated units are ignored; whitespace-trimmed comparison.
192
+ */
193
+ export function findInconsistentTranslations(
194
+ entries: ConsistencyEntry[],
195
+ language: string,
196
+ ): ConsistencyConflict[] {
197
+ const bySource = new Map<string, Map<string, string[]>>();
198
+ for (const entry of entries) {
199
+ const source = entry.sourceText.trim();
200
+ const text = entry.units[language]?.text?.trim();
201
+ if (!source || !text) continue;
202
+ const variants = bySource.get(source) ?? new Map<string, string[]>();
203
+ variants.set(text, [...(variants.get(text) ?? []), entry.key]);
204
+ bySource.set(source, variants);
205
+ }
206
+ const conflicts: ConsistencyConflict[] = [];
207
+ for (const [sourceText, variants] of bySource) {
208
+ if (variants.size < 2) continue;
209
+ conflicts.push({
210
+ sourceText,
211
+ language,
212
+ variants: [...variants].map(([text, keys]) => ({ text, keys: keys.sort() })),
213
+ });
214
+ }
215
+ return conflicts.sort((a, b) => a.sourceText.localeCompare(b.sourceText));
216
+ }
217
+
218
+ /**
219
+ * Pick reviewed units as translation-memory REFERENCES for an MT batch:
220
+ * same-namespace reviewed translations first (shared vocabulary), then the
221
+ * shortest reviewed units overall (vocabulary-dense). Deterministic.
222
+ */
223
+ export function pickReferenceUnits(
224
+ entries: ConsistencyEntry[],
225
+ language: string,
226
+ batchNamespaces: string[],
227
+ max = 20,
228
+ ): MtReference[] {
229
+ const namespaces = new Set(batchNamespaces);
230
+ const reviewed = entries
231
+ .map((entry) => ({
232
+ namespace: entry.namespace ?? '',
233
+ sourceText: entry.sourceText.trim(),
234
+ unit: entry.units[language],
235
+ }))
236
+ .filter(
237
+ (row) =>
238
+ row.sourceText &&
239
+ row.unit?.state === 'reviewed' &&
240
+ row.unit.text.trim(),
241
+ );
242
+ const score = (row: (typeof reviewed)[number]) =>
243
+ (namespaces.has(row.namespace) ? 0 : 100000) + row.sourceText.length;
244
+ return reviewed
245
+ .sort((a, b) => score(a) - score(b) || a.sourceText.localeCompare(b.sourceText))
246
+ .slice(0, max)
247
+ .map((row) => ({ sourceText: row.sourceText, text: row.unit!.text.trim() }));
248
+ }
@@ -0,0 +1,234 @@
1
+ import { createNameSpace } from '@_linked/core/utils/NameSpace';
2
+ import { linkedOntology } from '../package.js';
3
+ import * as _this from './translation.js';
4
+
5
+ /** Lazy-load the (currently empty) serialized ontology graph. */
6
+ export var loadData = () => {
7
+ //@ts-ignore
8
+ return import('../data/translation.json', { with: { type: 'json' } }).then(
9
+ (data) => data.default
10
+ );
11
+ };
12
+
13
+ /** Vocabulary namespace for the translation package. */
14
+ export var ns = createNameSpace('https://id.linked.cm/translation/vocab#');
15
+ export var _self = ns('');
16
+
17
+ // ── Classes ─────────────────────────────────────────────────────────────
18
+ export var TranslationKey = ns('TranslationKey');
19
+ export var TranslationKeyVersion = ns('TranslationKeyVersion');
20
+ export var TranslationRelease = ns('TranslationRelease');
21
+ export var TranslationUnit = ns('TranslationUnit');
22
+ export var TranslationRevision = ns('TranslationRevision');
23
+ export var GlossaryTerm = ns('GlossaryTerm');
24
+ export var TranslationInventoryOrigin = ns('TranslationInventoryOrigin');
25
+
26
+ // Open language resources; BCP-47 is a string identifier, not an enum.
27
+ export var TranslationLanguage = ns('TranslationLanguage');
28
+ export var languageIdentifier = ns('languageIdentifier');
29
+ export var languageCode = ns('languageCode');
30
+ export var nativeName = ns('nativeName');
31
+ export var englishName = ns('englishName');
32
+ export var textDirection = ns('textDirection');
33
+ export var parentLanguage = ns('parentLanguage');
34
+ export var fallbackLanguage = ns('fallbackLanguage');
35
+ export var languageEnabled = ns('languageEnabled');
36
+ export var languageSupported = ns('languageSupported');
37
+
38
+ // ── TranslationKey properties ───────────────────────────────────────────
39
+ export var namespace = ns('namespace');
40
+ export var key = ns('key');
41
+ export var sourceText = ns('sourceText'); // canonical English (Plan 014 AD-J/R7)
42
+ export var description = ns('description');
43
+ export var kind = ns('kind'); // 'ui' | 'content' (AD-I)
44
+ export var ofApplication = ns('ofApplication'); // owning app IRI (AD-H/R2)
45
+ export var ofNode = ns('ofNode'); // content-translation subject (kind:'content')
46
+ export var ofField = ns('ofField');
47
+ export var ofShape = ns('ofShape'); // semantic SHACL shape context for package keys
48
+ export var ofProperty = ns('ofProperty'); // semantic predicate context for package keys
49
+ export var fromPackage = ns('fromPackage'); // package that supplied a seed catalog
50
+ export var overridden = ns('overridden'); // app diverged this shape key from canonical → excluded from shape propagation (AD-N)
51
+ export var shapeSource = ns('shapeSource'); // canonical source snapshot at override time → the value "clear override" reverts to (AD-N)
52
+ export var format = ns('format'); // 'simple' | 'icu' (AD-J/R8)
53
+ export var currentVersion = ns('currentVersion'); // → TranslationKeyVersion
54
+
55
+ // ── TranslationKeyVersion properties (Plan 018) ────────────────────────
56
+ export var versionId = ns('versionId');
57
+ export var sourceLanguage = ns('sourceLanguage');
58
+ export var argumentSignature = ns('argumentSignature');
59
+ export var contractHash = ns('contractHash');
60
+ export var sourceHash = ns('sourceHash');
61
+ export var supersedes = ns('supersedes');
62
+ export var createdBy = ns('createdBy');
63
+
64
+ // ── TranslationRelease properties (Plan 018) ───────────────────────────
65
+ export var branch = ns('branch');
66
+ export var releaseId = ns('releaseId');
67
+ export var buildId = ns('buildId');
68
+ export var channel = ns('channel');
69
+ export var contractSetHash = ns('contractSetHash');
70
+ export var manifestHash = ns('manifestHash');
71
+ export var hotfixSequence = ns('hotfixSequence');
72
+ export var publishedAt = ns('publishedAt');
73
+ export var supportedUntil = ns('supportedUntil');
74
+ export var qualitySummary = ns('qualitySummary');
75
+
76
+ // ── CapabilityAppConfig properties (stored in Create Now's cn-main) ────
77
+ export var languages = ns('languages'); // active BCP-47 tags, including source
78
+ export var defaultLanguage = ns('defaultLanguage'); // authored/source language
79
+ export var cdnTarget = ns('cdnTarget'); // delivery base URL / bucket prefix — the "repoint" knob (AD-K, P2)
80
+ export var autoPublish = ns('autoPublish'); // boolean — overridable auto-publish-on-edit toggle (P2, default off)
81
+ export var reviewedOnlyLanguage = ns('reviewedOnlyLanguage'); // BCP-47 tags published reviewed-only; absence ⇒ machine-ok (AD-K/R9, P2)
82
+ export var mtDefault = ns('mtDefault'); // default MT provider key (AD-L, unused until P4)
83
+ export var mtFormality = ns('mtFormality'); // multi-value: "formal" (app default) or "de:informal" (per-language) — Plan 017 §7.4b
84
+ export var mtStyleNote = ns('mtStyleNote'); // free-text tone guidance fed to LLM MT providers (Plan 017 §7.4b)
85
+
86
+ // ── TranslationUnit properties ──────────────────────────────────────────
87
+ export var ofKey = ns('ofKey'); // → TranslationKey
88
+ export var ofKeyVersion = ns('ofKeyVersion'); // → TranslationKeyVersion
89
+ export var language = ns('language'); // BCP-47 tag
90
+ export var text = ns('text');
91
+ export var state = ns('state'); // untranslated | machine | reviewed | stale
92
+ export var updatedAt = ns('updatedAt');
93
+ export var updatedBy = ns('updatedBy');
94
+ export var origin = ns('origin'); // current-value provenance, e.g. `tm`
95
+
96
+ // ── TranslationRevision properties (Plan 017 AD-P) ─────────────────────
97
+ // One append-only node per authored value: history (`applied`), translator
98
+ // proposals (`proposed`), and MT suggestions (`suggested`) share the shape.
99
+ export var status = ns('status'); // applied | proposed | suggested | accepted | rejected | superseded
100
+ export var author = ns('author'); // WebID or MT provider IRI
101
+ export var authorKind = ns('authorKind'); // 'human' | 'machine'
102
+ export var mtProvider = ns('mtProvider'); // provider key when authorKind=machine
103
+ export var basedOnText = ns('basedOnText'); // unit text at authoring time (conflict detection)
104
+ export var note = ns('note'); // translator/reviewer comment
105
+ export var createdAt = ns('createdAt');
106
+ export var decidedAt = ns('decidedAt'); // when a proposal/suggestion was accepted/rejected
107
+ export var decidedBy = ns('decidedBy'); // WebID of the reviewer
108
+
109
+ // ── GlossaryTerm properties ─────────────────────────────────────────────
110
+ export var term = ns('term');
111
+ export var glossaryTranslation = ns('glossaryTranslation');
112
+ export var glossaryLanguage = ns('glossaryLanguage');
113
+ export var termType = ns('termType'); // prefer | require | keep | forbid (Plan 017 B2-1 termbase)
114
+ export var useInstead = ns('useInstead'); // replacement for a forbidden term/spelling
115
+ export var caseSensitive = ns('caseSensitive'); // boolean — exact-casing matching (proper nouns)
116
+
117
+ // ── Branch translation inventory metadata (Plan 017 Phase 8b) ─────────
118
+ // These records are materialized into the active branch metadata dataset,
119
+ // never app data. Confirmed TranslationKey/TranslationUnit records retain
120
+ // their existing app-data placement.
121
+ export var inventoryBranch = ns('inventoryBranch');
122
+ export var inventoryIdentity = ns('inventoryIdentity');
123
+ export var inventoryStatus = ns('inventoryStatus');
124
+ export var inventorySource = ns('inventorySource');
125
+ export var inventorySourceId = ns('inventorySourceId');
126
+ export var inventoryAuthoritative = ns('inventoryAuthoritative');
127
+ export var inventoryFile = ns('inventoryFile');
128
+ export var inventoryLine = ns('inventoryLine');
129
+ export var inventoryOwner = ns('inventoryOwner');
130
+ export var inventoryShape = ns('inventoryShape');
131
+ export var inventoryProperty = ns('inventoryProperty');
132
+ export var inventoryNode = ns('inventoryNode');
133
+ export var inventoryTargetClass = ns('inventoryTargetClass');
134
+ export var inventoryObservedAt = ns('inventoryObservedAt');
135
+
136
+ /** Grouped export for prefix/name access. */
137
+ export const tr = {
138
+ TranslationLanguage,
139
+ languageIdentifier,
140
+ languageCode,
141
+ nativeName,
142
+ englishName,
143
+ textDirection,
144
+ parentLanguage,
145
+ fallbackLanguage,
146
+ languageEnabled,
147
+ languageSupported,
148
+ TranslationKey,
149
+ TranslationKeyVersion,
150
+ TranslationRelease,
151
+ TranslationUnit,
152
+ TranslationRevision,
153
+ GlossaryTerm,
154
+ TranslationInventoryOrigin,
155
+ namespace,
156
+ key,
157
+ sourceText,
158
+ description,
159
+ kind,
160
+ ofApplication,
161
+ ofNode,
162
+ ofField,
163
+ ofShape,
164
+ ofProperty,
165
+ fromPackage,
166
+ overridden,
167
+ shapeSource,
168
+ format,
169
+ currentVersion,
170
+ versionId,
171
+ sourceLanguage,
172
+ argumentSignature,
173
+ contractHash,
174
+ sourceHash,
175
+ supersedes,
176
+ createdBy,
177
+ branch,
178
+ releaseId,
179
+ buildId,
180
+ channel,
181
+ contractSetHash,
182
+ manifestHash,
183
+ hotfixSequence,
184
+ publishedAt,
185
+ supportedUntil,
186
+ qualitySummary,
187
+ languages,
188
+ defaultLanguage,
189
+ cdnTarget,
190
+ autoPublish,
191
+ reviewedOnlyLanguage,
192
+ mtDefault,
193
+ mtFormality,
194
+ mtStyleNote,
195
+ ofKey,
196
+ ofKeyVersion,
197
+ language,
198
+ text,
199
+ state,
200
+ updatedAt,
201
+ updatedBy,
202
+ origin,
203
+ status,
204
+ author,
205
+ authorKind,
206
+ mtProvider,
207
+ basedOnText,
208
+ note,
209
+ createdAt,
210
+ decidedAt,
211
+ decidedBy,
212
+ term,
213
+ glossaryTranslation,
214
+ glossaryLanguage,
215
+ termType,
216
+ useInstead,
217
+ caseSensitive,
218
+ inventoryBranch,
219
+ inventoryIdentity,
220
+ inventoryStatus,
221
+ inventorySource,
222
+ inventorySourceId,
223
+ inventoryAuthoritative,
224
+ inventoryFile,
225
+ inventoryLine,
226
+ inventoryOwner,
227
+ inventoryShape,
228
+ inventoryProperty,
229
+ inventoryNode,
230
+ inventoryTargetClass,
231
+ inventoryObservedAt,
232
+ };
233
+
234
+ linkedOntology(_this, ns, 'translation', loadData, '../data/translation.json');