@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.
Files changed (96) hide show
  1. package/README.md +286 -0
  2. package/dist/browser.cjs +7669 -0
  3. package/dist/browser.cjs.map +1 -0
  4. package/dist/browser.d.cts +50 -0
  5. package/dist/browser.d.ts +50 -0
  6. package/dist/browser.js +7592 -0
  7. package/dist/browser.js.map +1 -0
  8. package/dist/hyperfixi-i18n.min.js +2 -0
  9. package/dist/hyperfixi-i18n.min.js.map +1 -0
  10. package/dist/hyperfixi-i18n.mjs +8558 -0
  11. package/dist/hyperfixi-i18n.mjs.map +1 -0
  12. package/dist/index.cjs +14205 -0
  13. package/dist/index.cjs.map +1 -0
  14. package/dist/index.d.cts +947 -0
  15. package/dist/index.d.ts +947 -0
  16. package/dist/index.js +14095 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/transformer-Ckask-yw.d.cts +1041 -0
  19. package/dist/transformer-Ckask-yw.d.ts +1041 -0
  20. package/package.json +84 -0
  21. package/src/browser.ts +122 -0
  22. package/src/compatibility/browser-tests/grammar-demo.spec.ts +169 -0
  23. package/src/constants.ts +366 -0
  24. package/src/dictionaries/ar.ts +233 -0
  25. package/src/dictionaries/bn.ts +156 -0
  26. package/src/dictionaries/de.ts +233 -0
  27. package/src/dictionaries/derive.ts +515 -0
  28. package/src/dictionaries/en.ts +237 -0
  29. package/src/dictionaries/es.ts +233 -0
  30. package/src/dictionaries/fr.ts +233 -0
  31. package/src/dictionaries/hi.ts +270 -0
  32. package/src/dictionaries/id.ts +233 -0
  33. package/src/dictionaries/index.ts +238 -0
  34. package/src/dictionaries/it.ts +233 -0
  35. package/src/dictionaries/ja.ts +233 -0
  36. package/src/dictionaries/ko.ts +233 -0
  37. package/src/dictionaries/ms.ts +276 -0
  38. package/src/dictionaries/pl.ts +239 -0
  39. package/src/dictionaries/pt.ts +237 -0
  40. package/src/dictionaries/qu.ts +233 -0
  41. package/src/dictionaries/ru.ts +270 -0
  42. package/src/dictionaries/sw.ts +233 -0
  43. package/src/dictionaries/th.ts +156 -0
  44. package/src/dictionaries/tl.ts +276 -0
  45. package/src/dictionaries/tr.ts +233 -0
  46. package/src/dictionaries/uk.ts +270 -0
  47. package/src/dictionaries/vi.ts +210 -0
  48. package/src/dictionaries/zh.ts +233 -0
  49. package/src/enhanced-i18n.test.ts +454 -0
  50. package/src/enhanced-i18n.ts +713 -0
  51. package/src/examples/new-languages.ts +326 -0
  52. package/src/formatting.test.ts +213 -0
  53. package/src/formatting.ts +416 -0
  54. package/src/grammar/direct-mappings.ts +353 -0
  55. package/src/grammar/grammar.test.ts +1053 -0
  56. package/src/grammar/index.ts +59 -0
  57. package/src/grammar/profiles/index.ts +860 -0
  58. package/src/grammar/transformer.ts +1318 -0
  59. package/src/grammar/types.ts +630 -0
  60. package/src/index.ts +202 -0
  61. package/src/new-languages.test.ts +389 -0
  62. package/src/parser/analyze-conflicts.test.ts +229 -0
  63. package/src/parser/ar.ts +40 -0
  64. package/src/parser/create-provider.ts +309 -0
  65. package/src/parser/de.ts +36 -0
  66. package/src/parser/es.ts +31 -0
  67. package/src/parser/fr.ts +31 -0
  68. package/src/parser/id.ts +34 -0
  69. package/src/parser/index.ts +50 -0
  70. package/src/parser/ja.ts +36 -0
  71. package/src/parser/ko.ts +37 -0
  72. package/src/parser/locale-manager.test.ts +198 -0
  73. package/src/parser/locale-manager.ts +197 -0
  74. package/src/parser/parser-integration.test.ts +439 -0
  75. package/src/parser/pt.ts +37 -0
  76. package/src/parser/qu.ts +37 -0
  77. package/src/parser/sw.ts +37 -0
  78. package/src/parser/tr.ts +38 -0
  79. package/src/parser/types.ts +113 -0
  80. package/src/parser/zh.ts +38 -0
  81. package/src/plugins/vite.ts +224 -0
  82. package/src/plugins/webpack.ts +124 -0
  83. package/src/pluralization.test.ts +197 -0
  84. package/src/pluralization.ts +393 -0
  85. package/src/runtime.ts +441 -0
  86. package/src/ssr-integration.ts +225 -0
  87. package/src/test-setup.ts +195 -0
  88. package/src/translation-validation.test.ts +171 -0
  89. package/src/translator.test.ts +252 -0
  90. package/src/translator.ts +297 -0
  91. package/src/types.ts +209 -0
  92. package/src/utils/locale.ts +190 -0
  93. package/src/utils/tokenizer-adapter.ts +469 -0
  94. package/src/utils/tokenizer.ts +19 -0
  95. package/src/validators/index.ts +174 -0
  96. package/src/validators/schema.ts +129 -0
