@lokascript/i18n 1.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/README.md +286 -0
- package/dist/browser.cjs +7669 -0
- package/dist/browser.cjs.map +1 -0
- package/dist/browser.d.cts +50 -0
- package/dist/browser.d.ts +50 -0
- package/dist/browser.js +7592 -0
- package/dist/browser.js.map +1 -0
- package/dist/hyperfixi-i18n.min.js +2 -0
- package/dist/hyperfixi-i18n.min.js.map +1 -0
- package/dist/hyperfixi-i18n.mjs +8558 -0
- package/dist/hyperfixi-i18n.mjs.map +1 -0
- package/dist/index.cjs +14205 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +947 -0
- package/dist/index.d.ts +947 -0
- package/dist/index.js +14095 -0
- package/dist/index.js.map +1 -0
- package/dist/transformer-Ckask-yw.d.cts +1041 -0
- package/dist/transformer-Ckask-yw.d.ts +1041 -0
- package/package.json +84 -0
- package/src/browser.ts +122 -0
- package/src/compatibility/browser-tests/grammar-demo.spec.ts +169 -0
- package/src/constants.ts +366 -0
- package/src/dictionaries/ar.ts +233 -0
- package/src/dictionaries/bn.ts +156 -0
- package/src/dictionaries/de.ts +233 -0
- package/src/dictionaries/derive.ts +515 -0
- package/src/dictionaries/en.ts +237 -0
- package/src/dictionaries/es.ts +233 -0
- package/src/dictionaries/fr.ts +233 -0
- package/src/dictionaries/hi.ts +270 -0
- package/src/dictionaries/id.ts +233 -0
- package/src/dictionaries/index.ts +238 -0
- package/src/dictionaries/it.ts +233 -0
- package/src/dictionaries/ja.ts +233 -0
- package/src/dictionaries/ko.ts +233 -0
- package/src/dictionaries/ms.ts +276 -0
- package/src/dictionaries/pl.ts +239 -0
- package/src/dictionaries/pt.ts +237 -0
- package/src/dictionaries/qu.ts +233 -0
- package/src/dictionaries/ru.ts +270 -0
- package/src/dictionaries/sw.ts +233 -0
- package/src/dictionaries/th.ts +156 -0
- package/src/dictionaries/tl.ts +276 -0
- package/src/dictionaries/tr.ts +233 -0
- package/src/dictionaries/uk.ts +270 -0
- package/src/dictionaries/vi.ts +210 -0
- package/src/dictionaries/zh.ts +233 -0
- package/src/enhanced-i18n.test.ts +454 -0
- package/src/enhanced-i18n.ts +713 -0
- package/src/examples/new-languages.ts +326 -0
- package/src/formatting.test.ts +213 -0
- package/src/formatting.ts +416 -0
- package/src/grammar/direct-mappings.ts +353 -0
- package/src/grammar/grammar.test.ts +1053 -0
- package/src/grammar/index.ts +59 -0
- package/src/grammar/profiles/index.ts +860 -0
- package/src/grammar/transformer.ts +1318 -0
- package/src/grammar/types.ts +630 -0
- package/src/index.ts +202 -0
- package/src/new-languages.test.ts +389 -0
- package/src/parser/analyze-conflicts.test.ts +229 -0
- package/src/parser/ar.ts +40 -0
- package/src/parser/create-provider.ts +309 -0
- package/src/parser/de.ts +36 -0
- package/src/parser/es.ts +31 -0
- package/src/parser/fr.ts +31 -0
- package/src/parser/id.ts +34 -0
- package/src/parser/index.ts +50 -0
- package/src/parser/ja.ts +36 -0
- package/src/parser/ko.ts +37 -0
- package/src/parser/locale-manager.test.ts +198 -0
- package/src/parser/locale-manager.ts +197 -0
- package/src/parser/parser-integration.test.ts +439 -0
- package/src/parser/pt.ts +37 -0
- package/src/parser/qu.ts +37 -0
- package/src/parser/sw.ts +37 -0
- package/src/parser/tr.ts +38 -0
- package/src/parser/types.ts +113 -0
- package/src/parser/zh.ts +38 -0
- package/src/plugins/vite.ts +224 -0
- package/src/plugins/webpack.ts +124 -0
- package/src/pluralization.test.ts +197 -0
- package/src/pluralization.ts +393 -0
- package/src/runtime.ts +441 -0
- package/src/ssr-integration.ts +225 -0
- package/src/test-setup.ts +195 -0
- package/src/translation-validation.test.ts +171 -0
- package/src/translator.test.ts +252 -0
- package/src/translator.ts +297 -0
- package/src/types.ts +209 -0
- package/src/utils/locale.ts +190 -0
- package/src/utils/tokenizer-adapter.ts +469 -0
- package/src/utils/tokenizer.ts +19 -0
- package/src/validators/index.ts +174 -0
- package/src/validators/schema.ts +129 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// packages/i18n/src/parser/analyze-conflicts.test.ts
|
|
2
|
+
/**
|
|
3
|
+
* Analyzes dictionaries for parsing conflicts.
|
|
4
|
+
*
|
|
5
|
+
* A conflict occurs when the same locale word maps to different English words
|
|
6
|
+
* across different categories (e.g., 'de' in Spanish maps to both 'from' and 'of').
|
|
7
|
+
*
|
|
8
|
+
* The priority system in create-provider.ts handles these by giving priority to:
|
|
9
|
+
* commands > logical > events > values > temporal > modifiers > attributes
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect } from 'vitest';
|
|
13
|
+
import { es } from '../dictionaries/es';
|
|
14
|
+
import { ja } from '../dictionaries/ja';
|
|
15
|
+
import { fr } from '../dictionaries/fr';
|
|
16
|
+
import { de } from '../dictionaries/de';
|
|
17
|
+
import { ar } from '../dictionaries/ar';
|
|
18
|
+
import { ko } from '../dictionaries/ko';
|
|
19
|
+
import { zh } from '../dictionaries/zh';
|
|
20
|
+
import { tr } from '../dictionaries/tr';
|
|
21
|
+
import type { Dictionary } from '../types';
|
|
22
|
+
|
|
23
|
+
interface Conflict {
|
|
24
|
+
localeWord: string;
|
|
25
|
+
mappings: Array<{ english: string; category: string }>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function analyzeConflicts(dictionary: Dictionary, localeName: string): Conflict[] {
|
|
29
|
+
const conflicts: Conflict[] = [];
|
|
30
|
+
const wordMap = new Map<string, Array<{ english: string; category: string }>>();
|
|
31
|
+
|
|
32
|
+
const categories = [
|
|
33
|
+
{ name: 'commands', data: dictionary.commands },
|
|
34
|
+
{ name: 'modifiers', data: dictionary.modifiers },
|
|
35
|
+
{ name: 'events', data: dictionary.events },
|
|
36
|
+
{ name: 'logical', data: dictionary.logical },
|
|
37
|
+
{ name: 'temporal', data: dictionary.temporal },
|
|
38
|
+
{ name: 'values', data: dictionary.values },
|
|
39
|
+
{ name: 'attributes', data: dictionary.attributes },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
for (const { name, data } of categories) {
|
|
43
|
+
if (!data) continue;
|
|
44
|
+
for (const [english, locale] of Object.entries(data)) {
|
|
45
|
+
const normalized = locale.toLowerCase();
|
|
46
|
+
if (!wordMap.has(normalized)) {
|
|
47
|
+
wordMap.set(normalized, []);
|
|
48
|
+
}
|
|
49
|
+
wordMap.get(normalized)!.push({ english: english.toLowerCase(), category: name });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Find words with multiple mappings
|
|
54
|
+
for (const [localeWord, mappings] of wordMap) {
|
|
55
|
+
if (mappings.length > 1) {
|
|
56
|
+
// Check if they map to different English words
|
|
57
|
+
const uniqueEnglish = new Set(mappings.map(m => m.english));
|
|
58
|
+
if (uniqueEnglish.size > 1) {
|
|
59
|
+
conflicts.push({ localeWord, mappings });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return conflicts;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe('Dictionary Conflict Analysis', () => {
|
|
68
|
+
describe('Spanish (es)', () => {
|
|
69
|
+
it('should identify conflicts', () => {
|
|
70
|
+
const conflicts = analyzeConflicts(es, 'es');
|
|
71
|
+
console.log('\n=== Spanish Conflicts ===');
|
|
72
|
+
for (const c of conflicts) {
|
|
73
|
+
console.log(` '${c.localeWord}' maps to:`);
|
|
74
|
+
for (const m of c.mappings) {
|
|
75
|
+
console.log(` - '${m.english}' (${m.category})`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Known conflicts: 'en' (on/in/into), 'de' (from/of)
|
|
80
|
+
// These are handled by priority - commands win
|
|
81
|
+
expect(conflicts.length).toBeGreaterThan(0);
|
|
82
|
+
|
|
83
|
+
// Verify 'en' conflict exists
|
|
84
|
+
const enConflict = conflicts.find(c => c.localeWord === 'en');
|
|
85
|
+
expect(enConflict).toBeDefined();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('Japanese (ja)', () => {
|
|
90
|
+
it('should identify conflicts', () => {
|
|
91
|
+
const conflicts = analyzeConflicts(ja, 'ja');
|
|
92
|
+
console.log('\n=== Japanese Conflicts ===');
|
|
93
|
+
for (const c of conflicts) {
|
|
94
|
+
console.log(` '${c.localeWord}' maps to:`);
|
|
95
|
+
for (const m of c.mappings) {
|
|
96
|
+
console.log(` - '${m.english}' (${m.category})`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Document any conflicts found
|
|
101
|
+
if (conflicts.length === 0) {
|
|
102
|
+
console.log(' No conflicts found!');
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('French (fr)', () => {
|
|
108
|
+
it('should identify conflicts', () => {
|
|
109
|
+
const conflicts = analyzeConflicts(fr, 'fr');
|
|
110
|
+
console.log('\n=== French Conflicts ===');
|
|
111
|
+
for (const c of conflicts) {
|
|
112
|
+
console.log(` '${c.localeWord}' maps to:`);
|
|
113
|
+
for (const m of c.mappings) {
|
|
114
|
+
console.log(` - '${m.english}' (${m.category})`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 'à' maps to both 'to' and 'at', 'de' maps to 'from' and 'of'
|
|
119
|
+
const aConflict = conflicts.find(c => c.localeWord === 'à');
|
|
120
|
+
expect(aConflict).toBeDefined();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('German (de)', () => {
|
|
125
|
+
it('should identify conflicts', () => {
|
|
126
|
+
const conflicts = analyzeConflicts(de, 'de');
|
|
127
|
+
console.log('\n=== German Conflicts ===');
|
|
128
|
+
for (const c of conflicts) {
|
|
129
|
+
console.log(` '${c.localeWord}' maps to:`);
|
|
130
|
+
for (const m of c.mappings) {
|
|
131
|
+
console.log(` - '${m.english}' (${m.category})`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Document any conflicts
|
|
136
|
+
if (conflicts.length === 0) {
|
|
137
|
+
console.log(' No conflicts found!');
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('Arabic (ar)', () => {
|
|
143
|
+
it('should identify conflicts', () => {
|
|
144
|
+
const conflicts = analyzeConflicts(ar, 'ar');
|
|
145
|
+
console.log('\n=== Arabic Conflicts ===');
|
|
146
|
+
for (const c of conflicts) {
|
|
147
|
+
console.log(` '${c.localeWord}' maps to:`);
|
|
148
|
+
for (const m of c.mappings) {
|
|
149
|
+
console.log(` - '${m.english}' (${m.category})`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Document any conflicts
|
|
154
|
+
if (conflicts.length === 0) {
|
|
155
|
+
console.log(' No conflicts found!');
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe('Korean (ko)', () => {
|
|
161
|
+
it('should identify conflicts', () => {
|
|
162
|
+
const conflicts = analyzeConflicts(ko, 'ko');
|
|
163
|
+
console.log('\n=== Korean Conflicts ===');
|
|
164
|
+
for (const c of conflicts) {
|
|
165
|
+
console.log(` '${c.localeWord}' maps to:`);
|
|
166
|
+
for (const m of c.mappings) {
|
|
167
|
+
console.log(` - '${m.english}' (${m.category})`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Korean has notable conflicts: '에' (on/to/at), '동안' (for/while), '아니면' (unless/else)
|
|
172
|
+
const aeConflict = conflicts.find(c => c.localeWord === '에');
|
|
173
|
+
expect(aeConflict).toBeDefined();
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
describe('Chinese (zh)', () => {
|
|
178
|
+
it('should identify conflicts', () => {
|
|
179
|
+
const conflicts = analyzeConflicts(zh, 'zh');
|
|
180
|
+
console.log('\n=== Chinese Conflicts ===');
|
|
181
|
+
for (const c of conflicts) {
|
|
182
|
+
console.log(` '${c.localeWord}' maps to:`);
|
|
183
|
+
for (const m of c.mappings) {
|
|
184
|
+
console.log(` - '${m.english}' (${m.category})`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Chinese has notable conflicts: '当' (on/while), '获取' (take/get/fetch)
|
|
189
|
+
const dangConflict = conflicts.find(c => c.localeWord === '当');
|
|
190
|
+
expect(dangConflict).toBeDefined();
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
describe('Turkish (tr)', () => {
|
|
195
|
+
it('should identify conflicts', () => {
|
|
196
|
+
const conflicts = analyzeConflicts(tr, 'tr');
|
|
197
|
+
console.log('\n=== Turkish Conflicts ===');
|
|
198
|
+
for (const c of conflicts) {
|
|
199
|
+
console.log(` '${c.localeWord}' maps to:`);
|
|
200
|
+
for (const m of c.mappings) {
|
|
201
|
+
console.log(` - '${m.english}' (${m.category})`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Document any conflicts
|
|
206
|
+
if (conflicts.length === 0) {
|
|
207
|
+
console.log(' No conflicts found!');
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('Priority Resolution', () => {
|
|
213
|
+
it('should explain how conflicts are resolved', () => {
|
|
214
|
+
// This test documents the priority system
|
|
215
|
+
console.log('\n=== Conflict Resolution Strategy ===');
|
|
216
|
+
console.log('Priority order (highest to lowest):');
|
|
217
|
+
console.log(' 1. commands - core actions like toggle, put, set');
|
|
218
|
+
console.log(' 2. logical - operators like and, or, then, else');
|
|
219
|
+
console.log(' 3. events - DOM events like click, focus');
|
|
220
|
+
console.log(' 4. values - literals like true, false, me, it');
|
|
221
|
+
console.log(' 5. temporal - time units like seconds, ms');
|
|
222
|
+
console.log(' 6. modifiers - prepositions like to, from, with');
|
|
223
|
+
console.log(' 7. attributes - DOM properties like class, style');
|
|
224
|
+
console.log('\nExample: Spanish "en" maps to both "on" (command) and "in" (modifier)');
|
|
225
|
+
console.log(' → Resolution: "en" resolves to "on" because commands have priority');
|
|
226
|
+
expect(true).toBe(true);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
});
|
package/src/parser/ar.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// packages/i18n/src/parser/ar.ts
|
|
2
|
+
|
|
3
|
+
import { ar } from '../dictionaries/ar';
|
|
4
|
+
import { createKeywordProvider } from './create-provider';
|
|
5
|
+
import type { KeywordProvider } from './types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Arabic keyword provider for the hyperscript parser.
|
|
9
|
+
*
|
|
10
|
+
* Enables parsing hyperscript written in Arabic:
|
|
11
|
+
* - `على نقر بدل .active` → parses as `on click toggle .active`
|
|
12
|
+
* - `إذا صحيح ثم سجل "مرحبا"` → parses as `if true then log "hello"`
|
|
13
|
+
*
|
|
14
|
+
* English keywords are also accepted (mixed mode), so:
|
|
15
|
+
* - `على click بدل .active` also works (Arabic `على` + English `click`)
|
|
16
|
+
*
|
|
17
|
+
* Arabic is a crucial test case because:
|
|
18
|
+
* - Right-to-left (RTL) script
|
|
19
|
+
* - Different character set (Unicode Arabic block)
|
|
20
|
+
* - Space-separated words with diacritics
|
|
21
|
+
* - Tests parser's Unicode handling robustly
|
|
22
|
+
*
|
|
23
|
+
* Note: The parser handles RTL text correctly because tokenization
|
|
24
|
+
* is based on whitespace/operator boundaries, not visual direction.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* import { arKeywords } from '@lokascript/i18n/parser/ar';
|
|
29
|
+
* import { Parser } from '@lokascript/core';
|
|
30
|
+
*
|
|
31
|
+
* const parser = new Parser({ keywords: arKeywords });
|
|
32
|
+
* parser.parse('على نقر بدل .active');
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export const arKeywords: KeywordProvider = createKeywordProvider(ar, 'ar', {
|
|
36
|
+
allowEnglishFallback: true,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Re-export for convenience
|
|
40
|
+
export { ar as arDictionary } from '../dictionaries/ar';
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
// packages/i18n/src/parser/create-provider.ts
|
|
2
|
+
|
|
3
|
+
import type { Dictionary } from '../types';
|
|
4
|
+
import type { KeywordProvider, KeywordProviderOptions } from './types';
|
|
5
|
+
import {
|
|
6
|
+
ENGLISH_COMMANDS,
|
|
7
|
+
ENGLISH_KEYWORDS,
|
|
8
|
+
ENGLISH_MODIFIERS,
|
|
9
|
+
ENGLISH_LOGICAL_KEYWORDS,
|
|
10
|
+
ENGLISH_VALUE_KEYWORDS,
|
|
11
|
+
ENGLISH_EXPRESSION_KEYWORDS,
|
|
12
|
+
UNIVERSAL_ENGLISH_KEYWORDS,
|
|
13
|
+
} from '../constants';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Creates a KeywordProvider from a dictionary.
|
|
17
|
+
*
|
|
18
|
+
* The provider creates reverse mappings (locale → English) for fast
|
|
19
|
+
* resolution during parsing.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* import { es } from '../dictionaries/es';
|
|
24
|
+
* export const esKeywords = createKeywordProvider(es, 'es');
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function createKeywordProvider(
|
|
28
|
+
dictionary: Dictionary,
|
|
29
|
+
locale: string,
|
|
30
|
+
options: KeywordProviderOptions = {}
|
|
31
|
+
): KeywordProvider {
|
|
32
|
+
const { allowEnglishFallback = true } = options;
|
|
33
|
+
|
|
34
|
+
// Build reverse maps: locale keyword → English canonical
|
|
35
|
+
const reverseCommands = new Map<string, string>();
|
|
36
|
+
const reverseModifiers = new Map<string, string>();
|
|
37
|
+
const reverseEvents = new Map<string, string>();
|
|
38
|
+
const reverseLogical = new Map<string, string>();
|
|
39
|
+
const reverseTemporal = new Map<string, string>();
|
|
40
|
+
const reverseValues = new Map<string, string>();
|
|
41
|
+
const reverseAttributes = new Map<string, string>();
|
|
42
|
+
const reverseExpressions = new Map<string, string>();
|
|
43
|
+
const reverseAll = new Map<string, string>();
|
|
44
|
+
|
|
45
|
+
// Forward maps: English → locale keyword
|
|
46
|
+
const forwardAll = new Map<string, string>();
|
|
47
|
+
|
|
48
|
+
// Build reverse mappings from dictionary
|
|
49
|
+
// Note: reverseAll uses priority - first category to claim a locale word wins
|
|
50
|
+
function buildReverseMap(
|
|
51
|
+
category: Record<string, string>,
|
|
52
|
+
reverseMap: Map<string, string>,
|
|
53
|
+
priority: boolean = false
|
|
54
|
+
): void {
|
|
55
|
+
for (const [english, localeWord] of Object.entries(category)) {
|
|
56
|
+
const normalizedLocale = localeWord.toLowerCase();
|
|
57
|
+
reverseMap.set(normalizedLocale, english.toLowerCase());
|
|
58
|
+
// Only set in reverseAll if not already claimed (priority) or if this is a priority category
|
|
59
|
+
if (priority || !reverseAll.has(normalizedLocale)) {
|
|
60
|
+
reverseAll.set(normalizedLocale, english.toLowerCase());
|
|
61
|
+
}
|
|
62
|
+
forwardAll.set(english.toLowerCase(), localeWord.toLowerCase());
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Build all reverse maps
|
|
67
|
+
// Priority order: commands first (highest priority for parsing), then logical, events, values
|
|
68
|
+
// This ensures 'en' (Spanish) → 'on' (command) rather than 'in' (modifier)
|
|
69
|
+
if (dictionary.commands) {
|
|
70
|
+
buildReverseMap(dictionary.commands, reverseCommands, true);
|
|
71
|
+
}
|
|
72
|
+
if (dictionary.logical) {
|
|
73
|
+
buildReverseMap(dictionary.logical, reverseLogical, true);
|
|
74
|
+
}
|
|
75
|
+
if (dictionary.events) {
|
|
76
|
+
buildReverseMap(dictionary.events, reverseEvents);
|
|
77
|
+
}
|
|
78
|
+
if (dictionary.values) {
|
|
79
|
+
buildReverseMap(dictionary.values, reverseValues);
|
|
80
|
+
}
|
|
81
|
+
if (dictionary.temporal) {
|
|
82
|
+
buildReverseMap(dictionary.temporal, reverseTemporal);
|
|
83
|
+
}
|
|
84
|
+
// Modifiers last (lowest priority) - they often conflict with commands
|
|
85
|
+
if (dictionary.modifiers) {
|
|
86
|
+
buildReverseMap(dictionary.modifiers, reverseModifiers);
|
|
87
|
+
}
|
|
88
|
+
if (dictionary.attributes) {
|
|
89
|
+
buildReverseMap(dictionary.attributes, reverseAttributes);
|
|
90
|
+
}
|
|
91
|
+
if (dictionary.expressions) {
|
|
92
|
+
buildReverseMap(dictionary.expressions, reverseExpressions);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Collect all locale commands and keywords for completions
|
|
96
|
+
const localeCommands = new Set(reverseCommands.keys());
|
|
97
|
+
const allLocaleKeywords = new Set(reverseAll.keys());
|
|
98
|
+
|
|
99
|
+
// Add English keywords/commands if fallback is enabled
|
|
100
|
+
if (allowEnglishFallback) {
|
|
101
|
+
for (const cmd of ENGLISH_COMMANDS) {
|
|
102
|
+
localeCommands.add(cmd);
|
|
103
|
+
}
|
|
104
|
+
for (const kw of ENGLISH_KEYWORDS) {
|
|
105
|
+
allLocaleKeywords.add(kw);
|
|
106
|
+
}
|
|
107
|
+
for (const kw of UNIVERSAL_ENGLISH_KEYWORDS) {
|
|
108
|
+
allLocaleKeywords.add(kw);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
locale,
|
|
114
|
+
|
|
115
|
+
resolve(token: string): string | undefined {
|
|
116
|
+
const normalized = token.toLowerCase();
|
|
117
|
+
|
|
118
|
+
// Check if it's a locale keyword
|
|
119
|
+
const english = reverseAll.get(normalized);
|
|
120
|
+
if (english !== undefined) {
|
|
121
|
+
return english;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// If English fallback is enabled, check if it's already English
|
|
125
|
+
if (allowEnglishFallback) {
|
|
126
|
+
if (ENGLISH_COMMANDS.has(normalized) || ENGLISH_KEYWORDS.has(normalized)) {
|
|
127
|
+
return normalized;
|
|
128
|
+
}
|
|
129
|
+
// Universal keywords (DOM events, etc.) pass through
|
|
130
|
+
if (UNIVERSAL_ENGLISH_KEYWORDS.has(normalized)) {
|
|
131
|
+
return normalized;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return undefined;
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
isCommand(token: string): boolean {
|
|
139
|
+
const normalized = token.toLowerCase();
|
|
140
|
+
|
|
141
|
+
// Check locale commands
|
|
142
|
+
if (reverseCommands.has(normalized)) {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Check English commands if fallback enabled
|
|
147
|
+
if (allowEnglishFallback && ENGLISH_COMMANDS.has(normalized)) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return false;
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
isKeyword(token: string): boolean {
|
|
155
|
+
const normalized = token.toLowerCase();
|
|
156
|
+
|
|
157
|
+
// Check locale keywords (modifiers, logical, temporal, values)
|
|
158
|
+
if (
|
|
159
|
+
reverseModifiers.has(normalized) ||
|
|
160
|
+
reverseLogical.has(normalized) ||
|
|
161
|
+
reverseTemporal.has(normalized) ||
|
|
162
|
+
reverseValues.has(normalized)
|
|
163
|
+
) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Check English keywords if fallback enabled
|
|
168
|
+
if (allowEnglishFallback && ENGLISH_KEYWORDS.has(normalized)) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return false;
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
isEvent(token: string): boolean {
|
|
176
|
+
const normalized = token.toLowerCase();
|
|
177
|
+
|
|
178
|
+
// Check locale events
|
|
179
|
+
if (reverseEvents.has(normalized)) {
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Universal English events always accepted
|
|
184
|
+
if (UNIVERSAL_ENGLISH_KEYWORDS.has(normalized)) {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return false;
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
isModifier(token: string): boolean {
|
|
192
|
+
const normalized = token.toLowerCase();
|
|
193
|
+
return (
|
|
194
|
+
reverseModifiers.has(normalized) ||
|
|
195
|
+
(allowEnglishFallback && ENGLISH_MODIFIERS.has(normalized))
|
|
196
|
+
);
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
isLogical(token: string): boolean {
|
|
200
|
+
const normalized = token.toLowerCase();
|
|
201
|
+
return (
|
|
202
|
+
reverseLogical.has(normalized) ||
|
|
203
|
+
(allowEnglishFallback && ENGLISH_LOGICAL_KEYWORDS.has(normalized))
|
|
204
|
+
);
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
isValue(token: string): boolean {
|
|
208
|
+
const normalized = token.toLowerCase();
|
|
209
|
+
return (
|
|
210
|
+
reverseValues.has(normalized) ||
|
|
211
|
+
(allowEnglishFallback && ENGLISH_VALUE_KEYWORDS.has(normalized))
|
|
212
|
+
);
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
isExpression(token: string): boolean {
|
|
216
|
+
const normalized = token.toLowerCase();
|
|
217
|
+
return (
|
|
218
|
+
reverseExpressions.has(normalized) ||
|
|
219
|
+
(allowEnglishFallback && ENGLISH_EXPRESSION_KEYWORDS.has(normalized))
|
|
220
|
+
);
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
getCommands(): string[] {
|
|
224
|
+
return Array.from(localeCommands);
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
getKeywords(): string[] {
|
|
228
|
+
return Array.from(allLocaleKeywords);
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
toLocale(englishKeyword: string): string | undefined {
|
|
232
|
+
return forwardAll.get(englishKeyword.toLowerCase());
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Creates a default English-only keyword provider.
|
|
239
|
+
* This is used when no locale is specified.
|
|
240
|
+
*/
|
|
241
|
+
export function createEnglishProvider(): KeywordProvider {
|
|
242
|
+
return {
|
|
243
|
+
locale: 'en',
|
|
244
|
+
|
|
245
|
+
resolve(token: string): string | undefined {
|
|
246
|
+
const normalized = token.toLowerCase();
|
|
247
|
+
if (
|
|
248
|
+
ENGLISH_COMMANDS.has(normalized) ||
|
|
249
|
+
ENGLISH_KEYWORDS.has(normalized) ||
|
|
250
|
+
UNIVERSAL_ENGLISH_KEYWORDS.has(normalized)
|
|
251
|
+
) {
|
|
252
|
+
return normalized;
|
|
253
|
+
}
|
|
254
|
+
return undefined;
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
isCommand(token: string): boolean {
|
|
258
|
+
return ENGLISH_COMMANDS.has(token.toLowerCase());
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
isKeyword(token: string): boolean {
|
|
262
|
+
return ENGLISH_KEYWORDS.has(token.toLowerCase());
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
isEvent(token: string): boolean {
|
|
266
|
+
return UNIVERSAL_ENGLISH_KEYWORDS.has(token.toLowerCase());
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
isModifier(token: string): boolean {
|
|
270
|
+
return ENGLISH_MODIFIERS.has(token.toLowerCase());
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
isLogical(token: string): boolean {
|
|
274
|
+
return ENGLISH_LOGICAL_KEYWORDS.has(token.toLowerCase());
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
isValue(token: string): boolean {
|
|
278
|
+
return ENGLISH_VALUE_KEYWORDS.has(token.toLowerCase());
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
isExpression(token: string): boolean {
|
|
282
|
+
return ENGLISH_EXPRESSION_KEYWORDS.has(token.toLowerCase());
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
getCommands(): string[] {
|
|
286
|
+
return Array.from(ENGLISH_COMMANDS);
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
getKeywords(): string[] {
|
|
290
|
+
return Array.from(ENGLISH_KEYWORDS);
|
|
291
|
+
},
|
|
292
|
+
|
|
293
|
+
toLocale(_englishKeyword: string): string | undefined {
|
|
294
|
+
// English provider returns the keyword as-is
|
|
295
|
+
return _englishKeyword.toLowerCase();
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Re-export the English keyword sets from constants for use in core
|
|
301
|
+
export {
|
|
302
|
+
ENGLISH_COMMANDS,
|
|
303
|
+
ENGLISH_KEYWORDS,
|
|
304
|
+
ENGLISH_MODIFIERS,
|
|
305
|
+
ENGLISH_LOGICAL_KEYWORDS,
|
|
306
|
+
ENGLISH_VALUE_KEYWORDS,
|
|
307
|
+
ENGLISH_EXPRESSION_KEYWORDS,
|
|
308
|
+
UNIVERSAL_ENGLISH_KEYWORDS,
|
|
309
|
+
} from '../constants';
|
package/src/parser/de.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// packages/i18n/src/parser/de.ts
|
|
2
|
+
|
|
3
|
+
import { de } from '../dictionaries/de';
|
|
4
|
+
import { createKeywordProvider } from './create-provider';
|
|
5
|
+
import type { KeywordProvider } from './types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* German keyword provider for the hyperscript parser.
|
|
9
|
+
*
|
|
10
|
+
* Enables parsing hyperscript written in German:
|
|
11
|
+
* - `bei klick umschalten .active` → parses as `on click toggle .active`
|
|
12
|
+
* - `wenn wahr dann protokollieren "hallo"` → parses as `if true then log "hello"`
|
|
13
|
+
*
|
|
14
|
+
* English keywords are also accepted (mixed mode), so:
|
|
15
|
+
* - `bei click umschalten .active` also works (German `bei` + English `click`)
|
|
16
|
+
*
|
|
17
|
+
* German is a useful test case because:
|
|
18
|
+
* - Compound words (Zusammensetzungen) are common
|
|
19
|
+
* - Different word order in clauses (V2 in main, V-final in subordinate)
|
|
20
|
+
* - Case system (nominative, accusative, dative, genitive)
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* import { deKeywords } from '@lokascript/i18n/parser/de';
|
|
25
|
+
* import { Parser } from '@lokascript/core';
|
|
26
|
+
*
|
|
27
|
+
* const parser = new Parser({ keywords: deKeywords });
|
|
28
|
+
* parser.parse('bei klick umschalten .active');
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export const deKeywords: KeywordProvider = createKeywordProvider(de, 'de', {
|
|
32
|
+
allowEnglishFallback: true,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Re-export for convenience
|
|
36
|
+
export { de as deDictionary } from '../dictionaries/de';
|
package/src/parser/es.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// packages/i18n/src/parser/es.ts
|
|
2
|
+
|
|
3
|
+
import { es } from '../dictionaries/es';
|
|
4
|
+
import { createKeywordProvider } from './create-provider';
|
|
5
|
+
import type { KeywordProvider } from './types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Spanish keyword provider for the hyperscript parser.
|
|
9
|
+
*
|
|
10
|
+
* Enables parsing hyperscript written in Spanish:
|
|
11
|
+
* - `en clic alternar .active` → parses as `on click toggle .active`
|
|
12
|
+
* - `si verdadero entonces registrar "hola"` → parses as `if true then log "hello"`
|
|
13
|
+
*
|
|
14
|
+
* English keywords are also accepted (mixed mode), so:
|
|
15
|
+
* - `en click alternar .active` also works (Spanish `en` + English `click`)
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* import { esKeywords } from '@lokascript/i18n/parser/es';
|
|
20
|
+
* import { Parser } from '@lokascript/core';
|
|
21
|
+
*
|
|
22
|
+
* const parser = new Parser({ keywords: esKeywords });
|
|
23
|
+
* parser.parse('en clic alternar .active');
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export const esKeywords: KeywordProvider = createKeywordProvider(es, 'es', {
|
|
27
|
+
allowEnglishFallback: true,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Re-export for convenience
|
|
31
|
+
export { es as esDictionary } from '../dictionaries/es';
|
package/src/parser/fr.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// packages/i18n/src/parser/fr.ts
|
|
2
|
+
|
|
3
|
+
import { fr } from '../dictionaries/fr';
|
|
4
|
+
import { createKeywordProvider } from './create-provider';
|
|
5
|
+
import type { KeywordProvider } from './types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* French keyword provider for the hyperscript parser.
|
|
9
|
+
*
|
|
10
|
+
* Enables parsing hyperscript written in French:
|
|
11
|
+
* - `sur clic basculer .active` → parses as `on click toggle .active`
|
|
12
|
+
* - `si vrai alors enregistrer "bonjour"` → parses as `if true then log "hello"`
|
|
13
|
+
*
|
|
14
|
+
* English keywords are also accepted (mixed mode), so:
|
|
15
|
+
* - `sur click basculer .active` also works (French `sur` + English `click`)
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* import { frKeywords } from '@lokascript/i18n/parser/fr';
|
|
20
|
+
* import { Parser } from '@lokascript/core';
|
|
21
|
+
*
|
|
22
|
+
* const parser = new Parser({ keywords: frKeywords });
|
|
23
|
+
* parser.parse('sur clic basculer .active');
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export const frKeywords: KeywordProvider = createKeywordProvider(fr, 'fr', {
|
|
27
|
+
allowEnglishFallback: true,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Re-export for convenience
|
|
31
|
+
export { fr as frDictionary } from '../dictionaries/fr';
|