@lokascript/i18n 2.11.1 → 3.0.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/CHANGELOG.md +13 -0
- package/dist/browser.cjs +5 -1690
- package/dist/browser.cjs.map +1 -1
- package/dist/browser.d.cts +2 -2
- package/dist/browser.d.ts +2 -2
- package/dist/browser.js +6 -1685
- package/dist/browser.js.map +1 -1
- package/dist/dictionaries/index.cjs +5 -1
- package/dist/dictionaries/index.cjs.map +1 -1
- package/dist/dictionaries/index.js +5 -1
- package/dist/dictionaries/index.js.map +1 -1
- package/dist/{transformer-CsOeqayN.d.cts → index-BykxjYST.d.cts} +1 -232
- package/dist/{transformer-DWCTG1DQ.d.ts → index-DuIef8O7.d.ts} +1 -232
- package/dist/index.cjs +16 -1878
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +35 -35
- package/dist/index.d.ts +35 -35
- package/dist/index.js +17 -1873
- 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 +49 -2680
- package/dist/lokascript-i18n.mjs.map +1 -1
- package/dist/plugins/vite.cjs +5 -1
- package/dist/plugins/vite.cjs.map +1 -1
- package/dist/plugins/vite.js +5 -1
- package/dist/plugins/vite.js.map +1 -1
- package/dist/plugins/webpack.cjs +5 -1
- package/dist/plugins/webpack.cjs.map +1 -1
- package/dist/plugins/webpack.js +5 -1
- package/dist/plugins/webpack.js.map +1 -1
- package/package.json +4 -4
- package/src/browser.ts +0 -7
- package/src/compatibility/browser-tests/grammar-demo.spec.ts +22 -8
- package/src/constants.ts +1 -0
- package/src/dictionaries/bn.ts +5 -1
- package/src/grammar/index.ts +15 -9
- package/src/grammar/profiles.test.ts +440 -0
- package/src/index.ts +0 -7
- package/src/lexicon-parity.test.ts +77 -0
- package/src/grammar/grammar.test.ts +0 -2751
- package/src/grammar/transformer.ts +0 -2737
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lokascript/i18n",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "Keyword dictionaries, keyword providers and grammar profiles for hyperscript",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
7
7
|
"module": "dist/index.js",
|
|
@@ -64,8 +64,8 @@
|
|
|
64
64
|
},
|
|
65
65
|
"homepage": "https://github.com/codetalcott/hyperfixi/tree/main/packages/i18n#readme",
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"@hyperfixi/core": "^
|
|
68
|
-
"@lokascript/semantic": "^
|
|
67
|
+
"@hyperfixi/core": "^3.0.0",
|
|
68
|
+
"@lokascript/semantic": "^3.0.0",
|
|
69
69
|
"esbuild": "^0.28.0",
|
|
70
70
|
"happy-dom": "^20.10.6",
|
|
71
71
|
"vite": "^8.1.4"
|
package/src/browser.ts
CHANGED
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { test, expect } from '@playwright/test';
|
|
8
8
|
|
|
9
|
+
// NOTE (2026-08-28): no `playwright.config` in this package points at this
|
|
10
|
+
// directory — core's config has `testDir: './src/compatibility/browser-tests'`,
|
|
11
|
+
// which is core's own tree — so CI does not run this file. It is kept correct
|
|
12
|
+
// rather than deleted, but do not read a green CI as evidence it passes.
|
|
13
|
+
|
|
9
14
|
test.describe('Grammar Transformation Demo', () => {
|
|
10
15
|
test.beforeEach(async ({ page }) => {
|
|
11
16
|
await page.goto('http://127.0.0.1:3000/examples/multilingual/index.html');
|
|
@@ -13,17 +18,26 @@ test.describe('Grammar Transformation Demo', () => {
|
|
|
13
18
|
await page.waitForFunction(() => window.LokaScriptI18n !== undefined, { timeout: 5000 });
|
|
14
19
|
});
|
|
15
20
|
|
|
16
|
-
test('
|
|
21
|
+
test('the i18n bundle exposes vocabulary and profiles, not a translator', async ({ page }) => {
|
|
22
|
+
// Was `should load LokaScriptI18n bundle with GrammarTransformer`. That
|
|
23
|
+
// bundle no longer carries a translator: the grammar transformer was retired
|
|
24
|
+
// 2026-08-28 (4/24 canonical-valid on an en -> L -> en round trip against the
|
|
25
|
+
// real hyperscript.org parser, where semantic measures 24/24). The demo page
|
|
26
|
+
// translates through `hyperfixi.translate`, which IS semantic's.
|
|
17
27
|
const hasI18n = await page.evaluate(() => !!window.LokaScriptI18n);
|
|
18
28
|
expect(hasI18n).toBe(true);
|
|
19
29
|
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
const surface = await page.evaluate(() => ({
|
|
31
|
+
transformer: typeof (window.LokaScriptI18n as Record<string, unknown>).GrammarTransformer,
|
|
32
|
+
translate: typeof (window.LokaScriptI18n as Record<string, unknown>).translate,
|
|
33
|
+
getProfile: typeof (window.LokaScriptI18n as Record<string, unknown>).getProfile,
|
|
34
|
+
hyperfixiTranslate: typeof (window as unknown as { hyperfixi?: Record<string, unknown> })
|
|
35
|
+
.hyperfixi?.translate,
|
|
36
|
+
}));
|
|
37
|
+
expect(surface.transformer).toBe('undefined');
|
|
38
|
+
expect(surface.translate).toBe('undefined');
|
|
39
|
+
expect(surface.getProfile).toBe('function');
|
|
40
|
+
expect(surface.hyperfixiTranslate).toBe('function');
|
|
27
41
|
});
|
|
28
42
|
|
|
29
43
|
test('should have profile functions available', async ({ page }) => {
|
package/src/constants.ts
CHANGED
|
@@ -61,6 +61,7 @@ export const COMMAND_PRIMARY_ROLES: Readonly<Record<string, SemanticRole>> = {
|
|
|
61
61
|
if: 'condition',
|
|
62
62
|
unless: 'condition',
|
|
63
63
|
while: 'condition',
|
|
64
|
+
when: 'condition', // reactive `when <expr> changes` — the watched expression
|
|
64
65
|
repeat: 'loopType',
|
|
65
66
|
go: 'destination',
|
|
66
67
|
scroll: 'destination',
|
package/src/dictionaries/bn.ts
CHANGED
|
@@ -181,7 +181,11 @@ export const bengaliDictionary: Dictionary = {
|
|
|
181
181
|
random: 'এলোমেলো',
|
|
182
182
|
length: 'দৈর্ঘ্য',
|
|
183
183
|
index: 'সূচক',
|
|
184
|
-
empty
|
|
184
|
+
// The EXPRESSION `empty` is the state predicate (`if my value is empty`),
|
|
185
|
+
// not the command: `খালি-করুন` is the imperative "empty it!" and belongs to
|
|
186
|
+
// the `empty` COMMAND, which keeps it. Kept in step with the semantic
|
|
187
|
+
// lexicon by `lexicon-parity.test.ts`.
|
|
188
|
+
empty: 'খালি',
|
|
185
189
|
'starts with': 'দিয়ে_শুরু',
|
|
186
190
|
'ends with': 'দিয়ে_শেষ',
|
|
187
191
|
'ignoring case': 'কেস_উপেক্ষা',
|
package/src/grammar/index.ts
CHANGED
|
@@ -59,12 +59,18 @@ export {
|
|
|
59
59
|
getSupportedDirectPairs,
|
|
60
60
|
} from './direct-mappings';
|
|
61
61
|
|
|
62
|
-
// Transformer
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
62
|
+
// Transformer — RETIRED 2026-08-28.
|
|
63
|
+
//
|
|
64
|
+
// `transformer.ts` (2,747 lines) exported `GrammarTransformer`, `parseStatement`,
|
|
65
|
+
// `toLocale`, `toEnglish`, `translate` and `examples`. Every consumer has moved to
|
|
66
|
+
// @lokascript/semantic, which is what the 3,657-row corpus is written by and what
|
|
67
|
+
// every runtime surface in this repo already called: `@hyperscript-tools/i18n`
|
|
68
|
+
// (#999), the vite-plugin's generated bundle (#997), and the classic-i18n browser
|
|
69
|
+
// bundle, which dropped the four helpers rather than pay +173 KB gzipped to keep
|
|
70
|
+
// them (#998).
|
|
71
|
+
//
|
|
72
|
+
// The rest of this directory STAYS and is not part of that retirement: `profiles/`
|
|
73
|
+
// is imported by i18n's own `runtime.ts` and re-exported to the classic-i18n
|
|
74
|
+
// bundle; `types.ts` backs `constants.ts` and the role helpers above;
|
|
75
|
+
// `direct-mappings.ts` is part of the browser API (`types-browser/i18n-api.ts`
|
|
76
|
+
// declares it). This was never "delete the grammar directory".
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grammar PROFILES and role-transformation tests.
|
|
3
|
+
*
|
|
4
|
+
* What this file is: the surviving ~400 lines of the old 2,780-line
|
|
5
|
+
* `grammar.test.ts`, which tested `GrammarTransformer` and the five profile /
|
|
6
|
+
* role / dictionary describes below in one place. The transformer is retired
|
|
7
|
+
* (see grammar/index.ts); these are not.
|
|
8
|
+
*
|
|
9
|
+
* They cover the half of `grammar/` that STAYS and has its own consumers:
|
|
10
|
+
* `profiles/` (i18n's `runtime.ts`, and the classic-i18n browser bundle's
|
|
11
|
+
* `getProfile` / `getSupportedGrammarLocales` / `profiles` surface), the role
|
|
12
|
+
* helpers in `types.ts` (`reorderRoles` / `insertMarkers` / `joinTokens`, which
|
|
13
|
+
* `constants.ts` imports from), and the has/have dictionary entries.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, it, expect } from 'vitest';
|
|
17
|
+
import {
|
|
18
|
+
getProfile,
|
|
19
|
+
getSupportedLocales,
|
|
20
|
+
profiles,
|
|
21
|
+
englishProfile,
|
|
22
|
+
japaneseProfile,
|
|
23
|
+
chineseProfile,
|
|
24
|
+
arabicProfile,
|
|
25
|
+
} from './profiles';
|
|
26
|
+
import {
|
|
27
|
+
reorderRoles,
|
|
28
|
+
insertMarkers,
|
|
29
|
+
joinTokens,
|
|
30
|
+
UNIVERSAL_PATTERNS,
|
|
31
|
+
LANGUAGE_FAMILY_DEFAULTS,
|
|
32
|
+
type SemanticRole,
|
|
33
|
+
} from './types';
|
|
34
|
+
|
|
35
|
+
describe('Language Profiles', () => {
|
|
36
|
+
it('should have profiles for all supported locales', () => {
|
|
37
|
+
const locales = getSupportedLocales();
|
|
38
|
+
// Explicit expected list — when adding a profile, append it here so the
|
|
39
|
+
// count assertion stays meaningful without becoming a magic number.
|
|
40
|
+
const expectedLocales = [
|
|
41
|
+
'en',
|
|
42
|
+
'ja',
|
|
43
|
+
'ko',
|
|
44
|
+
'zh',
|
|
45
|
+
'ar',
|
|
46
|
+
'tr',
|
|
47
|
+
'es',
|
|
48
|
+
'de',
|
|
49
|
+
'fr',
|
|
50
|
+
'pt',
|
|
51
|
+
'id',
|
|
52
|
+
'ms',
|
|
53
|
+
'qu',
|
|
54
|
+
'sw',
|
|
55
|
+
'bn',
|
|
56
|
+
'it',
|
|
57
|
+
'ru',
|
|
58
|
+
'uk',
|
|
59
|
+
'vi',
|
|
60
|
+
'hi',
|
|
61
|
+
'tl',
|
|
62
|
+
'th',
|
|
63
|
+
'pl',
|
|
64
|
+
'he',
|
|
65
|
+
];
|
|
66
|
+
for (const code of expectedLocales) {
|
|
67
|
+
expect(locales, `missing profile: ${code}`).toContain(code);
|
|
68
|
+
}
|
|
69
|
+
expect(locales.length).toBe(expectedLocales.length);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should return undefined for unknown locales', () => {
|
|
73
|
+
expect(getProfile('xx')).toBeUndefined();
|
|
74
|
+
expect(getProfile('xyz')).toBeUndefined();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('English Profile', () => {
|
|
78
|
+
it('should have SVO word order', () => {
|
|
79
|
+
expect(englishProfile.wordOrder).toBe('SVO');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should use prepositions', () => {
|
|
83
|
+
expect(englishProfile.adpositionType).toBe('preposition');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should have required markers', () => {
|
|
87
|
+
const onMarker = englishProfile.markers.find(m => m.form === 'on');
|
|
88
|
+
expect(onMarker).toBeDefined();
|
|
89
|
+
expect(onMarker?.role).toBe('event');
|
|
90
|
+
expect(onMarker?.required).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('Japanese Profile', () => {
|
|
95
|
+
it('should have SOV word order', () => {
|
|
96
|
+
expect(japaneseProfile.wordOrder).toBe('SOV');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should use postpositions', () => {
|
|
100
|
+
expect(japaneseProfile.adpositionType).toBe('postposition');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should have particle markers', () => {
|
|
104
|
+
const woMarker = japaneseProfile.markers.find(m => m.form === 'を');
|
|
105
|
+
expect(woMarker).toBeDefined();
|
|
106
|
+
expect(woMarker?.role).toBe('patient');
|
|
107
|
+
expect(woMarker?.position).toBe('postposition');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should place patient before action in canonical order', () => {
|
|
111
|
+
const patientIndex = japaneseProfile.canonicalOrder.indexOf('patient');
|
|
112
|
+
const actionIndex = japaneseProfile.canonicalOrder.indexOf('action');
|
|
113
|
+
expect(patientIndex).toBeLessThan(actionIndex);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('Arabic Profile', () => {
|
|
118
|
+
it('should have VSO word order', () => {
|
|
119
|
+
expect(arabicProfile.wordOrder).toBe('VSO');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should be RTL', () => {
|
|
123
|
+
expect(arabicProfile.direction).toBe('rtl');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should place action first in canonical order', () => {
|
|
127
|
+
expect(arabicProfile.canonicalOrder[0]).toBe('action');
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe('Chinese Profile', () => {
|
|
132
|
+
it('should have isolating morphology', () => {
|
|
133
|
+
expect(chineseProfile.morphology).toBe('isolating');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should have circumfix markers for events', () => {
|
|
137
|
+
const eventMarkers = chineseProfile.markers.filter(m => m.role === 'event');
|
|
138
|
+
const hasPreposition = eventMarkers.some(m => m.position === 'preposition');
|
|
139
|
+
const hasPostposition = eventMarkers.some(m => m.position === 'postposition');
|
|
140
|
+
expect(hasPreposition).toBe(true);
|
|
141
|
+
expect(hasPostposition).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// =============================================================================
|
|
147
|
+
// Statement Parsing Tests
|
|
148
|
+
// =============================================================================
|
|
149
|
+
|
|
150
|
+
describe('Role Transformation', () => {
|
|
151
|
+
describe('reorderRoles', () => {
|
|
152
|
+
it('should reorder roles according to target order', () => {
|
|
153
|
+
const roles = new Map<SemanticRole, ParsedElement>([
|
|
154
|
+
['action', { role: 'action', value: 'increment' }],
|
|
155
|
+
['patient', { role: 'patient', value: '#count' }],
|
|
156
|
+
['event', { role: 'event', value: 'click' }],
|
|
157
|
+
]);
|
|
158
|
+
|
|
159
|
+
// Japanese order: patient, event, action
|
|
160
|
+
const reordered = reorderRoles(roles, ['patient', 'event', 'action']);
|
|
161
|
+
|
|
162
|
+
expect(reordered[0].role).toBe('patient');
|
|
163
|
+
expect(reordered[1].role).toBe('event');
|
|
164
|
+
expect(reordered[2].role).toBe('action');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should skip roles not present in input', () => {
|
|
168
|
+
const roles = new Map<SemanticRole, ParsedElement>([
|
|
169
|
+
['action', { role: 'action', value: 'toggle' }],
|
|
170
|
+
['patient', { role: 'patient', value: '.active' }],
|
|
171
|
+
]);
|
|
172
|
+
|
|
173
|
+
const reordered = reorderRoles(roles, ['patient', 'destination', 'action']);
|
|
174
|
+
|
|
175
|
+
expect(reordered.length).toBe(2);
|
|
176
|
+
expect(reordered[0].role).toBe('patient');
|
|
177
|
+
expect(reordered[1].role).toBe('action');
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe('insertMarkers', () => {
|
|
182
|
+
it('should insert preposition markers before elements', () => {
|
|
183
|
+
const elements: ParsedElement[] = [
|
|
184
|
+
{ role: 'destination', value: '#output', translated: '#output' },
|
|
185
|
+
];
|
|
186
|
+
const markers = [
|
|
187
|
+
{
|
|
188
|
+
form: 'to',
|
|
189
|
+
role: 'destination' as SemanticRole,
|
|
190
|
+
position: 'preposition' as const,
|
|
191
|
+
required: false,
|
|
192
|
+
},
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
const result = insertMarkers(elements, markers, 'preposition');
|
|
196
|
+
expect(result).toEqual(['to', '#output']);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should insert postposition markers after elements', () => {
|
|
200
|
+
const elements: ParsedElement[] = [
|
|
201
|
+
{ role: 'patient', value: '#count', translated: '#count' },
|
|
202
|
+
];
|
|
203
|
+
const markers = [
|
|
204
|
+
{
|
|
205
|
+
form: 'を',
|
|
206
|
+
role: 'patient' as SemanticRole,
|
|
207
|
+
position: 'postposition' as const,
|
|
208
|
+
required: true,
|
|
209
|
+
},
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
const result = insertMarkers(elements, markers, 'postposition');
|
|
213
|
+
expect(result).toEqual(['#count', 'を']);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should use translated values when available', () => {
|
|
217
|
+
const elements: ParsedElement[] = [
|
|
218
|
+
{ role: 'action', value: 'increment', translated: '増加' },
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
const result = insertMarkers(elements, [], 'none');
|
|
222
|
+
expect(result).toEqual(['増加']);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
describe('joinTokens', () => {
|
|
227
|
+
it('should join regular tokens with spaces', () => {
|
|
228
|
+
const result = joinTokens(['hello', 'world']);
|
|
229
|
+
expect(result).toBe('hello world');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should handle empty array', () => {
|
|
233
|
+
const result = joinTokens([]);
|
|
234
|
+
expect(result).toBe('');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should handle single token', () => {
|
|
238
|
+
const result = joinTokens(['hello']);
|
|
239
|
+
expect(result).toBe('hello');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should attach suffix markers without space (Quechua -ta)', () => {
|
|
243
|
+
// #count + -ta → #countta
|
|
244
|
+
const result = joinTokens(['#count', '-ta']);
|
|
245
|
+
expect(result).toBe('#countta');
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('should attach prefix markers without space (Arabic بـ-)', () => {
|
|
249
|
+
// بـ- + الماوس → بـالماوس
|
|
250
|
+
const result = joinTokens(['بـ-', 'الماوس']);
|
|
251
|
+
expect(result).toBe('بـالماوس');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('should handle multiple suffix markers (Turkish case suffixes)', () => {
|
|
255
|
+
// value + -i + another → valuei another
|
|
256
|
+
const result = joinTokens(['value', '-i', 'another']);
|
|
257
|
+
expect(result).toBe('valuei another');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should handle Japanese particles with normal spacing', () => {
|
|
261
|
+
// Japanese particles don't use hyphen notation, so they get spaces
|
|
262
|
+
const result = joinTokens(['#count', 'を', 'クリック', 'で', '増加']);
|
|
263
|
+
expect(result).toBe('#count を クリック で 増加');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('should handle Quechua agglutinative chain', () => {
|
|
267
|
+
// #count + -ta + click + -pi + increment
|
|
268
|
+
const result = joinTokens(['#count', '-ta', 'click', '-pi', 'increment']);
|
|
269
|
+
expect(result).toBe('#countta clickpi increment');
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('should handle mixed prefix and regular tokens', () => {
|
|
273
|
+
const result = joinTokens(['كـ-', 'JSON', 'format']);
|
|
274
|
+
expect(result).toBe('كـJSON format');
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
// =============================================================================
|
|
280
|
+
// Grammar Transformer Tests
|
|
281
|
+
// =============================================================================
|
|
282
|
+
|
|
283
|
+
describe('Universal Patterns', () => {
|
|
284
|
+
it('should define event-increment pattern', () => {
|
|
285
|
+
const pattern = UNIVERSAL_PATTERNS.eventIncrement;
|
|
286
|
+
expect(pattern.name).toBe('event-increment');
|
|
287
|
+
expect(pattern.roles).toContain('event');
|
|
288
|
+
expect(pattern.roles).toContain('action');
|
|
289
|
+
expect(pattern.roles).toContain('patient');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('should define put-into pattern', () => {
|
|
293
|
+
const pattern = UNIVERSAL_PATTERNS.putInto;
|
|
294
|
+
expect(pattern.name).toBe('put-into');
|
|
295
|
+
expect(pattern.roles).toContain('action');
|
|
296
|
+
expect(pattern.roles).toContain('patient');
|
|
297
|
+
expect(pattern.roles).toContain('destination');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('should define wait-duration pattern', () => {
|
|
301
|
+
const pattern = UNIVERSAL_PATTERNS.waitDuration;
|
|
302
|
+
expect(pattern.roles).toContain('action');
|
|
303
|
+
expect(pattern.roles).toContain('quantity');
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// =============================================================================
|
|
308
|
+
// Language Family Defaults Tests
|
|
309
|
+
// =============================================================================
|
|
310
|
+
|
|
311
|
+
describe('Language Family Defaults', () => {
|
|
312
|
+
it('should have Germanic defaults', () => {
|
|
313
|
+
const germanic = LANGUAGE_FAMILY_DEFAULTS.germanic;
|
|
314
|
+
expect(germanic.wordOrder).toBe('SVO');
|
|
315
|
+
expect(germanic.adpositionType).toBe('preposition');
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('should have Japonic defaults', () => {
|
|
319
|
+
const japonic = LANGUAGE_FAMILY_DEFAULTS.japonic;
|
|
320
|
+
expect(japonic.wordOrder).toBe('SOV');
|
|
321
|
+
expect(japonic.adpositionType).toBe('postposition');
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('should have Semitic defaults', () => {
|
|
325
|
+
const semitic = LANGUAGE_FAMILY_DEFAULTS.semitic;
|
|
326
|
+
expect(semitic.wordOrder).toBe('VSO');
|
|
327
|
+
expect(semitic.direction).toBe('rtl');
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('should have Sinitic defaults', () => {
|
|
331
|
+
const sinitic = LANGUAGE_FAMILY_DEFAULTS.sinitic;
|
|
332
|
+
expect(sinitic.morphology).toBe('isolating');
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// =============================================================================
|
|
337
|
+
// Examples Tests
|
|
338
|
+
// =============================================================================
|
|
339
|
+
|
|
340
|
+
describe('Has/Have Operator Translations', () => {
|
|
341
|
+
describe('Dictionary Entries', () => {
|
|
342
|
+
// Import dictionaries to verify has/have entries exist
|
|
343
|
+
it('should have has/have in English dictionary', async () => {
|
|
344
|
+
const { en } = await import('../dictionaries/en');
|
|
345
|
+
expect(en.logical.has).toBe('has');
|
|
346
|
+
expect(en.logical.have).toBe('have');
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('should have has/have in Spanish dictionary', async () => {
|
|
350
|
+
const { es } = await import('../dictionaries/es');
|
|
351
|
+
expect(es.logical.has).toBe('tiene'); // third-person
|
|
352
|
+
expect(es.logical.have).toBe('tengo'); // first-person
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('should have has/have in Japanese dictionary', async () => {
|
|
356
|
+
const { ja } = await import('../dictionaries/ja');
|
|
357
|
+
expect(ja.logical.has).toBe('ある');
|
|
358
|
+
expect(ja.logical.have).toBe('ある');
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('should have has/have in German dictionary', async () => {
|
|
362
|
+
const { de } = await import('../dictionaries/de');
|
|
363
|
+
expect(de.logical.has).toBe('hat'); // third-person
|
|
364
|
+
expect(de.logical.have).toBe('habe'); // first-person
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('should have has/have in French dictionary', async () => {
|
|
368
|
+
const { fr } = await import('../dictionaries/fr');
|
|
369
|
+
expect(fr.logical.has).toBe('a'); // third-person
|
|
370
|
+
expect(fr.logical.have).toBe('ai'); // first-person
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('should have has/have in Korean dictionary', async () => {
|
|
374
|
+
const { ko } = await import('../dictionaries/ko');
|
|
375
|
+
expect(ko.logical.has).toBe('있다');
|
|
376
|
+
expect(ko.logical.have).toBe('있다');
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('should have has/have in Chinese dictionary', async () => {
|
|
380
|
+
const { zh } = await import('../dictionaries/zh');
|
|
381
|
+
expect(zh.logical.has).toBe('有');
|
|
382
|
+
expect(zh.logical.have).toBe('有');
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it('should have has/have in Arabic dictionary', async () => {
|
|
386
|
+
const { ar } = await import('../dictionaries/ar');
|
|
387
|
+
expect(ar.logical.has).toBe('لديه'); // third-person
|
|
388
|
+
expect(ar.logical.have).toBe('لدي'); // first-person
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
describe('Conjugating Languages', () => {
|
|
393
|
+
// Languages that have different forms for has (3rd person) vs have (1st person)
|
|
394
|
+
const conjugatingLanguages = [
|
|
395
|
+
{ code: 'es', has: 'tiene', have: 'tengo' },
|
|
396
|
+
{ code: 'de', has: 'hat', have: 'habe' },
|
|
397
|
+
{ code: 'fr', has: 'a', have: 'ai' },
|
|
398
|
+
{ code: 'pt', has: 'tem', have: 'tenho' },
|
|
399
|
+
{ code: 'it', has: 'ha', have: 'ho' },
|
|
400
|
+
{ code: 'pl', has: 'ma', have: 'mam' },
|
|
401
|
+
];
|
|
402
|
+
|
|
403
|
+
for (const lang of conjugatingLanguages) {
|
|
404
|
+
it(`should have different has/have forms in ${lang.code}`, async () => {
|
|
405
|
+
const dict = await import(`../dictionaries/${lang.code}`);
|
|
406
|
+
const dictionary = Object.values(dict)[0] as { logical: { has: string; have: string } };
|
|
407
|
+
expect(dictionary.logical.has).toBe(lang.has);
|
|
408
|
+
expect(dictionary.logical.have).toBe(lang.have);
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
describe('Non-Conjugating Languages', () => {
|
|
414
|
+
// Languages that use the same form for both has and have
|
|
415
|
+
const sameFormLanguages = [
|
|
416
|
+
{ code: 'ja', form: 'ある' },
|
|
417
|
+
{ code: 'ko', form: '있다' },
|
|
418
|
+
{ code: 'zh', form: '有' },
|
|
419
|
+
{ code: 'tr', form: 'var' },
|
|
420
|
+
{ code: 'id', form: 'punya' },
|
|
421
|
+
{ code: 'vi', form: 'có' },
|
|
422
|
+
{ code: 'th', form: 'มี' },
|
|
423
|
+
{ code: 'tl', form: 'may' },
|
|
424
|
+
{ code: 'ms', form: 'ada' },
|
|
425
|
+
];
|
|
426
|
+
|
|
427
|
+
for (const lang of sameFormLanguages) {
|
|
428
|
+
it(`should have same has/have form in ${lang.code}`, async () => {
|
|
429
|
+
const dict = await import(`../dictionaries/${lang.code}`);
|
|
430
|
+
const dictionary = Object.values(dict)[0] as { logical: { has: string; have: string } };
|
|
431
|
+
expect(dictionary.logical.has).toBe(lang.form);
|
|
432
|
+
expect(dictionary.logical.have).toBe(lang.form);
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// =============================================================================
|
|
439
|
+
// Possessive Dot Notation Translation Tests
|
|
440
|
+
// =============================================================================
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile `lexicon` ↔ i18n Dictionary parity
|
|
3
|
+
*
|
|
4
|
+
* The semantic profile gained a `lexicon` block (2026-08-26) carrying the six
|
|
5
|
+
* Dictionary categories it never modelled — the vocabulary that appears INSIDE
|
|
6
|
+
* role values (`true`, `value`, `seconds`) rather than as a command verb or a
|
|
7
|
+
* role marker. The renderer needs it: without it, `set @disabled to true`
|
|
8
|
+
* renders into Spanish as `… a true` instead of `… a verdadero`, the Spanish
|
|
9
|
+
* parser cannot bind the value back, and the role is silently dropped.
|
|
10
|
+
*
|
|
11
|
+
* This test is the oracle for that move: every entry in the hand-authored
|
|
12
|
+
* dictionary must be reachable from the profile with the SAME rendering. It
|
|
13
|
+
* lives in @lokascript/i18n rather than @lokascript/semantic because semantic
|
|
14
|
+
* builds first and cannot import the dictionaries.
|
|
15
|
+
*
|
|
16
|
+
* Direction of authority: the dictionary is the incumbent, so it wins. A
|
|
17
|
+
* mismatch here means the profile drifted from a curated translation, not that
|
|
18
|
+
* the dictionary is stale.
|
|
19
|
+
*
|
|
20
|
+
* Nothing is deleted while this passes — the dictionaries remain the source for
|
|
21
|
+
* i18n's own consumers. It exists so a later derive-flip can be made safely.
|
|
22
|
+
*/
|
|
23
|
+
import { describe, it, expect } from 'vitest';
|
|
24
|
+
import { dictionaries } from './dictionaries';
|
|
25
|
+
import { KNOWN_PROFILES, getLexicon } from '@lokascript/semantic';
|
|
26
|
+
import type { LanguageProfile } from '@lokascript/semantic';
|
|
27
|
+
// The lexicons are separate modules so a parse-only consumer can drop them; the
|
|
28
|
+
// package's main entry pulls all 24 in (via `languages/_all`), which is what the
|
|
29
|
+
// import above already gives us. A consumer taking only `core` + `languages/{code}`
|
|
30
|
+
// would get none — that is the point of the split.
|
|
31
|
+
|
|
32
|
+
const CATEGORIES = [
|
|
33
|
+
'events',
|
|
34
|
+
'logical',
|
|
35
|
+
'temporal',
|
|
36
|
+
'values',
|
|
37
|
+
'attributes',
|
|
38
|
+
'expressions',
|
|
39
|
+
] as const;
|
|
40
|
+
|
|
41
|
+
const profiles = KNOWN_PROFILES as Readonly<Record<string, LanguageProfile>>;
|
|
42
|
+
const codes = Object.keys(dictionaries).filter(c => profiles[c]);
|
|
43
|
+
|
|
44
|
+
describe('profile.lexicon ↔ i18n dictionary parity', () => {
|
|
45
|
+
it('covers every language that has both a dictionary and a profile', () => {
|
|
46
|
+
expect(codes.length).toBe(24);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe.each(codes)('%s', code => {
|
|
50
|
+
const dict = dictionaries[code] as unknown as Record<string, Record<string, string>>;
|
|
51
|
+
const lexicon = (getLexicon(code) ?? {}) as Record<string, Record<string, { primary: string }>>;
|
|
52
|
+
|
|
53
|
+
it('has registered render vocabulary', () => {
|
|
54
|
+
expect(getLexicon(code), `${code} has no registered lexicon`).toBeDefined();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
for (const category of CATEGORIES) {
|
|
58
|
+
const entries = dict[category] ?? {};
|
|
59
|
+
const keys = Object.keys(entries);
|
|
60
|
+
if (keys.length === 0) continue;
|
|
61
|
+
|
|
62
|
+
it(`${category}: all ${keys.length} dictionary entries are present with the same rendering`, () => {
|
|
63
|
+
const missing: string[] = [];
|
|
64
|
+
const diverged: string[] = [];
|
|
65
|
+
for (const key of keys) {
|
|
66
|
+
const onProfile = lexicon[category]?.[key];
|
|
67
|
+
if (!onProfile) {
|
|
68
|
+
missing.push(key);
|
|
69
|
+
} else if (onProfile.primary !== entries[key]) {
|
|
70
|
+
diverged.push(`${key}: profile=${onProfile.primary} dictionary=${entries[key]}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
expect({ missing, diverged }).toEqual({ missing: [], diverged: [] });
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
});
|