@@ -0,0 +1,195 @@
1
+ // packages/i18n/src/test-setup.ts
2
+
3
+ import { vi } from 'vitest';
4
+
5
+ /**
6
+ * Test setup for HyperFixi i18n package
7
+ */
8
+
9
+ // Mock browser APIs for Node.js environment
10
+ Object.defineProperty(globalThis, 'navigator', {
11
+ value: {
12
+ language: 'en-US',
13
+ languages: ['en-US', 'en', 'fr'],
14
+ userLanguage: 'en-US',
15
+ },
16
+ writable: true,
17
+ });
18
+
19
+ Object.defineProperty(globalThis, 'window', {
20
+ value: {
21
+ location: {
22
+ href: 'http://localhost:3000',
23
+ search: '',
24
+ },
25
+ history: {
26
+ replaceState: vi.fn(),
27
+ },
28
+ localStorage: {
29
+ getItem: vi.fn(),
30
+ setItem: vi.fn(),
31
+ removeItem: vi.fn(),
32
+ },
33
+ addEventListener: vi.fn(),
34
+ removeEventListener: vi.fn(),
35
+ },
36
+ writable: true,
37
+ });
38
+
39
+ Object.defineProperty(globalThis, 'document', {
40
+ value: {
41
+ documentElement: {
42
+ lang: 'en',
43
+ dir: 'ltr',
44
+ },
45
+ title: 'Test Page',
46
+ cookie: '',
47
+ querySelectorAll: vi.fn(() => []),
48
+ createElement: vi.fn((tag: string) => ({
49
+ tagName: tag.toUpperCase(),
50
+ className: '',
51
+ dataset: {},
52
+ getAttribute: vi.fn(),
53
+ setAttribute: vi.fn(),
54
+ appendChild: vi.fn(),
55
+ addEventListener: vi.fn(),
56
+ textContent: '',
57
+ value: '',
58
+ selected: false,
59
+ })),
60
+ },
61
+ writable: true,
62
+ });
63
+
64
+ // Mock performance API
65
+ Object.defineProperty(globalThis, 'performance', {
66
+ value: {
67
+ now: () => Date.now(),
68
+ },
69
+ writable: true,
70
+ });
71
+
72
+ // Mock Intl APIs for consistent testing
73
+ // These need to work as constructors (called with `new`)
74
+ class MockNumberFormat {
75
+ private options: Intl.NumberFormatOptions;
76
+
77
+ constructor(_locale?: string, options?: Intl.NumberFormatOptions) {
78
+ this.options = options || {};
79
+ }
80
+
81
+ format(value: number): string {
82
+ const { style, currency, minimumFractionDigits, maximumFractionDigits } = this.options;
83
+
84
+ if (style === 'currency' && currency) {
85
+ const symbols: Record<string, string> = { USD: '$', EUR: '€', GBP: '£' };
86
+ const symbol = symbols[currency] || currency;
87
+ return `${symbol}${value.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',')}`;
88
+ }
89
+
90
+ if (style === 'percent') {
91
+ return `${(value * 100).toFixed(0)}%`;
92
+ }
93
+
94
+ // Default decimal formatting with proper grouping
95
+ const minFrac = minimumFractionDigits ?? 0;
96
+ const maxFrac = maximumFractionDigits ?? 3;
97
+ let formatted = value.toFixed(Math.max(minFrac, Math.min(maxFrac, 2)));
98
+ // Remove trailing zeros if not required
99
+ if (minFrac === 0 && formatted.includes('.')) {
100
+ formatted = formatted.replace(/\.?0+$/, '');
101
+ }
102
+ // Add thousand separators
103
+ const parts = formatted.split('.');
104
+ parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
105
+ return parts.join('.');
106
+ }
107
+ }
108
+
109
+ class MockDateTimeFormat {
110
+ private options: Intl.DateTimeFormatOptions;
111
+
112
+ constructor(_locale?: string, options?: Intl.DateTimeFormatOptions) {
113
+ this.options = options || {};
114
+ }
115
+
116
+ format(date: Date | number): string {
117
+ const d = new Date(date);
118
+ const month = String(d.getMonth() + 1).padStart(2, '0');
119
+ const day = String(d.getDate()).padStart(2, '0');
120
+ const year = d.getFullYear();
121
+ const hours = String(d.getHours()).padStart(2, '0');
122
+ const minutes = String(d.getMinutes()).padStart(2, '0');
123
+
124
+ if (this.options.timeStyle && this.options.dateStyle) {
125
+ return `${month}/${day}/${year}, ${hours}:${minutes}`;
126
+ }
127
+ if (this.options.timeStyle) {
128
+ return `${hours}:${minutes}`;
129
+ }
130
+ return `${month}/${day}/${year}`;
131
+ }
132
+ }
133
+
134
+ class MockRelativeTimeFormat {
135
+ constructor(_locale?: string, _options?: Intl.RelativeTimeFormatOptions) {
136
+ // Parameters unused in mock - kept for API compatibility
137
+ }
138
+
139
+ format(value: number, unit: Intl.RelativeTimeFormatUnit): string {
140
+ const absValue = Math.abs(value);
141
+ const unitStr = absValue === 1 ? unit : `${unit}s`;
142
+ if (value < 0) {
143
+ return `${absValue} ${unitStr} ago`;
144
+ }
145
+ return `in ${absValue} ${unitStr}`;
146
+ }
147
+ }
148
+
149
+ class MockListFormat {
150
+ constructor(_locale?: string, _options?: { style?: string; type?: string }) {}
151
+
152
+ format(items: string[]): string {
153
+ if (items.length <= 1) return items[0] || '';
154
+ if (items.length === 2) return `${items[0]} and ${items[1]}`;
155
+ return `${items.slice(0, -1).join(', ')}, and ${items[items.length - 1]}`;
156
+ }
157
+ }
158
+
159
+ Object.defineProperty(globalThis, 'Intl', {
160
+ value: {
161
+ NumberFormat: MockNumberFormat,
162
+ DateTimeFormat: MockDateTimeFormat,
163
+ RelativeTimeFormat: MockRelativeTimeFormat,
164
+ ListFormat: MockListFormat,
165
+ },
166
+ writable: true,
167
+ });
168
+
169
+ // Helper function to reset mocks between tests
170
+ export function resetMocks() {
171
+ vi.clearAllMocks();
172
+
173
+ // Reset DOM state
174
+ if (typeof document !== 'undefined') {
175
+ document.documentElement.lang = 'en';
176
+ document.documentElement.dir = 'ltr';
177
+ document.title = 'Test Page';
178
+ document.cookie = '';
179
+ }
180
+
181
+ // Reset localStorage
182
+ if (typeof window !== 'undefined' && window.localStorage) {
183
+ window.localStorage.getItem = vi.fn();
184
+ window.localStorage.setItem = vi.fn();
185
+ window.localStorage.removeItem = vi.fn();
186
+ }
187
+ }
188
+
189
+ // Mock node-html-parser for plugin tests
190
+ vi.mock('node-html-parser', () => ({
191
+ parse: vi.fn((html: string) => ({
192
+ toString: () => html,
193
+ querySelectorAll: vi.fn(() => []),
194
+ })),
195
+ }));
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Translation Validation Tests
3
+ *
4
+ * Verifies that all dictionaries have complete translations
5
+ * with no TODO placeholders remaining.
6
+ */
7
+
8
+ import { describe, it, expect } from 'vitest';
9
+ import { dictionaries } from './dictionaries';
10
+ import type { Dictionary } from './types';
11
+
12
+ // Focus on when/where which were the main TODO items we fixed
13
+ const REQUIRED_LOGICAL_KEYWORDS = ['when', 'where', 'and', 'or', 'not'] as const;
14
+
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
+ // Languages with placeholder translations (none currently)
42
+ const INCOMPLETE_LANGUAGES = [] as const;
43
+
44
+ describe('Translation Validation', () => {
45
+ describe('Logical keywords completeness', () => {
46
+ COMPLETE_LANGUAGES.forEach(code => {
47
+ const dict = dictionaries[code] as Dictionary | undefined;
48
+ if (!dict) return;
49
+
50
+ it(`${code}: all logical keywords are translated (no TODO)`, () => {
51
+ REQUIRED_LOGICAL_KEYWORDS.forEach(keyword => {
52
+ const value = dict.logical?.[keyword];
53
+ expect(value, `Missing logical keyword: ${keyword}`).toBeDefined();
54
+ expect(value, `${keyword} is still TODO`).not.toBe('TODO');
55
+ expect(typeof value === 'string' && value.length > 0, `${keyword} is empty`).toBe(true);
56
+ });
57
+ });
58
+ });
59
+ });
60
+
61
+ describe('No TODO placeholders in complete languages', () => {
62
+ COMPLETE_LANGUAGES.forEach(code => {
63
+ const dict = dictionaries[code] as Dictionary | undefined;
64
+ if (!dict) return;
65
+
66
+ it(`${code}: dictionary has no TODO values in core categories`, () => {
67
+ const coreCategories = ['commands', 'modifiers', 'logical', 'values'] as const;
68
+ const todos: string[] = [];
69
+
70
+ coreCategories.forEach(category => {
71
+ const section = dict[category];
72
+ if (section && typeof section === 'object') {
73
+ Object.entries(section).forEach(([key, value]) => {
74
+ if (value === 'TODO') {
75
+ todos.push(`${category}.${key}`);
76
+ }
77
+ });
78
+ }
79
+ });
80
+
81
+ expect(todos.length, `Found TODO values: ${todos.join(', ')}`).toBe(0);
82
+ });
83
+ });
84
+ });
85
+
86
+ describe('when/where linguistic validation', () => {
87
+ it('when keywords are temporal/conditional words (not interrogatives)', () => {
88
+ // These patterns verify the translations are temporal markers, not questions
89
+ const temporalPatterns: Record<string, RegExp> = {
90
+ es: /cuando/i, // Spanish: "cuando" (when as temporal)
91
+ pt: /quando/i, // Portuguese: "quando"
92
+ it: /quando/i, // Italian: "quando"
93
+ fr: /quand/i, // French: "quand"
94
+ ja: /とき|時/, // Japanese: "toki" (time/when)
95
+ ko: /때/, // Korean: "ttae" (time/when)
96
+ de: /wenn|wann/i, // German: "wenn" (conditional when)
97
+ ru: /когда/i, // Russian: "kogda"
98
+ uk: /коли/i, // Ukrainian: "koly"
99
+ pl: /kiedy/i, // Polish: "kiedy"
100
+ ar: /عندما/, // Arabic: "endama"
101
+ zh: /当|當/, // Chinese: "dang" (when)
102
+ };
103
+
104
+ Object.entries(temporalPatterns).forEach(([code, pattern]) => {
105
+ const dict = dictionaries[code] as Dictionary | undefined;
106
+ if (dict?.logical?.when && dict.logical.when !== 'TODO') {
107
+ expect(dict.logical.when, `${code}: 'when' should match temporal pattern`).toMatch(
108
+ pattern
109
+ );
110
+ }
111
+ });
112
+ });
113
+
114
+ it('where keywords are locative words', () => {
115
+ // These patterns verify the translations are locative, not interrogatives
116
+ const locativePatterns: Record<string, RegExp> = {
117
+ es: /donde|dónde/i, // Spanish: "donde"
118
+ pt: /onde/i, // Portuguese: "onde"
119
+ it: /dove/i, // Italian: "dove"
120
+ fr: /où/i, // French: "où"
121
+ de: /wo/i, // German: "wo"
122
+ ru: /где/i, // Russian: "gde"
123
+ uk: /де/i, // Ukrainian: "de"
124
+ pl: /gdzie/i, // Polish: "gdzie"
125
+ ar: /أين/, // Arabic: "ayna"
126
+ zh: /哪里|哪裡/, // Chinese: "nali"
127
+ };
128
+
129
+ Object.entries(locativePatterns).forEach(([code, pattern]) => {
130
+ const dict = dictionaries[code] as Dictionary | undefined;
131
+ if (dict?.logical?.where && dict.logical.where !== 'TODO') {
132
+ expect(dict.logical.where, `${code}: 'where' should match locative pattern`).toMatch(
133
+ pattern
134
+ );
135
+ }
136
+ });
137
+ });
138
+ });
139
+
140
+ describe('Dictionary structure validation', () => {
141
+ Object.entries(dictionaries).forEach(([code, dict]) => {
142
+ it(`${code}: has required top-level categories`, () => {
143
+ const requiredCategories = ['commands', 'modifiers', 'logical'];
144
+ requiredCategories.forEach(category => {
145
+ expect(dict, `Missing category: ${category}`).toHaveProperty(category);
146
+ });
147
+ });
148
+ });
149
+ });
150
+
151
+ describe('All languages are complete', () => {
152
+ it('no incomplete languages remain', () => {
153
+ expect(INCOMPLETE_LANGUAGES.length).toBe(0);
154
+ });
155
+
156
+ // Verify tl and ms are now complete
157
+ ['tl', 'ms'].forEach(code => {
158
+ const dict = dictionaries[code] as Dictionary | undefined;
159
+ if (!dict) return;
160
+
161
+ it(`${code}: is now fully translated (no TODO placeholders in core)`, () => {
162
+ const coreValues = [
163
+ ...Object.values(dict.commands || {}),
164
+ ...Object.values(dict.logical || {}),
165
+ ];
166
+ const todos = coreValues.filter(v => v === 'TODO');
167
+ expect(todos.length, `${code} still has TODO values`).toBe(0);
168
+ });
169
+ });
170
+ });
171
+ });
@@ -0,0 +1,252 @@
1
+ // packages/i18n/src/translator.test.ts
2
+
3
+ import { describe, it, expect, beforeEach } from 'vitest';
4
+ import { HyperscriptTranslator } from './translator';
5
+ import { Dictionary } from './types';
6
+
7
+ describe('HyperscriptTranslator', () => {
8
+ let translator: HyperscriptTranslator;
9
+
10
+ const testDictionary: Dictionary = {
11
+ commands: {
12
+ on: 'sur',
13
+ click: 'cliquer',
14
+ set: 'définir',
15
+ get: 'obtenir',
16
+ },
17
+ modifiers: {
18
+ to: 'à',
19
+ from: 'de',
20
+ },
21
+ events: {
22
+ click: 'clic',
23
+ },
24
+ logical: {
25
+ and: 'et',
26
+ or: 'ou',
27
+ },
28
+ temporal: {
29
+ seconds: 'secondes',
30
+ },
31
+ values: {
32
+ true: 'vrai',
33
+ false: 'faux',
34
+ },
35
+ attributes: {
36
+ class: 'classe',
37
+ },
38
+ };
39
+
40
+ beforeEach(() => {
41
+ translator = new HyperscriptTranslator({
42
+ locale: 'en',
43
+ dictionaries: {
44
+ fr: testDictionary,
45
+ },
46
+ });
47
+ });
48
+
49
+ describe('basic translation', () => {
50
+ it('should translate simple commands', () => {
51
+ const result = translator.translate('on click set #value', {
52
+ from: 'en',
53
+ to: 'fr',
54
+ });
55
+
56
+ expect(result).toBe('sur clic définir #value');
57
+ });
58
+
59
+ it('should handle mixed content', () => {
60
+ const result = translator.translate('on click get .className and set #result', {
61
+ from: 'en',
62
+ to: 'fr',
63
+ });
64
+
65
+ expect(result).toBe('sur clic obtenir .className et définir #result');
66
+ });
67
+
68
+ it('should preserve identifiers and literals', () => {
69
+ const result = translator.translate('on click set #myId to "hello world"', {
70
+ from: 'en',
71
+ to: 'fr',
72
+ });
73
+
74
+ expect(result).toBe('sur clic définir #myId à "hello world"');
75
+ });
76
+
77
+ it('should handle boolean values', () => {
78
+ const result = translator.translate('set #visible to true', {
79
+ from: 'en',
80
+ to: 'fr',
81
+ });
82
+
83
+ expect(result).toBe('définir #visible à vrai');
84
+ });
85
+ });
86
+
87
+ describe('translation with details', () => {
88
+ it('should return detailed translation result', () => {
89
+ const result = translator.translateWithDetails('on click set #value', {
90
+ from: 'en',
91
+ to: 'fr',
92
+ });
93
+
94
+ expect(result.translated).toBe('sur clic définir #value');
95
+ expect(result.locale.from).toBe('en');
96
+ expect(result.locale.to).toBe('fr');
97
+ expect(result.tokens).toBeDefined();
98
+ expect(result.tokens.length).toBeGreaterThan(0);
99
+ });
100
+
101
+ it('should preserve original when requested', () => {
102
+ const result = translator.translateWithDetails('on click', {
103
+ from: 'en',
104
+ to: 'fr',
105
+ preserveOriginal: true,
106
+ });
107
+
108
+ expect(result.original).toBe('on click');
109
+ });
110
+ });
111
+
112
+ describe('language detection', () => {
113
+ it('should detect language from content', () => {
114
+ const detected = translator.detectLanguage('sur clic définir');
115
+ expect(detected).toBe('fr');
116
+ });
117
+
118
+ it('should fallback to default when detection disabled', () => {
119
+ const translator2 = new HyperscriptTranslator({
120
+ locale: 'en',
121
+ detectLocale: false,
122
+ });
123
+
124
+ const detected = translator2.detectLanguage('sur clic définir');
125
+ expect(detected).toBe('en');
126
+ });
127
+ });
128
+
129
+ describe('dictionary management', () => {
130
+ it('should add new dictionaries', () => {
131
+ const newDict: Dictionary = {
132
+ commands: { test: 'prueba' },
133
+ modifiers: {},
134
+ events: {},
135
+ logical: {},
136
+ temporal: {},
137
+ values: {},
138
+ attributes: {},
139
+ };
140
+
141
+ translator.addDictionary('es', newDict);
142
+ expect(translator.getSupportedLocales()).toContain('es');
143
+ });
144
+
145
+ it('should validate dictionaries', () => {
146
+ const validation = translator.validateDictionary('fr');
147
+ expect(validation).toBeDefined();
148
+ expect(typeof validation.valid).toBe('boolean');
149
+ });
150
+ });
151
+
152
+ describe('RTL support', () => {
153
+ it('should identify RTL locales', () => {
154
+ expect(translator.isRTL('ar')).toBe(true);
155
+ expect(translator.isRTL('he')).toBe(true);
156
+ expect(translator.isRTL('en')).toBe(false);
157
+ expect(translator.isRTL('fr')).toBe(false);
158
+ });
159
+ });
160
+
161
+ describe('completions', () => {
162
+ it('should provide completions for partial input', () => {
163
+ const completions = translator.getCompletions({
164
+ text: 'on cl',
165
+ position: 5,
166
+ locale: 'fr',
167
+ });
168
+
169
+ expect(completions).toContain('cliquer');
170
+ });
171
+
172
+ it('should handle empty partial input', () => {
173
+ const completions = translator.getCompletions({
174
+ text: 'on ',
175
+ position: 3,
176
+ locale: 'fr',
177
+ });
178
+
179
+ expect(completions.length).toBeGreaterThan(0);
180
+ });
181
+ });
182
+
183
+ describe('edge cases', () => {
184
+ it('should handle same source and target locale', () => {
185
+ const result = translator.translate('on click', {
186
+ from: 'en',
187
+ to: 'en',
188
+ });
189
+
190
+ expect(result).toBe('on click');
191
+ });
192
+
193
+ it('should handle missing dictionary gracefully', () => {
194
+ expect(() => {
195
+ translator.translate('on click', {
196
+ from: 'en',
197
+ to: 'nonexistent',
198
+ });
199
+ }).toThrow();
200
+ });
201
+
202
+ it('should handle empty input', () => {
203
+ const result = translator.translate('', {
204
+ from: 'en',
205
+ to: 'fr',
206
+ });
207
+
208
+ expect(result).toBe('');
209
+ });
210
+
211
+ it('should handle complex hyperscript expressions', () => {
212
+ const input = 'on click if #myInput.value then set .result to true else set .result to false';
213
+ const result = translator.translate(input, {
214
+ from: 'en',
215
+ to: 'fr',
216
+ });
217
+
218
+ // Should translate keywords but preserve selectors and structure
219
+ expect(result).toContain('sur');
220
+ expect(result).toContain('clic');
221
+ expect(result).toContain('définir');
222
+ expect(result).toContain('#myInput.value');
223
+ expect(result).toContain('.result');
224
+ });
225
+ });
226
+
227
+ describe('whitespace and formatting preservation', () => {
228
+ it('should preserve whitespace', () => {
229
+ const result = translator.translate('on click set #value', {
230
+ from: 'en',
231
+ to: 'fr',
232
+ });
233
+
234
+ expect(result).toBe('sur clic définir #value');
235
+ });
236
+
237
+ it('should preserve line breaks', () => {
238
+ const input = `on click
239
+ set #value
240
+ to true`;
241
+
242
+ const result = translator.translate(input, {
243
+ from: 'en',
244
+ to: 'fr',
245
+ });
246
+
247
+ expect(result).toContain('\n');
248
+ expect(result).toContain('sur');
249
+ expect(result).toContain('définir');
250
+ });
251
+ });
252
+ });