@_linked/translation 0.9.3 → 0.9.5

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 (41) hide show
  1. package/package.json +5 -4
  2. package/src/archive.ts +0 -914
  3. package/src/authorization.ts +0 -56
  4. package/src/backend.ts +0 -2
  5. package/src/cdn-loader.ts +0 -264
  6. package/src/checks.ts +0 -413
  7. package/src/compile.ts +0 -168
  8. package/src/core/messages.ts +0 -139
  9. package/src/data/translation.ttl +0 -489
  10. package/src/discovery.ts +0 -594
  11. package/src/exchange.ts +0 -437
  12. package/src/export.ts +0 -138
  13. package/src/formats/csv.ts +0 -467
  14. package/src/formats/json.ts +0 -596
  15. package/src/formats/xliff.ts +0 -733
  16. package/src/import.ts +0 -648
  17. package/src/index.ts +0 -201
  18. package/src/key-sync-node.ts +0 -269
  19. package/src/key-sync.ts +0 -208
  20. package/src/key-version.ts +0 -207
  21. package/src/languages.ts +0 -102
  22. package/src/linked-discovery.ts +0 -385
  23. package/src/message-format.ts +0 -214
  24. package/src/mt.ts +0 -253
  25. package/src/ontologies/translation.register.ts +0 -17
  26. package/src/ontologies/translation.ts +0 -239
  27. package/src/package.ts +0 -14
  28. package/src/react.tsx +0 -378
  29. package/src/records.ts +0 -181
  30. package/src/release.ts +0 -754
  31. package/src/shape-catalog.ts +0 -198
  32. package/src/shapes/GlossaryTerm.ts +0 -61
  33. package/src/shapes/TranslationInventoryOrigin.ts +0 -131
  34. package/src/shapes/TranslationKey.ts +0 -183
  35. package/src/shapes/TranslationKeyVersion.ts +0 -77
  36. package/src/shapes/TranslationLanguage.ts +0 -51
  37. package/src/shapes/TranslationProvider.ts +0 -1927
  38. package/src/shapes/TranslationRelease.ts +0 -114
  39. package/src/shapes/TranslationRevision.ts +0 -105
  40. package/src/shapes/TranslationUnit.ts +0 -72
  41. package/src/shapes/index.ts +0 -13
