@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,297 @@
1
+ // packages/i18n/src/translator.ts
2
+
3
+ import {
4
+ Dictionary,
5
+ DICTIONARY_CATEGORIES,
6
+ I18nConfig,
7
+ TranslationOptions,
8
+ TranslationResult,
9
+ Token,
10
+ TokenType,
11
+ ValidationResult,
12
+ } from './types';
13
+ import { dictionaries } from './dictionaries';
14
+ import { detectLocale } from './utils/locale';
15
+ import { tokenize } from './utils/tokenizer';
16
+ import { validate } from './validators';
17
+
18
+ export class HyperscriptTranslator {
19
+ private config: I18nConfig;
20
+ private dictionaries: Map<string, Dictionary>;
21
+ private reverseDictionaries: Map<string, Map<string, string>>;
22
+
23
+ constructor(config: I18nConfig) {
24
+ this.config = {
25
+ fallbackLocale: 'en',
26
+ preserveOriginalAttribute: 'data-i18n-original',
27
+ detectLocale: true, // Enable language detection by default
28
+ ...config,
29
+ locale: config.locale || 'en', // Ensure locale has a default
30
+ };
31
+
32
+ this.dictionaries = new Map();
33
+ this.reverseDictionaries = new Map();
34
+
35
+ // Load built-in dictionaries
36
+ Object.entries(dictionaries).forEach(([locale, dict]) => {
37
+ this.addDictionary(locale, dict);
38
+ });
39
+
40
+ // Load custom dictionaries
41
+ if (config.dictionaries) {
42
+ Object.entries(config.dictionaries).forEach(([locale, dict]) => {
43
+ this.addDictionary(locale, dict);
44
+ });
45
+ }
46
+ }
47
+
48
+ translate(text: string, options: TranslationOptions): string {
49
+ const result = this.translateWithDetails(text, options);
50
+ return result.translated;
51
+ }
52
+
53
+ translateWithDetails(text: string, options: TranslationOptions): TranslationResult {
54
+ const fromLocale = options.from || this.detectLanguage(text);
55
+ const toLocale = options.to;
56
+
57
+ if (fromLocale === toLocale) {
58
+ return {
59
+ translated: text,
60
+ original: text,
61
+ tokens: [],
62
+ locale: { from: fromLocale, to: toLocale },
63
+ };
64
+ }
65
+
66
+ // Get dictionaries
67
+ const fromDict = this.getDictionary(fromLocale);
68
+ const toDict = this.getDictionary(toLocale);
69
+
70
+ if (!fromDict || !toDict) {
71
+ throw new Error(`Missing dictionary for locale: ${!fromDict ? fromLocale : toLocale}`);
72
+ }
73
+
74
+ // Tokenize the input
75
+ const tokens = tokenize(text, fromLocale);
76
+
77
+ // Translate tokens
78
+ const translatedTokens = this.translateTokens(tokens, fromLocale, toLocale);
79
+
80
+ // Reconstruct the text
81
+ const translated = this.reconstructText(translatedTokens);
82
+
83
+ // Validate target dictionary if requested
84
+ if (options.validate && toDict) {
85
+ const validation = validate(toDict, toLocale);
86
+ if (!validation.valid) {
87
+ console.warn('Translation validation warnings:', validation.warnings);
88
+ }
89
+ }
90
+
91
+ const result: TranslationResult = {
92
+ translated,
93
+ tokens: translatedTokens,
94
+ locale: { from: fromLocale, to: toLocale },
95
+ warnings: [],
96
+ };
97
+ if (options.preserveOriginal) {
98
+ result.original = text;
99
+ }
100
+ return result;
101
+ }
102
+
103
+ private translateTokens(tokens: Token[], fromLocale: string, toLocale: string): Token[] {
104
+ const fromDict = this.getDictionary(fromLocale);
105
+ const toDict = this.getDictionary(toLocale);
106
+ const reverseFromDict = this.getReverseDictionary(fromLocale);
107
+ const emptyDict: Dictionary = {
108
+ commands: {},
109
+ modifiers: {},
110
+ events: {},
111
+ logical: {},
112
+ temporal: {},
113
+ values: {},
114
+ attributes: {},
115
+ expressions: {},
116
+ };
117
+
118
+ return tokens.map(token => {
119
+ let translated = token.value;
120
+
121
+ // Only translate keywords, not identifiers or literals
122
+ if (this.isTranslatableToken(token)) {
123
+ // First, try direct translation from source to target
124
+ if (fromLocale !== 'en' && toLocale !== 'en') {
125
+ // Translate through English as intermediate
126
+ const english = this.findTranslation(token.value, fromDict || emptyDict, reverseFromDict);
127
+ if (english) {
128
+ translated =
129
+ this.findTranslation(english, toDict || emptyDict, new Map()) || token.value;
130
+ }
131
+ } else if (fromLocale === 'en') {
132
+ // Direct translation from English
133
+ translated =
134
+ this.findTranslation(token.value, toDict || emptyDict, new Map()) || token.value;
135
+ } else {
136
+ // Translation to English
137
+ translated =
138
+ this.findTranslation(token.value, fromDict || emptyDict, reverseFromDict) ||
139
+ token.value;
140
+ }
141
+ }
142
+
143
+ return {
144
+ ...token,
145
+ translated,
146
+ };
147
+ });
148
+ }
149
+
150
+ private findTranslation(
151
+ word: string,
152
+ dict: Dictionary,
153
+ reverseDict: Map<string, string>
154
+ ): string | null {
155
+ const lowerWord = word.toLowerCase();
156
+
157
+ // Check if translating FROM this locale using reverse dictionary
158
+ if (reverseDict.size > 0) {
159
+ const english = reverseDict.get(lowerWord);
160
+ if (english) return english;
161
+ }
162
+
163
+ // Check categories in priority order (events before commands to handle 'click' etc.)
164
+ const categoryOrder = [
165
+ 'events',
166
+ 'commands',
167
+ 'expressions',
168
+ 'modifiers',
169
+ 'logical',
170
+ 'temporal',
171
+ 'values',
172
+ 'attributes',
173
+ ];
174
+
175
+ for (const category of categoryOrder) {
176
+ const translations = dict[category as keyof Dictionary];
177
+ if (translations && typeof translations === 'object') {
178
+ for (const [key, value] of Object.entries(translations)) {
179
+ if (key.toLowerCase() === lowerWord) return value;
180
+ }
181
+ }
182
+ }
183
+
184
+ return null;
185
+ }
186
+
187
+ private isTranslatableToken(token: Token): boolean {
188
+ const translatableTypes: TokenType[] = [
189
+ 'command',
190
+ 'modifier',
191
+ 'event',
192
+ 'logical',
193
+ 'temporal',
194
+ 'value',
195
+ 'attribute',
196
+ 'expression',
197
+ ];
198
+ return translatableTypes.includes(token.type);
199
+ }
200
+
201
+ private reconstructText(tokens: Token[]): string {
202
+ return tokens.map(token => token.translated || token.value).join('');
203
+ }
204
+
205
+ detectLanguage(text: string): string {
206
+ if (!this.config.detectLocale) {
207
+ return this.config.locale;
208
+ }
209
+
210
+ return detectLocale(text, Array.from(this.dictionaries.keys()));
211
+ }
212
+
213
+ addDictionary(locale: string, dictionary: Dictionary): void {
214
+ this.dictionaries.set(locale, dictionary);
215
+
216
+ // Build reverse dictionary for this locale using type-safe iteration
217
+ const reverseDict = new Map<string, string>();
218
+
219
+ for (const category of DICTIONARY_CATEGORIES) {
220
+ const translations = dictionary[category];
221
+ if (translations) {
222
+ for (const [english, translated] of Object.entries(translations)) {
223
+ reverseDict.set(translated.toLowerCase(), english);
224
+ }
225
+ }
226
+ }
227
+
228
+ this.reverseDictionaries.set(locale, reverseDict);
229
+ }
230
+
231
+ getDictionary(locale: string): Dictionary | undefined {
232
+ return this.dictionaries.get(locale);
233
+ }
234
+
235
+ private getReverseDictionary(locale: string): Map<string, string> {
236
+ return this.reverseDictionaries.get(locale) || new Map();
237
+ }
238
+
239
+ getSupportedLocales(): string[] {
240
+ return Array.from(this.dictionaries.keys());
241
+ }
242
+
243
+ validateDictionary(locale: string): ValidationResult {
244
+ const dict = this.getDictionary(locale);
245
+ if (!dict) {
246
+ return {
247
+ valid: false,
248
+ errors: [
249
+ { type: 'missing', key: locale, message: `Dictionary not found for locale: ${locale}` },
250
+ ],
251
+ warnings: [],
252
+ coverage: { total: 0, translated: 0, missing: [] },
253
+ };
254
+ }
255
+
256
+ return validate(dict, locale);
257
+ }
258
+
259
+ isRTL(locale: string): boolean {
260
+ const rtlLocales = this.config.rtlLocales || ['ar', 'he', 'fa', 'ur'];
261
+ return rtlLocales.includes(locale);
262
+ }
263
+
264
+ getCompletions(context: { text: string; position: number; locale: string }): string[] {
265
+ const dict = this.getDictionary(context.locale);
266
+ if (!dict) return [];
267
+
268
+ const completions: string[] = [];
269
+
270
+ // Get the partial word at cursor
271
+ const beforeCursor = context.text.substring(0, context.position);
272
+ const partial = beforeCursor.match(/\b(\w+)$/)?.[1] || '';
273
+ const lowerPartial = partial.toLowerCase();
274
+
275
+ // Search all categories for matches using type-safe iteration
276
+ // For non-English locales, search translated values; for English, search keys
277
+ for (const category of DICTIONARY_CATEGORIES) {
278
+ const translations = dict[category];
279
+ if (translations) {
280
+ for (const [key, value] of Object.entries(translations)) {
281
+ // Search keys (English terms) and values (translated terms)
282
+ if (key.toLowerCase().startsWith(lowerPartial)) {
283
+ completions.push(key);
284
+ }
285
+ if (value.toLowerCase().startsWith(lowerPartial) && !completions.includes(value)) {
286
+ completions.push(value);
287
+ }
288
+ }
289
+ }
290
+ }
291
+
292
+ return completions;
293
+ }
294
+ }
295
+
296
+ // Export a singleton instance for convenience
297
+ export const translator = new HyperscriptTranslator({ locale: 'en' });
package/src/types.ts ADDED
@@ -0,0 +1,209 @@
1
+ // packages/i18n/src/types.ts
2
+
3
+ /**
4
+ * Dictionary category names as a union type for type-safe access.
5
+ */
6
+ export type DictionaryCategory =
7
+ | 'commands'
8
+ | 'modifiers'
9
+ | 'events'
10
+ | 'logical'
11
+ | 'temporal'
12
+ | 'values'
13
+ | 'attributes'
14
+ | 'expressions';
15
+
16
+ /**
17
+ * All valid dictionary categories.
18
+ */
19
+ export const DICTIONARY_CATEGORIES: readonly DictionaryCategory[] = [
20
+ 'commands',
21
+ 'modifiers',
22
+ 'events',
23
+ 'logical',
24
+ 'temporal',
25
+ 'values',
26
+ 'attributes',
27
+ 'expressions',
28
+ ] as const;
29
+
30
+ /**
31
+ * Dictionary structure for i18n translations.
32
+ * Maps English canonical keywords to locale-specific translations.
33
+ *
34
+ * Note: Index signature removed for stricter type safety.
35
+ * Use DICTIONARY_CATEGORIES to iterate over categories.
36
+ */
37
+ export interface Dictionary {
38
+ commands: Record<string, string>;
39
+ modifiers: Record<string, string>;
40
+ events: Record<string, string>;
41
+ logical: Record<string, string>;
42
+ temporal: Record<string, string>;
43
+ values: Record<string, string>;
44
+ attributes: Record<string, string>;
45
+ expressions: Record<string, string>;
46
+ }
47
+
48
+ // =============================================================================
49
+ // Type Guards
50
+ // =============================================================================
51
+
52
+ /**
53
+ * Type guard to check if a string is a valid dictionary category.
54
+ */
55
+ export function isDictionaryCategory(key: string): key is DictionaryCategory {
56
+ return DICTIONARY_CATEGORIES.includes(key as DictionaryCategory);
57
+ }
58
+
59
+ /**
60
+ * Safely get a category from a dictionary with type narrowing.
61
+ * Returns undefined if the category doesn't exist.
62
+ */
63
+ export function getDictionaryCategory(
64
+ dict: Dictionary,
65
+ category: string
66
+ ): Record<string, string> | undefined {
67
+ if (isDictionaryCategory(category)) {
68
+ return dict[category];
69
+ }
70
+ return undefined;
71
+ }
72
+
73
+ /**
74
+ * Iterate over all categories in a dictionary with proper typing.
75
+ */
76
+ export function forEachCategory(
77
+ dict: Dictionary,
78
+ callback: (category: DictionaryCategory, entries: Record<string, string>) => void
79
+ ): void {
80
+ for (const category of DICTIONARY_CATEGORIES) {
81
+ callback(category, dict[category]);
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Find a translation in any category of a dictionary.
87
+ * Returns the English key if found, undefined otherwise.
88
+ */
89
+ export function findInDictionary(
90
+ dict: Dictionary,
91
+ localizedWord: string
92
+ ): { category: DictionaryCategory; englishKey: string } | undefined {
93
+ const normalized = localizedWord.toLowerCase();
94
+ for (const category of DICTIONARY_CATEGORIES) {
95
+ const entries = dict[category];
96
+ for (const [english, localized] of Object.entries(entries)) {
97
+ if (localized.toLowerCase() === normalized) {
98
+ return { category, englishKey: english };
99
+ }
100
+ }
101
+ }
102
+ return undefined;
103
+ }
104
+
105
+ /**
106
+ * Find a translation for an English word in any category.
107
+ * Returns the localized word if found, undefined otherwise.
108
+ */
109
+ export function translateFromEnglish(dict: Dictionary, englishWord: string): string | undefined {
110
+ const normalized = englishWord.toLowerCase();
111
+ for (const category of DICTIONARY_CATEGORIES) {
112
+ const entries = dict[category];
113
+ const translated = entries[normalized];
114
+ if (translated) {
115
+ return translated;
116
+ }
117
+ }
118
+ return undefined;
119
+ }
120
+
121
+ export interface I18nConfig {
122
+ locale: string;
123
+ fallbackLocale?: string;
124
+ dictionaries?: Record<string, Dictionary>;
125
+ detectLocale?: boolean;
126
+ rtlLocales?: string[];
127
+ preserveOriginalAttribute?: string;
128
+ }
129
+
130
+ export interface TranslationOptions {
131
+ from?: string;
132
+ to: string;
133
+ preserveOriginal?: boolean;
134
+ validate?: boolean;
135
+ }
136
+
137
+ export interface ValidationResult {
138
+ valid: boolean;
139
+ errors: ValidationError[];
140
+ warnings: ValidationWarning[];
141
+ coverage: {
142
+ total: number;
143
+ translated: number;
144
+ missing: string[];
145
+ };
146
+ }
147
+
148
+ export interface ValidationError {
149
+ type: 'missing' | 'invalid' | 'duplicate';
150
+ key: string;
151
+ message: string;
152
+ }
153
+
154
+ export interface ValidationWarning {
155
+ type: 'unused' | 'deprecated' | 'inconsistent';
156
+ key: string;
157
+ message: string;
158
+ }
159
+
160
+ export interface LocaleMetadata {
161
+ code: string;
162
+ name: string;
163
+ nativeName: string;
164
+ rtl: boolean;
165
+ pluralRules?: (n: number) => string;
166
+ }
167
+
168
+ export interface TranslationContext {
169
+ locale: string;
170
+ direction: 'ltr' | 'rtl';
171
+ dictionary: Dictionary;
172
+ metadata: LocaleMetadata;
173
+ }
174
+
175
+ export type TokenType =
176
+ | 'command'
177
+ | 'modifier'
178
+ | 'event'
179
+ | 'logical'
180
+ | 'temporal'
181
+ | 'value'
182
+ | 'attribute'
183
+ | 'expression'
184
+ | 'identifier'
185
+ | 'operator'
186
+ | 'literal';
187
+
188
+ export interface Token {
189
+ type: TokenType;
190
+ value: string;
191
+ translated?: string;
192
+ position: {
193
+ start: number;
194
+ end: number;
195
+ line: number;
196
+ column: number;
197
+ };
198
+ }
199
+
200
+ export interface TranslationResult {
201
+ translated: string;
202
+ original?: string;
203
+ tokens: Token[];
204
+ locale: {
205
+ from: string;
206
+ to: string;
207
+ };
208
+ warnings?: string[];
209
+ }
@@ -0,0 +1,190 @@
1
+ // packages/i18n/src/utils/locale.ts
2
+
3
+ import { dictionaries } from '../dictionaries';
4
+ import { DICTIONARY_CATEGORIES } from '../types';
5
+
6
+ export interface LocaleInfo {
7
+ code: string;
8
+ language: string;
9
+ region?: string;
10
+ script?: string;
11
+ }
12
+
13
+ /**
14
+ * Parse a locale string into its components
15
+ * Examples: 'en-US', 'zh-Hans-CN', 'es-419'
16
+ */
17
+ export function parseLocale(locale: string): LocaleInfo {
18
+ const parts = locale.split('-');
19
+ const info: LocaleInfo = {
20
+ code: locale,
21
+ language: parts[0].toLowerCase(),
22
+ };
23
+
24
+ if (parts.length > 1) {
25
+ // Check if second part is a script (4 letters, first capitalized)
26
+ if (parts[1].length === 4 && /^[A-Z][a-z]{3}$/.test(parts[1])) {
27
+ info.script = parts[1];
28
+ if (parts[2]) {
29
+ info.region = parts[2].toUpperCase();
30
+ }
31
+ } else {
32
+ info.region = parts[1].toUpperCase();
33
+ }
34
+ }
35
+
36
+ return info;
37
+ }
38
+
39
+ /**
40
+ * Detect the language of hyperscript text
41
+ */
42
+ export function detectLocale(text: string, supportedLocales: string[]): string {
43
+ const scores = new Map<string, number>();
44
+
45
+ // Initialize scores
46
+ supportedLocales.forEach(locale => scores.set(locale, 0));
47
+
48
+ // Count keyword matches for each locale using type-safe iteration
49
+ for (const [locale, dictionary] of Object.entries(dictionaries)) {
50
+ if (!supportedLocales.includes(locale)) continue;
51
+
52
+ let score = 0;
53
+
54
+ // Check all categories
55
+ for (const category of DICTIONARY_CATEGORIES) {
56
+ const translations = dictionary[category];
57
+ for (const keyword of Object.values(translations)) {
58
+ // Skip empty or single-character keywords (too ambiguous for detection)
59
+ if (!keyword || keyword.length < 2) continue;
60
+
61
+ // Use word boundary matching for accuracy
62
+ const regex = new RegExp(`\\b${escapeRegex(keyword)}\\b`, 'gi');
63
+ const matches = text.match(regex);
64
+ if (matches) {
65
+ score += matches.length;
66
+ }
67
+ }
68
+ }
69
+
70
+ scores.set(locale, score);
71
+ }
72
+
73
+ // Find locale with highest score
74
+ let maxScore = 0;
75
+ let detectedLocale = 'en'; // Default to English
76
+
77
+ scores.forEach((score, locale) => {
78
+ if (score > maxScore) {
79
+ maxScore = score;
80
+ detectedLocale = locale;
81
+ }
82
+ });
83
+
84
+ return detectedLocale;
85
+ }
86
+
87
+ /**
88
+ * Get the best matching locale from available locales
89
+ */
90
+ export function getBestMatchingLocale(
91
+ requestedLocale: string,
92
+ availableLocales: string[]
93
+ ): string | null {
94
+ // Exact match
95
+ if (availableLocales.includes(requestedLocale)) {
96
+ return requestedLocale;
97
+ }
98
+
99
+ const requested = parseLocale(requestedLocale);
100
+
101
+ // Try language-only match
102
+ const languageMatch = availableLocales.find(locale => {
103
+ const available = parseLocale(locale);
104
+ return available.language === requested.language;
105
+ });
106
+
107
+ if (languageMatch) {
108
+ return languageMatch;
109
+ }
110
+
111
+ // Try language-script match
112
+ if (requested.script) {
113
+ const scriptMatch = availableLocales.find(locale => {
114
+ const available = parseLocale(locale);
115
+ return available.language === requested.language && available.script === requested.script;
116
+ });
117
+
118
+ if (scriptMatch) {
119
+ return scriptMatch;
120
+ }
121
+ }
122
+
123
+ return null;
124
+ }
125
+
126
+ /**
127
+ * Get user's preferred locales from browser
128
+ */
129
+ export function getBrowserLocales(): string[] {
130
+ if (typeof window === 'undefined') {
131
+ return ['en'];
132
+ }
133
+
134
+ const locales: string[] = [];
135
+
136
+ // Modern browser API
137
+ if (navigator.languages && navigator.languages.length > 0) {
138
+ locales.push(...navigator.languages);
139
+ }
140
+
141
+ // Fallback to single language
142
+ if (navigator.language) {
143
+ locales.push(navigator.language);
144
+ }
145
+
146
+ // Legacy IE
147
+ if ((navigator as any).userLanguage) {
148
+ locales.push((navigator as any).userLanguage);
149
+ }
150
+
151
+ // Remove duplicates and return
152
+ return [...new Set(locales)];
153
+ }
154
+
155
+ /**
156
+ * Escape special regex characters
157
+ */
158
+ function escapeRegex(str: string): string {
159
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
160
+ }
161
+
162
+ /**
163
+ * Format a locale for display
164
+ */
165
+ export function formatLocaleName(locale: string): string {
166
+ const names: Record<string, string> = {
167
+ en: 'English',
168
+ es: 'Español',
169
+ ko: '한국어',
170
+ zh: '中文',
171
+ 'zh-TW': '繁體中文',
172
+ ja: '日本語',
173
+ fr: 'Français',
174
+ de: 'Deutsch',
175
+ pt: 'Português',
176
+ hi: 'हिन्दी',
177
+ ar: 'العربية',
178
+ };
179
+
180
+ return names[locale] || locale;
181
+ }
182
+
183
+ /**
184
+ * Check if a locale uses RTL writing
185
+ */
186
+ export function isRTL(locale: string): boolean {
187
+ const rtlLocales = ['ar', 'he', 'fa', 'ur', 'yi', 'ji', 'ku', 'dv'];
188
+ const info = parseLocale(locale);
189
+ return rtlLocales.includes(info.language);
190
+ }