@lokascript/i18n 2.3.0 → 2.4.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/dist/browser.cjs +626 -473
- package/dist/browser.cjs.map +1 -1
- package/dist/browser.d.cts +25 -3
- package/dist/browser.d.ts +25 -3
- package/dist/browser.js +623 -474
- package/dist/browser.js.map +1 -1
- package/dist/dictionaries/index.cjs +489 -472
- package/dist/dictionaries/index.cjs.map +1 -1
- package/dist/dictionaries/index.d.cts +5 -3
- package/dist/dictionaries/index.d.ts +5 -3
- package/dist/dictionaries/index.js +489 -473
- package/dist/dictionaries/index.js.map +1 -1
- package/dist/index.cjs +617 -473
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -37
- package/dist/index.d.ts +37 -37
- package/dist/index.js +617 -473
- package/dist/index.js.map +1 -1
- package/dist/lokascript-i18n.min.js +1 -1
- package/dist/lokascript-i18n.min.js.map +1 -1
- package/dist/lokascript-i18n.mjs +730 -478
- package/dist/lokascript-i18n.mjs.map +1 -1
- package/dist/plugins/vite.cjs +488 -472
- package/dist/plugins/vite.cjs.map +1 -1
- package/dist/plugins/vite.js +488 -472
- package/dist/plugins/vite.js.map +1 -1
- package/dist/plugins/webpack.cjs +488 -472
- package/dist/plugins/webpack.cjs.map +1 -1
- package/dist/plugins/webpack.js +488 -472
- package/dist/plugins/webpack.js.map +1 -1
- package/dist/{transformer-B0zy-kti.d.ts → transformer-BLv389qz.d.ts} +12 -47
- package/dist/{transformer-CzY_vHU0.d.cts → transformer-PFsYELrc.d.cts} +12 -47
- package/dist/{types-BYtpqGq3.d.cts → types-BcALAO6h.d.cts} +1 -1
- package/dist/{types-BYtpqGq3.d.ts → types-BcALAO6h.d.ts} +1 -1
- package/package.json +5 -5
- package/src/browser.ts +3 -0
- package/src/constants.ts +9 -0
- package/src/dictionaries/ar.ts +22 -43
- package/src/dictionaries/bn.ts +17 -0
- package/src/dictionaries/de.ts +19 -40
- package/src/dictionaries/en.ts +26 -0
- package/src/dictionaries/es.ts +19 -40
- package/src/dictionaries/fr.ts +19 -40
- package/src/dictionaries/he.ts +2 -0
- package/src/dictionaries/hi.ts +18 -48
- package/src/dictionaries/id.ts +22 -43
- package/src/dictionaries/index.ts +5 -1
- package/src/dictionaries/it.ts +18 -39
- package/src/dictionaries/ja.ts +18 -39
- package/src/dictionaries/ko.ts +22 -43
- package/src/dictionaries/ms.ts +16 -50
- package/src/dictionaries/pl.ts +18 -45
- package/src/dictionaries/pt.ts +23 -48
- package/src/dictionaries/qu.ts +22 -43
- package/src/dictionaries/ru.ts +18 -48
- package/src/dictionaries/sw.ts +22 -43
- package/src/dictionaries/th.ts +17 -0
- package/src/dictionaries/tl.ts +16 -50
- package/src/dictionaries/tr.ts +24 -45
- package/src/dictionaries/uk.ts +18 -48
- package/src/dictionaries/vi.ts +17 -0
- package/src/dictionaries/zh.ts +22 -43
- package/src/grammar/grammar.test.ts +143 -24
- package/src/grammar/index.ts +1 -0
- package/src/grammar/profiles/index.ts +31 -0
- package/src/grammar/transformer.ts +161 -0
- package/src/parser/he.ts +29 -0
- package/src/parser/index.ts +1 -0
- package/src/schema-alignment.test.ts +154 -0
- package/src/test-utils/complete-languages.ts +40 -0
- package/src/translation-validation.test.ts +1 -26
|
@@ -62,6 +62,101 @@ function getCommandKeywordsForLocale(locale: string): Set<string> {
|
|
|
62
62
|
* Example: "wait 2s toggle .highlight"
|
|
63
63
|
* Returns: ["wait 2s", "toggle .highlight"]
|
|
64
64
|
*/
|
|
65
|
+
/**
|
|
66
|
+
* A reactive-block structure pulled apart by `extractBlockStructure`.
|
|
67
|
+
* Block-syntactic tokens (head, optional condition expression, optional
|
|
68
|
+
* connector, optional `end` tail) live outside the inner body so the
|
|
69
|
+
* body can be transformed by the regular pipeline (SOV reordering,
|
|
70
|
+
* possessives, etc.) without having block delimiters dragged into role
|
|
71
|
+
* values or moved by the canonical-order reorder.
|
|
72
|
+
*/
|
|
73
|
+
interface BlockStructure {
|
|
74
|
+
headKeyword: string;
|
|
75
|
+
/** Condition expression for `when X changes Y` / `unless X Y`. */
|
|
76
|
+
prefixExpr?: string;
|
|
77
|
+
/** `changes` for `when`; undefined for `live`/`unless`. */
|
|
78
|
+
connector?: string;
|
|
79
|
+
body: string;
|
|
80
|
+
/** `end` if the source had one; undefined otherwise. */
|
|
81
|
+
tailKeyword?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Detect a reactive block (`live ... end`, `when X changes Y [end]`,
|
|
86
|
+
* `unless X Y [end]`) and decompose it. Returns `null` when the input
|
|
87
|
+
* is not a reactive block, when there's content after the matched
|
|
88
|
+
* `end`, or when the heuristic can't locate a body — all of which fall
|
|
89
|
+
* through to the standard `parseStatement` path.
|
|
90
|
+
*/
|
|
91
|
+
function extractBlockStructure(input: string, sourceLocale: string): BlockStructure | null {
|
|
92
|
+
const tokens = input.split(/\s+/);
|
|
93
|
+
const head = tokens[0]?.toLowerCase();
|
|
94
|
+
if (!head || !BLOCK_HEAD_KEYWORDS.has(head)) return null;
|
|
95
|
+
|
|
96
|
+
// Depth-aware match for the closing `end` so nested blocks
|
|
97
|
+
// (`live when X changes Y end end`) slice correctly.
|
|
98
|
+
let depth = 1;
|
|
99
|
+
let endIdx = -1;
|
|
100
|
+
for (let i = 1; i < tokens.length; i++) {
|
|
101
|
+
const t = tokens[i].toLowerCase();
|
|
102
|
+
if (BLOCK_HEAD_KEYWORDS.has(t)) depth++;
|
|
103
|
+
else if (t === 'end') {
|
|
104
|
+
depth--;
|
|
105
|
+
if (depth === 0) {
|
|
106
|
+
endIdx = i;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// If there's trailing content after the matched `end`, bail out and
|
|
113
|
+
// let the existing splitter handle it. (`splitOnThen` normally
|
|
114
|
+
// separates trailing code before we get here.)
|
|
115
|
+
if (endIdx !== -1 && endIdx !== tokens.length - 1) return null;
|
|
116
|
+
|
|
117
|
+
const inner = endIdx !== -1 ? tokens.slice(1, endIdx) : tokens.slice(1);
|
|
118
|
+
const base: BlockStructure = { headKeyword: tokens[0], body: '' };
|
|
119
|
+
if (endIdx !== -1) base.tailKeyword = tokens[endIdx];
|
|
120
|
+
|
|
121
|
+
if (head === 'live') {
|
|
122
|
+
return { ...base, body: inner.join(' ') };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (head === 'when') {
|
|
126
|
+
// Reactive: `when <expr> changes <body>`. Without `changes`, fall
|
|
127
|
+
// through to the standard event-wait path (parseConditional).
|
|
128
|
+
const idx = inner.findIndex(t => t.toLowerCase() === 'changes');
|
|
129
|
+
if (idx >= 0) {
|
|
130
|
+
return {
|
|
131
|
+
...base,
|
|
132
|
+
prefixExpr: inner.slice(0, idx).join(' '),
|
|
133
|
+
connector: inner[idx],
|
|
134
|
+
body: inner.slice(idx + 1).join(' '),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// `unless <cond> <body>`: condition runs up to the first command
|
|
141
|
+
// keyword in `inner`. Heuristic — works because hyperscript bodies
|
|
142
|
+
// always start with a command verb, and `unless` conditions rarely
|
|
143
|
+
// contain bare command keywords as values.
|
|
144
|
+
const commands = getCommandKeywordsForLocale(sourceLocale);
|
|
145
|
+
let bodyStart = -1;
|
|
146
|
+
for (let i = 0; i < inner.length; i++) {
|
|
147
|
+
if (commands.has(inner[i].toLowerCase())) {
|
|
148
|
+
bodyStart = i;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (bodyStart <= 0) return null;
|
|
153
|
+
return {
|
|
154
|
+
...base,
|
|
155
|
+
prefixExpr: inner.slice(0, bodyStart).join(' '),
|
|
156
|
+
body: inner.slice(bodyStart).join(' '),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
65
160
|
function splitCompoundStatement(input: string, sourceLocale: string): string[] {
|
|
66
161
|
// First, split on newlines (preserving non-empty lines)
|
|
67
162
|
const lines = input
|
|
@@ -265,6 +360,19 @@ const BOUNDARY_MODIFIERS = new Set([
|
|
|
265
360
|
'over',
|
|
266
361
|
]);
|
|
267
362
|
|
|
363
|
+
/**
|
|
364
|
+
* Block-introducing keywords whose body should not be split at command
|
|
365
|
+
* boundaries by `splitOnCommandBoundaries`. Inputs starting with one of
|
|
366
|
+
* these are also routed around `parseStatement` entirely by
|
|
367
|
+
* `extractBlockStructure` + `transformBlock` so block-syntactic tokens
|
|
368
|
+
* never reach `parseCommand`/`parseConditional` (where they'd be
|
|
369
|
+
* misinterpreted as command verbs or swept into role values).
|
|
370
|
+
*
|
|
371
|
+
* `if` deliberately stays out: `if X then Y end` already works via the
|
|
372
|
+
* `splitOnThen` + `parseConditional` path.
|
|
373
|
+
*/
|
|
374
|
+
const BLOCK_HEAD_KEYWORDS = new Set(['live', 'when', 'unless']);
|
|
375
|
+
|
|
268
376
|
function splitOnCommandBoundaries(input: string, sourceLocale: string): string[] {
|
|
269
377
|
const commandKeywords = getCommandKeywordsForLocale(sourceLocale);
|
|
270
378
|
const tokens = input.split(/\s+/);
|
|
@@ -282,10 +390,23 @@ function splitOnCommandBoundaries(input: string, sourceLocale: string): string[]
|
|
|
282
390
|
// So we need to track whether we've seen the first command yet
|
|
283
391
|
let seenFirstCommand = !isEventHandler; // If not event handler, we're already past the "first command" phase
|
|
284
392
|
|
|
393
|
+
// Track block-scope depth (live/when/bind/if/unless/for/while/...). While
|
|
394
|
+
// inside a block, do not split on command boundaries — the body belongs
|
|
395
|
+
// to the block head and must transform as one unit. See comments on
|
|
396
|
+
// BLOCK_HEAD_KEYWORDS for the failure mode this prevents.
|
|
397
|
+
let blockDepth = 0;
|
|
398
|
+
|
|
285
399
|
for (let i = 0; i < tokens.length; i++) {
|
|
286
400
|
const token = tokens[i];
|
|
287
401
|
const lowerToken = token.toLowerCase();
|
|
288
402
|
|
|
403
|
+
// Update block-scope depth before any split decision.
|
|
404
|
+
if (BLOCK_HEAD_KEYWORDS.has(lowerToken)) {
|
|
405
|
+
blockDepth++;
|
|
406
|
+
} else if (lowerToken === 'end' && blockDepth > 0) {
|
|
407
|
+
blockDepth--;
|
|
408
|
+
}
|
|
409
|
+
|
|
289
410
|
// If this is a command keyword and we already have tokens in current part
|
|
290
411
|
if (commandKeywords.has(lowerToken) && currentPart.length > 0) {
|
|
291
412
|
// Check if the previous token looks like it could end a command
|
|
@@ -301,6 +422,13 @@ function splitOnCommandBoundaries(input: string, sourceLocale: string): string[]
|
|
|
301
422
|
continue;
|
|
302
423
|
}
|
|
303
424
|
|
|
425
|
+
// Don't split inside a block (live/when/bind/unless body, etc.).
|
|
426
|
+
// The block head and its body must transform as one statement.
|
|
427
|
+
if (blockDepth > 0) {
|
|
428
|
+
currentPart.push(token);
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
|
|
304
432
|
if (!BOUNDARY_MODIFIERS.has(prevLower) && !commandKeywords.has(prevLower)) {
|
|
305
433
|
// This looks like a command boundary - save current part and start new one
|
|
306
434
|
parts.push(currentPart.join(' '));
|
|
@@ -1165,6 +1293,15 @@ export class GrammarTransformer {
|
|
|
1165
1293
|
* Transform a single hyperscript statement (no compound "then" chains).
|
|
1166
1294
|
*/
|
|
1167
1295
|
private transformSingle(input: string): string {
|
|
1296
|
+
// 0. Reactive block? Route around parseStatement entirely so
|
|
1297
|
+
// block-syntactic tokens (live/when/unless/end) aren't treated
|
|
1298
|
+
// as command verbs or swept into role values, and so SOV/VSO
|
|
1299
|
+
// reorder applies only inside the body.
|
|
1300
|
+
const block = extractBlockStructure(input, this.sourceProfile.code);
|
|
1301
|
+
if (block) {
|
|
1302
|
+
return this.transformBlock(block);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1168
1305
|
// 1. Parse into semantic roles
|
|
1169
1306
|
const parsed = parseStatement(input, this.sourceProfile.code);
|
|
1170
1307
|
if (!parsed) {
|
|
@@ -1202,6 +1339,30 @@ export class GrammarTransformer {
|
|
|
1202
1339
|
return joinTokens(reordered.map(e => e.translated || e.value));
|
|
1203
1340
|
}
|
|
1204
1341
|
|
|
1342
|
+
/**
|
|
1343
|
+
* Translate a reactive block by translating the head/tail/connector
|
|
1344
|
+
* via the dictionary, recursively transforming the body through the
|
|
1345
|
+
* regular pipeline, and rejoining in source-language position order.
|
|
1346
|
+
* Block-syntactic tokens are never reordered: they're delimiters, not
|
|
1347
|
+
* arguments, and authors expect them at start/end positions
|
|
1348
|
+
* regardless of target word order.
|
|
1349
|
+
*/
|
|
1350
|
+
private transformBlock(block: BlockStructure): string {
|
|
1351
|
+
const src = this.sourceProfile.code;
|
|
1352
|
+
const dst = this.targetProfile.code;
|
|
1353
|
+
|
|
1354
|
+
const head = translateWord(block.headKeyword, src, dst);
|
|
1355
|
+
const tail = block.tailKeyword ? translateWord(block.tailKeyword, src, dst) : '';
|
|
1356
|
+
const connector = block.connector ? translateWord(block.connector, src, dst) : '';
|
|
1357
|
+
const prefix = block.prefixExpr ? translateMultiWordValue(block.prefixExpr, src, dst) : '';
|
|
1358
|
+
|
|
1359
|
+
// Recurse through `transform()` (not `transformSingle`) so the body
|
|
1360
|
+
// gets `then`-splitting and nested-block handling for free.
|
|
1361
|
+
const body = this.transform(block.body);
|
|
1362
|
+
|
|
1363
|
+
return [head, prefix, connector, body, tail].filter(s => s.length > 0).join(' ');
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1205
1366
|
/**
|
|
1206
1367
|
* Find the best matching rule for this statement
|
|
1207
1368
|
*/
|
package/src/parser/he.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// packages/i18n/src/parser/he.ts
|
|
2
|
+
|
|
3
|
+
import { he } from '../dictionaries/he';
|
|
4
|
+
import { createKeywordProvider } from './create-provider';
|
|
5
|
+
import type { KeywordProvider } from './types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Hebrew keyword provider for the hyperscript parser.
|
|
9
|
+
*
|
|
10
|
+
* Enables parsing hyperscript written in Hebrew (RTL, SVO):
|
|
11
|
+
* - `ב לחיצה מתג .active` → parses as `on click toggle .active`
|
|
12
|
+
*
|
|
13
|
+
* English keywords are also accepted (mixed mode).
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { heKeywords } from '@lokascript/i18n/parser/he';
|
|
18
|
+
* import { Parser } from '@hyperfixi/core';
|
|
19
|
+
*
|
|
20
|
+
* const parser = new Parser({ keywords: heKeywords });
|
|
21
|
+
* parser.parse('ב לחיצה מתג .active');
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export const heKeywords: KeywordProvider = createKeywordProvider(he, 'he', {
|
|
25
|
+
allowEnglishFallback: true,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Re-export for convenience
|
|
29
|
+
export { he as heDictionary } from '../dictionaries/he';
|
package/src/parser/index.ts
CHANGED
|
@@ -55,6 +55,7 @@ export { bnKeywords, bnDictionary } from './bn';
|
|
|
55
55
|
export { thKeywords, thDictionary } from './th';
|
|
56
56
|
export { msKeywords, msDictionary } from './ms';
|
|
57
57
|
export { tlKeywords, tlDictionary } from './tl';
|
|
58
|
+
export { heKeywords, heDictionary } from './he';
|
|
58
59
|
|
|
59
60
|
// Locale management
|
|
60
61
|
export { LocaleManager, detectBrowserLocale } from './locale-manager';
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema ↔ i18n Alignment Tests
|
|
3
|
+
*
|
|
4
|
+
* Verifies that every CommandSchema exported by @lokascript/semantic has a
|
|
5
|
+
* matching English dictionary entry in en.ts's `commands` category, and that
|
|
6
|
+
* every COMPLETE language dictionary covers the same set of command keys as
|
|
7
|
+
* English.
|
|
8
|
+
*
|
|
9
|
+
* Catches drift going forward: adding a new schema without updating en.ts,
|
|
10
|
+
* or adding commands to en.ts without propagating via the semantic profile
|
|
11
|
+
* generator.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, it, expect } from 'vitest';
|
|
15
|
+
import { commandSchemas } from '@lokascript/semantic';
|
|
16
|
+
import { dictionaries } from './dictionaries';
|
|
17
|
+
import type { Dictionary } from './types';
|
|
18
|
+
import { DICTIONARY_CATEGORIES } from './types';
|
|
19
|
+
import { COMPLETE_LANGUAGES } from './test-utils/complete-languages';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Dictionaries store each English keyword once in whichever category it
|
|
23
|
+
* primarily belongs to — `focus`/`blur` end up in `events`, `empty` in
|
|
24
|
+
* `expressions`. This helper looks up a keyword across all categories.
|
|
25
|
+
*/
|
|
26
|
+
function findInDictionary(dict: Dictionary, keyword: string): string | undefined {
|
|
27
|
+
for (const cat of DICTIONARY_CATEGORIES) {
|
|
28
|
+
const v = dict[cat]?.[keyword];
|
|
29
|
+
if (typeof v === 'string' && v.length > 0) return v;
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Actions that don't need a user-facing keyword translation at command
|
|
35
|
+
// position. They're either AST meta-nodes (`compound`) or definition-time
|
|
36
|
+
// feature keywords (`init`, `behavior`) handled elsewhere in the parser.
|
|
37
|
+
const NON_USER_FACING_ACTIONS = new Set(['compound', 'init', 'behavior']);
|
|
38
|
+
|
|
39
|
+
// Event handler 'on' is a top-level feature, not a command the user writes at
|
|
40
|
+
// command position in the same way. It's handled specially by the parser.
|
|
41
|
+
// Still, it has an i18n entry under `commands` in en.ts.
|
|
42
|
+
|
|
43
|
+
describe('Schema ↔ i18n alignment', () => {
|
|
44
|
+
const schemaActions = Object.keys(commandSchemas).filter(
|
|
45
|
+
action => !NON_USER_FACING_ACTIONS.has(action)
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
describe('every command schema has an English dictionary entry', () => {
|
|
49
|
+
const en = dictionaries.en as Dictionary;
|
|
50
|
+
schemaActions.forEach(action => {
|
|
51
|
+
it(`en dictionary contains '${action}'`, () => {
|
|
52
|
+
const value = findInDictionary(en, action);
|
|
53
|
+
expect(
|
|
54
|
+
value,
|
|
55
|
+
`CommandSchema '${action}' is exported from @lokascript/semantic but has no en.ts dictionary entry in any category. Add it to packages/i18n/src/dictionaries/en.ts and run \`npm run generate:language-assets\`.`
|
|
56
|
+
).toBeDefined();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('Phase 1 commands (v0.9.90) are present in all complete languages', () => {
|
|
62
|
+
const PHASE_1_COMMANDS = [
|
|
63
|
+
'focus',
|
|
64
|
+
'blur',
|
|
65
|
+
'empty',
|
|
66
|
+
'open',
|
|
67
|
+
'close',
|
|
68
|
+
'select',
|
|
69
|
+
'clear',
|
|
70
|
+
'reset',
|
|
71
|
+
'breakpoint',
|
|
72
|
+
] as const;
|
|
73
|
+
|
|
74
|
+
COMPLETE_LANGUAGES.forEach(code => {
|
|
75
|
+
const dict = dictionaries[code] as Dictionary | undefined;
|
|
76
|
+
if (!dict) return;
|
|
77
|
+
|
|
78
|
+
it(`${code}: has all 9 Phase 1 commands (any category)`, () => {
|
|
79
|
+
const missing: string[] = [];
|
|
80
|
+
for (const cmd of PHASE_1_COMMANDS) {
|
|
81
|
+
if (!findInDictionary(dict, cmd)) {
|
|
82
|
+
missing.push(cmd);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
expect(
|
|
86
|
+
missing,
|
|
87
|
+
`Missing Phase 1 commands in ${code}: ${missing.join(', ')}. ` +
|
|
88
|
+
`Fix by adding to packages/semantic/src/generators/profiles/${code}.ts ` +
|
|
89
|
+
`and running \`npm run generate:language-assets\`.`
|
|
90
|
+
).toEqual([]);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('Phase 2 comparators (v0.9.90) are present in all complete languages', () => {
|
|
96
|
+
// Expression operators wired into the core Pratt parser. Unlike Phase 1
|
|
97
|
+
// commands, these live in the hand-written `expressions` category (or
|
|
98
|
+
// `modifiers` for `between`) rather than deriving from semantic profiles.
|
|
99
|
+
const PHASE_2_COMPARATORS = ['starts with', 'ends with', 'between', 'ignoring case'] as const;
|
|
100
|
+
|
|
101
|
+
COMPLETE_LANGUAGES.forEach(code => {
|
|
102
|
+
const dict = dictionaries[code] as Dictionary | undefined;
|
|
103
|
+
if (!dict) return;
|
|
104
|
+
|
|
105
|
+
it(`${code}: has all Phase 2 comparators (any category)`, () => {
|
|
106
|
+
const missing: string[] = [];
|
|
107
|
+
for (const op of PHASE_2_COMPARATORS) {
|
|
108
|
+
if (!findInDictionary(dict, op)) {
|
|
109
|
+
missing.push(op);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
expect(
|
|
113
|
+
missing,
|
|
114
|
+
`Missing Phase 2 comparators in ${code}: ${missing.join(', ')}. ` +
|
|
115
|
+
`Add to packages/i18n/src/dictionaries/${code}.ts's \`expressions\` ` +
|
|
116
|
+
`category (or \`modifiers\` for \`between\`).`
|
|
117
|
+
).toEqual([]);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('Phase 3 collection ops (v0.9.90) are present in all complete languages', () => {
|
|
123
|
+
// Infix operators on arrays and strings — parsed as custom Pratt AST nodes
|
|
124
|
+
// by the core parser. `where` predates v0.9.90 and lives in `logical`; the
|
|
125
|
+
// four `<verb> by/to` forms live alongside `starts with` in `expressions`.
|
|
126
|
+
const PHASE_3_COLLECTIONS = [
|
|
127
|
+
'where',
|
|
128
|
+
'sorted by',
|
|
129
|
+
'mapped to',
|
|
130
|
+
'split by',
|
|
131
|
+
'joined by',
|
|
132
|
+
] as const;
|
|
133
|
+
|
|
134
|
+
COMPLETE_LANGUAGES.forEach(code => {
|
|
135
|
+
const dict = dictionaries[code] as Dictionary | undefined;
|
|
136
|
+
if (!dict) return;
|
|
137
|
+
|
|
138
|
+
it(`${code}: has all Phase 3 collection ops (any category)`, () => {
|
|
139
|
+
const missing: string[] = [];
|
|
140
|
+
for (const op of PHASE_3_COLLECTIONS) {
|
|
141
|
+
if (!findInDictionary(dict, op)) {
|
|
142
|
+
missing.push(op);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
expect(
|
|
146
|
+
missing,
|
|
147
|
+
`Missing Phase 3 collection ops in ${code}: ${missing.join(', ')}. ` +
|
|
148
|
+
`Add to packages/i18n/src/dictionaries/${code}.ts's \`expressions\` ` +
|
|
149
|
+
`category (or \`logical\` for \`where\`).`
|
|
150
|
+
).toEqual([]);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Languages with complete (non-placeholder) dictionaries.
|
|
3
|
+
*
|
|
4
|
+
* Shared by `translation-validation.test.ts` and `schema-alignment.test.ts`
|
|
5
|
+
* so a new dictionary only needs to be added in one place to enter the test
|
|
6
|
+
* matrix.
|
|
7
|
+
*
|
|
8
|
+
* This is intentionally distinct from `@lokascript/semantic`'s
|
|
9
|
+
* `SUPPORTED_LANGUAGES`: a language can be loadable by the semantic parser
|
|
10
|
+
* (e.g. `tr` / Turkish) yet not be "complete" by i18n's definition if its
|
|
11
|
+
* dictionary is missing newer Phase command/comparator entries. When a
|
|
12
|
+
* dictionary catches up to en.ts, add its code here to enable the full
|
|
13
|
+
* test sweep.
|
|
14
|
+
*/
|
|
15
|
+
export const COMPLETE_LANGUAGES = [
|
|
16
|
+
'en',
|
|
17
|
+
'es',
|
|
18
|
+
'ja',
|
|
19
|
+
'ko',
|
|
20
|
+
'ar',
|
|
21
|
+
'id',
|
|
22
|
+
'pt',
|
|
23
|
+
'it',
|
|
24
|
+
'vi',
|
|
25
|
+
'qu',
|
|
26
|
+
'sw',
|
|
27
|
+
'pl',
|
|
28
|
+
'ru',
|
|
29
|
+
'zh',
|
|
30
|
+
'hi',
|
|
31
|
+
'bn',
|
|
32
|
+
'de',
|
|
33
|
+
'th',
|
|
34
|
+
'fr',
|
|
35
|
+
'uk',
|
|
36
|
+
'tl',
|
|
37
|
+
'ms',
|
|
38
|
+
] as const;
|
|
39
|
+
|
|
40
|
+
export type CompleteLanguage = (typeof COMPLETE_LANGUAGES)[number];
|
|
@@ -8,36 +8,11 @@
|
|
|
8
8
|
import { describe, it, expect } from 'vitest';
|
|
9
9
|
import { dictionaries } from './dictionaries';
|
|
10
10
|
import type { Dictionary } from './types';
|
|
11
|
+
import { COMPLETE_LANGUAGES } from './test-utils/complete-languages';
|
|
11
12
|
|
|
12
13
|
// Focus on when/where which were the main TODO items we fixed
|
|
13
14
|
const REQUIRED_LOGICAL_KEYWORDS = ['when', 'where', 'and', 'or', 'not'] as const;
|
|
14
15
|
|
|
15
|
-
// Languages that are fully implemented (not placeholder stubs)
|
|
16
|
-
const COMPLETE_LANGUAGES = [
|
|
17
|
-
'en',
|
|
18
|
-
'es',
|
|
19
|
-
'ja',
|
|
20
|
-
'ko',
|
|
21
|
-
'ar',
|
|
22
|
-
'id',
|
|
23
|
-
'pt',
|
|
24
|
-
'it',
|
|
25
|
-
'vi',
|
|
26
|
-
'qu',
|
|
27
|
-
'sw',
|
|
28
|
-
'pl',
|
|
29
|
-
'ru',
|
|
30
|
-
'zh',
|
|
31
|
-
'hi',
|
|
32
|
-
'bn',
|
|
33
|
-
'de',
|
|
34
|
-
'th',
|
|
35
|
-
'fr',
|
|
36
|
-
'uk',
|
|
37
|
-
'tl',
|
|
38
|
-
'ms',
|
|
39
|
-
] as const;
|
|
40
|
-
|
|
41
16
|
// Languages with placeholder translations (none currently)
|
|
42
17
|
const INCOMPLETE_LANGUAGES = [] as const;
|
|
43
18
|
|