@@ -1,214 +0,0 @@
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 DELETED
@@ -1,253 +0,0 @@
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 application and are
9
- * registered at boot.
10
- */
11
- import { sharedState } from '@_linked/core/utils/sharedState';
12
- import {
13
- analyzeMessage,
14
- compareMessageArguments,
15
- } from './message-format.js';
16
- import type { MessageFormat } from './core/messages.js';
17
-
18
- export interface MtItem {
19
- key: string;
20
- sourceText: string;
21
- /** Authoring context shown to the translator/model. */
22
- description?: string;
23
- format?: 'simple' | 'icu';
24
- }
25
-
26
- export interface MtGlossaryEntry {
27
- term: string;
28
- /** prefer (default) = translate as `translation`; keep = do-not-translate; forbid = never use. */
29
- termType?: 'prefer' | 'require' | 'keep' | 'forbid';
30
- /** Preferred translation (prefer entries); empty/undefined = keep the term as-is. */
31
- translation?: string;
32
- /** Replacement wording (forbid entries). */
33
- useInstead?: string;
34
- /** Target language this applies to; empty = all languages. */
35
- language?: string;
36
- /** Exact-casing matching (proper nouns / brand styling). */
37
- caseSensitive?: boolean;
38
- description?: string;
39
- }
40
-
41
- /** Register/tone controls (Plan 017 §7.4b) — NOT sampling temperature. */
42
- export interface MtStyle {
43
- formality?: 'formal' | 'informal';
44
- /** Free-text tone guidance, e.g. "friendly, energetic, no bureaucratese". */
45
- note?: string;
46
- }
47
-
48
- /** An established translation the provider should stay consistent with. */
49
- export interface MtReference {
50
- sourceText: string;
51
- text: string;
52
- }
53
-
54
- export interface MtBatchRequest {
55
- sourceLanguage: string;
56
- targetLanguage: string;
57
- items: MtItem[];
58
- glossary: MtGlossaryEntry[];
59
- style?: MtStyle;
60
- /** Translation-memory context: reviewed units for consistency (Plan 017 §7.4c). */
61
- references?: MtReference[];
62
- }
63
-
64
- export interface MtBatchResult {
65
- key: string;
66
- text: string;
67
- }
68
-
69
- export interface TranslationMtProvider {
70
- readonly key: string;
71
- /** Absent ⇒ the provider claims every language. */
72
- supportsLanguage?(language: string): boolean;
73
- translateBatch(request: MtBatchRequest): Promise<MtBatchResult[]>;
74
- }
75
-
76
- // On globalThis so a provider the host registers on one copy of this module is found by
77
- // another copy.
78
- const { providers } = sharedState('@_linked/translation', 'mt', () => ({
79
- providers: new Map<string, TranslationMtProvider>(),
80
- }));
81
-
82
- export function registerMtProvider(provider: TranslationMtProvider): void {
83
- providers.set(provider.key, provider);
84
- }
85
-
86
- export function getMtProvider(key: string): TranslationMtProvider | undefined {
87
- return providers.get(key);
88
- }
89
-
90
- export function listMtProviders(): TranslationMtProvider[] {
91
- return [...providers.values()];
92
- }
93
-
94
- /** Test seam. */
95
- export function clearMtProviders(): void {
96
- providers.clear();
97
- }
98
-
99
- /**
100
- * Ordered provider chain for one target language: the preferred provider
101
- * first when it exists AND supports the language, then every other provider
102
- * that supports it, registration order.
103
- */
104
- export function resolveMtProviders(
105
- targetLanguage: string,
106
- preferred?: string,
107
- ): TranslationMtProvider[] {
108
- const supports = (p: TranslationMtProvider) =>
109
- !p.supportsLanguage || p.supportsLanguage(targetLanguage);
110
- const chain = listMtProviders().filter(supports);
111
- if (!preferred) return chain;
112
- const first = chain.find((p) => p.key === preferred);
113
- if (!first) return chain;
114
- return [first, ...chain.filter((p) => p !== first)];
115
- }
116
-
117
- /** Unique `{placeholder}` tokens in a message, sorted for set comparison. */
118
- export function extractPlaceholders(
119
- text: string,
120
- format: MessageFormat = 'simple',
121
- ): string[] {
122
- return analyzeMessage(text, format).arguments.map(({ name }) => name);
123
- }
124
-
125
- /**
126
- * A translation is only usable if it preserves the source's placeholder SET
127
- * exactly — a dropped or renamed `{param}` breaks interpolation at runtime.
128
- */
129
- export function placeholdersIntact(
130
- source: string,
131
- translated: string,
132
- format: MessageFormat = 'simple',
133
- ): boolean {
134
- return compareMessageArguments(source, translated, format).valid;
135
- }
136
-
137
- /** Glossary entries applying to one target language (global + language-specific). */
138
- export function glossaryForLanguage(
139
- glossary: MtGlossaryEntry[],
140
- language: string,
141
- ): MtGlossaryEntry[] {
142
- return glossary.filter((g) => !g.language || g.language === language);
143
- }
144
-
145
- export function chunkItems<T>(items: T[], size: number): T[][] {
146
- if (size < 1) throw new Error('chunk size must be >= 1');
147
- const chunks: T[][] = [];
148
- for (let i = 0; i < items.length; i += size) chunks.push(items.slice(i, i + size));
149
- return chunks;
150
- }
151
-
152
- /**
153
- * Resolve the formality for one language from the encoded config entries
154
- * ("formal" = app default; "de:informal" = per-language override; override wins).
155
- */
156
- export function formalityForLanguage(
157
- entries: string[],
158
- language: string,
159
- ): 'formal' | 'informal' | undefined {
160
- let result: 'formal' | 'informal' | undefined;
161
- for (const entry of entries ?? []) {
162
- const i = entry.indexOf(':');
163
- const value = i === -1 ? entry : entry.slice(i + 1);
164
- if (value !== 'formal' && value !== 'informal') continue;
165
- if (i === -1) {
166
- if (!result) result = value;
167
- } else if (entry.slice(0, i) === language) {
168
- return value;
169
- }
170
- }
171
- return result;
172
- }
173
-
174
- // ── Consistency (Plan 017 §7.4c) ────────────────────────────────────────
175
-
176
- /** Minimal entry view the consistency helpers need (client-safe). */
177
- export interface ConsistencyEntry {
178
- key: string;
179
- namespace?: string;
180
- sourceText: string;
181
- format?: 'simple' | 'icu';
182
- units: Record<string, { text: string; state: string } | undefined>;
183
- }
184
-
185
- /** Identical English source translated DIFFERENTLY within one language. */
186
- export interface ConsistencyConflict {
187
- sourceText: string;
188
- language: string;
189
- /** One entry per distinct translation: the text + the keys carrying it. */
190
- variants: Array<{ text: string; keys: string[] }>;
191
- }
192
-
193
- /**
194
- * Find keys whose IDENTICAL source text has diverging translations in a
195
- * language — the core "same words, same translation" consistency check.
196
- * Empty/untranslated units are ignored; whitespace-trimmed comparison.
197
- */
198
- export function findInconsistentTranslations(
199
- entries: ConsistencyEntry[],
200
- language: string,
201
- ): ConsistencyConflict[] {
202
- const bySource = new Map<string, Map<string, string[]>>();
203
- for (const entry of entries) {
204
- const source = entry.sourceText.trim();
205
- const text = entry.units[language]?.text?.trim();
206
- if (!source || !text) continue;
207
- const variants = bySource.get(source) ?? new Map<string, string[]>();
208
- variants.set(text, [...(variants.get(text) ?? []), entry.key]);
209
- bySource.set(source, variants);
210
- }
211
- const conflicts: ConsistencyConflict[] = [];
212
- for (const [sourceText, variants] of bySource) {
213
- if (variants.size < 2) continue;
214
- conflicts.push({
215
- sourceText,
216
- language,
217
- variants: [...variants].map(([text, keys]) => ({ text, keys: keys.sort() })),
218
- });
219
- }
220
- return conflicts.sort((a, b) => a.sourceText.localeCompare(b.sourceText));
221
- }
222
-
223
- /**
224
- * Pick reviewed units as translation-memory REFERENCES for an MT batch:
225
- * same-namespace reviewed translations first (shared vocabulary), then the
226
- * shortest reviewed units overall (vocabulary-dense). Deterministic.
227
- */
228
- export function pickReferenceUnits(
229
- entries: ConsistencyEntry[],
230
- language: string,
231
- batchNamespaces: string[],
232
- max = 20,
233
- ): MtReference[] {
234
- const namespaces = new Set(batchNamespaces);
235
- const reviewed = entries
236
- .map((entry) => ({
237
- namespace: entry.namespace ?? '',
238
- sourceText: entry.sourceText.trim(),
239
- unit: entry.units[language],
240
- }))
241
- .filter(
242
- (row) =>
243
- row.sourceText &&
244
- row.unit?.state === 'reviewed' &&
245
- row.unit.text.trim(),
246
- );
247
- const score = (row: (typeof reviewed)[number]) =>
248
- (namespaces.has(row.namespace) ? 0 : 100000) + row.sourceText.length;
249
- return reviewed
250
- .sort((a, b) => score(a) - score(b) || a.sourceText.localeCompare(b.sourceText))
251
- .slice(0, max)
252
- .map((row) => ({ sourceText: row.sourceText, text: row.unit!.text.trim() }));
253
- }
@@ -1,17 +0,0 @@
1
- /**
2
- * Registers this ontology.
3
- *
4
- * Kept out of `translation.ts` because registration needs that module's whole export
5
- * namespace, and a module cannot import itself once a bundler is involved: Rollup
6
- * treats a static self-reference as a circular import and elides it, so the binding is
7
- * undefined at runtime and the consuming app dies at boot with `_this is not
8
- * defined`. `tsc` preserves it, which is why the pattern survived for as long as
9
- * packages were built with `tsc` alone.
10
- *
11
- * From a sibling module the same import is ordinary and survives.
12
- */
13
- import * as terms from './translation.js';
14
- import {dataFiles, loadData, ns} from './translation.js';
15
- import {linkedOntology} from '../package.js';
16
-
17
- linkedOntology(terms, ns, 'translation', loadData, dataFiles);