@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
package/src/runtime.ts ADDED
@@ -0,0 +1,441 @@
1
+ // packages/i18n/src/runtime.ts
2
+
3
+ import { HyperscriptTranslator } from './translator';
4
+ import { Dictionary, I18nConfig, TranslationOptions } from './types';
5
+ import { getBrowserLocales, isRTL } from './utils/locale';
6
+ import { getFormatter } from './formatting';
7
+
8
+ /**
9
+ * Runtime i18n manager for client-side locale switching
10
+ */
11
+ export interface RuntimeI18nOptions extends I18nConfig {
12
+ autoDetect?: boolean;
13
+ storageKey?: string;
14
+ urlParam?: string;
15
+ cookieName?: string;
16
+ updateURL?: boolean;
17
+ updateTitle?: boolean;
18
+ updateLang?: boolean;
19
+ updateDir?: boolean;
20
+ }
21
+
22
+ export class RuntimeI18nManager {
23
+ private translator: HyperscriptTranslator;
24
+ private options: Required<RuntimeI18nOptions>;
25
+ private currentLocale: string;
26
+ private observers: Set<(locale: string) => void> = new Set();
27
+ private initialized = false;
28
+
29
+ constructor(options: Partial<RuntimeI18nOptions> = {}) {
30
+ this.options = {
31
+ locale: options.locale || 'en',
32
+ fallbackLocale: options.fallbackLocale || 'en',
33
+ dictionaries: options.dictionaries || {},
34
+ detectLocale: options.detectLocale ?? true,
35
+ rtlLocales: options.rtlLocales || ['ar', 'he', 'fa', 'ur'],
36
+ preserveOriginalAttribute: options.preserveOriginalAttribute || 'data-i18n-original',
37
+ autoDetect: options.autoDetect ?? true,
38
+ storageKey: options.storageKey || 'hyperfixi-locale',
39
+ urlParam: options.urlParam || 'lang',
40
+ cookieName: options.cookieName || 'hyperfixi-locale',
41
+ updateURL: options.updateURL ?? false,
42
+ updateTitle: options.updateTitle ?? true,
43
+ updateLang: options.updateLang ?? true,
44
+ updateDir: options.updateDir ?? true,
45
+ };
46
+
47
+ this.translator = new HyperscriptTranslator(this.options);
48
+ this.currentLocale = this.options.locale;
49
+ }
50
+
51
+ /**
52
+ * Initialize the runtime i18n system
53
+ */
54
+ async initialize(): Promise<void> {
55
+ if (this.initialized) return;
56
+
57
+ if (typeof window !== 'undefined') {
58
+ // Detect initial locale
59
+ const detectedLocale = this.detectInitialLocale();
60
+ if (detectedLocale !== this.currentLocale) {
61
+ await this.setLocale(detectedLocale, false);
62
+ }
63
+
64
+ // Set up event listeners
65
+ this.setupEventListeners();
66
+
67
+ // Translate existing elements
68
+ this.translatePage();
69
+
70
+ // Update document attributes
71
+ this.updateDocumentAttributes();
72
+ }
73
+
74
+ this.initialized = true;
75
+ }
76
+
77
+ /**
78
+ * Set the current locale
79
+ */
80
+ async setLocale(locale: string, updateStorage = true): Promise<void> {
81
+ if (locale === this.currentLocale) return;
82
+
83
+ const oldLocale = this.currentLocale;
84
+ this.currentLocale = locale;
85
+
86
+ // Update storage if requested
87
+ if (updateStorage && typeof window !== 'undefined') {
88
+ this.saveLocaleToStorage(locale);
89
+ }
90
+
91
+ // Update URL if requested
92
+ if (this.options.updateURL && typeof window !== 'undefined') {
93
+ this.updateURL(locale);
94
+ }
95
+
96
+ // Update document attributes
97
+ if (typeof document !== 'undefined') {
98
+ this.updateDocumentAttributes();
99
+ }
100
+
101
+ // Translate the page
102
+ if (typeof document !== 'undefined') {
103
+ this.translatePage(oldLocale);
104
+ }
105
+
106
+ // Notify observers
107
+ this.notifyObservers(locale);
108
+ }
109
+
110
+ /**
111
+ * Get the current locale
112
+ */
113
+ getLocale(): string {
114
+ return this.currentLocale;
115
+ }
116
+
117
+ /**
118
+ * Get supported locales
119
+ */
120
+ getSupportedLocales(): string[] {
121
+ return this.translator.getSupportedLocales();
122
+ }
123
+
124
+ /**
125
+ * Check if a locale is RTL
126
+ */
127
+ isRTL(locale?: string): boolean {
128
+ return isRTL(locale || this.currentLocale);
129
+ }
130
+
131
+ /**
132
+ * Translate a hyperscript string
133
+ */
134
+ translate(text: string, options: Partial<TranslationOptions> = {}): string {
135
+ return this.translator.translate(text, {
136
+ from: options.from || 'en',
137
+ to: options.to || this.currentLocale,
138
+ ...options,
139
+ });
140
+ }
141
+
142
+ /**
143
+ * Format a value according to current locale
144
+ */
145
+ format(value: any, type?: string): string {
146
+ return getFormatter(this.currentLocale).formatHyperscriptValue(value, type);
147
+ }
148
+
149
+ /**
150
+ * Add dictionary for a locale
151
+ */
152
+ addDictionary(locale: string, dictionary: Dictionary): void {
153
+ this.translator.addDictionary(locale, dictionary);
154
+ }
155
+
156
+ /**
157
+ * Subscribe to locale changes
158
+ */
159
+ onLocaleChange(callback: (locale: string) => void): () => void {
160
+ this.observers.add(callback);
161
+ return () => this.observers.delete(callback);
162
+ }
163
+
164
+ /**
165
+ * Create a locale switcher element
166
+ */
167
+ createLocaleSwitcher(
168
+ options: {
169
+ type?: 'dropdown' | 'buttons';
170
+ className?: string;
171
+ showNativeNames?: boolean;
172
+ } = {}
173
+ ): HTMLElement {
174
+ const { type = 'dropdown', className = 'locale-switcher', showNativeNames = true } = options;
175
+ const supportedLocales = this.getSupportedLocales();
176
+
177
+ if (type === 'dropdown') {
178
+ const select = document.createElement('select');
179
+ select.className = className;
180
+
181
+ supportedLocales.forEach(locale => {
182
+ const option = document.createElement('option');
183
+ option.value = locale;
184
+ option.textContent = this.getLocaleDisplayName(locale, showNativeNames);
185
+ option.selected = locale === this.currentLocale;
186
+ select.appendChild(option);
187
+ });
188
+
189
+ select.addEventListener('change', e => {
190
+ const target = e.target as HTMLSelectElement;
191
+ this.setLocale(target.value);
192
+ });
193
+
194
+ return select;
195
+ } else {
196
+ const container = document.createElement('div');
197
+ container.className = className;
198
+
199
+ supportedLocales.forEach(locale => {
200
+ const button = document.createElement('button');
201
+ button.textContent = this.getLocaleDisplayName(locale, showNativeNames);
202
+ button.dataset.locale = locale;
203
+ button.className = locale === this.currentLocale ? 'active' : '';
204
+
205
+ button.addEventListener('click', () => {
206
+ this.setLocale(locale);
207
+ });
208
+
209
+ container.appendChild(button);
210
+ });
211
+
212
+ return container;
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Detect initial locale from various sources
218
+ */
219
+ private detectInitialLocale(): string {
220
+ if (!this.options.autoDetect || typeof window === 'undefined') {
221
+ return this.options.locale;
222
+ }
223
+
224
+ // 1. URL parameter
225
+ const urlParams = new URLSearchParams(window.location.search);
226
+ const urlLocale = urlParams.get(this.options.urlParam);
227
+ if (urlLocale && this.translator.getSupportedLocales().includes(urlLocale)) {
228
+ return urlLocale;
229
+ }
230
+
231
+ // 2. Local storage
232
+ const storageLocale = localStorage.getItem(this.options.storageKey);
233
+ if (storageLocale && this.translator.getSupportedLocales().includes(storageLocale)) {
234
+ return storageLocale;
235
+ }
236
+
237
+ // 3. Cookie
238
+ const cookieLocale = this.getCookie(this.options.cookieName);
239
+ if (cookieLocale && this.translator.getSupportedLocales().includes(cookieLocale)) {
240
+ return cookieLocale;
241
+ }
242
+
243
+ // 4. Browser languages
244
+ const browserLocales = getBrowserLocales();
245
+ for (const locale of browserLocales) {
246
+ if (this.translator.getSupportedLocales().includes(locale)) {
247
+ return locale;
248
+ }
249
+ // Try language-only match
250
+ const lang = locale.split('-')[0];
251
+ if (this.translator.getSupportedLocales().includes(lang)) {
252
+ return lang;
253
+ }
254
+ }
255
+
256
+ return this.options.fallbackLocale;
257
+ }
258
+
259
+ /**
260
+ * Save locale to storage
261
+ */
262
+ private saveLocaleToStorage(locale: string): void {
263
+ try {
264
+ localStorage.setItem(this.options.storageKey, locale);
265
+ this.setCookie(this.options.cookieName, locale, 365);
266
+ } catch (error) {
267
+ console.warn('Failed to save locale to storage:', error);
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Update URL with current locale
273
+ */
274
+ private updateURL(locale: string): void {
275
+ const url = new URL(window.location.href);
276
+ url.searchParams.set(this.options.urlParam, locale);
277
+ window.history.replaceState({}, '', url.toString());
278
+ }
279
+
280
+ /**
281
+ * Update document attributes
282
+ */
283
+ private updateDocumentAttributes(): void {
284
+ if (typeof document === 'undefined') return;
285
+
286
+ if (this.options.updateLang) {
287
+ document.documentElement.lang = this.currentLocale;
288
+ }
289
+
290
+ if (this.options.updateDir) {
291
+ document.documentElement.dir = this.isRTL() ? 'rtl' : 'ltr';
292
+ }
293
+
294
+ if (this.options.updateTitle && document.title) {
295
+ // Optionally translate the title if it contains hyperscript-like content
296
+ // This is a simple implementation - could be enhanced
297
+ document.title = this.translate(document.title);
298
+ }
299
+ }
300
+
301
+ /**
302
+ * Translate the entire page
303
+ */
304
+ private translatePage(fromLocale?: string): void {
305
+ if (typeof document === 'undefined') return;
306
+
307
+ // Find all elements with hyperscript attributes
308
+ const attributes = ['_', 'data-script', 'script'];
309
+
310
+ attributes.forEach(attr => {
311
+ const elements = document.querySelectorAll(`[${attr}]`);
312
+ elements.forEach(element => {
313
+ const original = element.getAttribute(attr);
314
+ if (!original) return;
315
+
316
+ try {
317
+ const translated = this.translator.translate(original, {
318
+ from: fromLocale || 'en',
319
+ to: this.currentLocale,
320
+ });
321
+
322
+ if (translated !== original) {
323
+ // Preserve original if configured
324
+ if (this.options.preserveOriginalAttribute) {
325
+ element.setAttribute(this.options.preserveOriginalAttribute, original);
326
+ }
327
+
328
+ element.setAttribute(attr, translated);
329
+ }
330
+ } catch (error) {
331
+ console.warn(`Failed to translate ${attr} attribute:`, error);
332
+ }
333
+ });
334
+ });
335
+ }
336
+
337
+ /**
338
+ * Set up event listeners
339
+ */
340
+ private setupEventListeners(): void {
341
+ // Listen for language change events
342
+ window.addEventListener('languagechange', () => {
343
+ if (this.options.autoDetect) {
344
+ const newLocale = this.detectInitialLocale();
345
+ if (newLocale !== this.currentLocale) {
346
+ this.setLocale(newLocale);
347
+ }
348
+ }
349
+ });
350
+
351
+ // Listen for popstate events (back/forward navigation)
352
+ window.addEventListener('popstate', () => {
353
+ if (this.options.updateURL) {
354
+ const urlParams = new URLSearchParams(window.location.search);
355
+ const urlLocale = urlParams.get(this.options.urlParam);
356
+ if (urlLocale && urlLocale !== this.currentLocale) {
357
+ this.setLocale(urlLocale, false);
358
+ }
359
+ }
360
+ });
361
+ }
362
+
363
+ /**
364
+ * Notify observers of locale change
365
+ */
366
+ private notifyObservers(locale: string): void {
367
+ this.observers.forEach(callback => {
368
+ try {
369
+ callback(locale);
370
+ } catch (error) {
371
+ console.warn('Locale change observer error:', error);
372
+ }
373
+ });
374
+ }
375
+
376
+ /**
377
+ * Get locale display name
378
+ */
379
+ private getLocaleDisplayName(locale: string, showNative: boolean): string {
380
+ const names: Record<string, { english: string; native: string }> = {
381
+ en: { english: 'English', native: 'English' },
382
+ es: { english: 'Spanish', native: 'Español' },
383
+ fr: { english: 'French', native: 'Français' },
384
+ de: { english: 'German', native: 'Deutsch' },
385
+ ja: { english: 'Japanese', native: '日本語' },
386
+ ko: { english: 'Korean', native: '한국어' },
387
+ zh: { english: 'Chinese', native: '中文' },
388
+ ar: { english: 'Arabic', native: 'العربية' },
389
+ };
390
+
391
+ const info = names[locale];
392
+ if (!info) return locale;
393
+
394
+ return showNative ? info.native : info.english;
395
+ }
396
+
397
+ /**
398
+ * Cookie utilities
399
+ */
400
+ private getCookie(name: string): string | null {
401
+ if (typeof document === 'undefined') return null;
402
+
403
+ const value = `; ${document.cookie}`;
404
+ const parts = value.split(`; ${name}=`);
405
+ if (parts.length === 2) {
406
+ return parts.pop()?.split(';').shift() || null;
407
+ }
408
+ return null;
409
+ }
410
+
411
+ private setCookie(name: string, value: string, days: number): void {
412
+ if (typeof document === 'undefined') return;
413
+
414
+ const expires = new Date();
415
+ expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000);
416
+ document.cookie = `${name}=${value};expires=${expires.toUTCString()};path=/`;
417
+ }
418
+ }
419
+
420
+ /**
421
+ * Global runtime instance
422
+ */
423
+ export let runtimeI18n: RuntimeI18nManager | null = null;
424
+
425
+ /**
426
+ * Initialize global runtime i18n
427
+ */
428
+ export function initializeI18n(options?: RuntimeI18nOptions): RuntimeI18nManager {
429
+ runtimeI18n = new RuntimeI18nManager(options);
430
+ return runtimeI18n;
431
+ }
432
+
433
+ /**
434
+ * Get global runtime i18n instance
435
+ */
436
+ export function getI18n(): RuntimeI18nManager {
437
+ if (!runtimeI18n) {
438
+ throw new Error('I18n not initialized. Call initializeI18n() first.');
439
+ }
440
+ return runtimeI18n;
441
+ }
@@ -0,0 +1,225 @@
1
+ // packages/i18n/src/ssr-integration.ts
2
+
3
+ import { HyperscriptTranslator } from './translator';
4
+ import type { TranslationOptions } from './types';
5
+
6
+ /**
7
+ * SSR integration for i18n support
8
+ */
9
+ export interface SSRLocaleContext {
10
+ locale: string;
11
+ direction: 'ltr' | 'rtl';
12
+ preferredLocales: string[];
13
+ userAgent?: string;
14
+ acceptLanguage?: string;
15
+ }
16
+
17
+ export interface SSRLocaleOptions {
18
+ detectFromHeaders?: boolean;
19
+ detectFromUrl?: boolean;
20
+ fallbackLocale?: string;
21
+ supportedLocales?: string[];
22
+ urlPattern?: RegExp;
23
+ }
24
+
25
+ export class SSRLocaleManager {
26
+ private translator: HyperscriptTranslator;
27
+ private options: Required<SSRLocaleOptions>;
28
+
29
+ constructor(translator: HyperscriptTranslator, options: SSRLocaleOptions = {}) {
30
+ this.translator = translator;
31
+ this.options = {
32
+ detectFromHeaders: options.detectFromHeaders ?? true,
33
+ detectFromUrl: options.detectFromUrl ?? true,
34
+ fallbackLocale: options.fallbackLocale ?? 'en',
35
+ supportedLocales: options.supportedLocales ?? translator.getSupportedLocales(),
36
+ urlPattern: options.urlPattern ?? /^\/([a-z]{2}(-[A-Z]{2})?)(\/|$)/,
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Extract locale from SSR request
42
+ */
43
+ extractLocale(request: {
44
+ url?: string;
45
+ headers?: Record<string, string>;
46
+ userAgent?: string;
47
+ }): SSRLocaleContext {
48
+ let locale = this.options.fallbackLocale;
49
+ const preferredLocales: string[] = [];
50
+
51
+ // Extract from URL first (highest priority)
52
+ if (this.options.detectFromUrl && request.url) {
53
+ const urlLocale = this.extractLocaleFromUrl(request.url);
54
+ if (urlLocale && this.options.supportedLocales.includes(urlLocale)) {
55
+ locale = urlLocale;
56
+ }
57
+ }
58
+
59
+ // Extract from Accept-Language header
60
+ if (this.options.detectFromHeaders && request.headers?.['accept-language']) {
61
+ const headerLocales = this.parseAcceptLanguage(request.headers['accept-language']);
62
+ preferredLocales.push(...headerLocales);
63
+
64
+ // Use first supported locale from header if URL didn't provide one
65
+ if (locale === this.options.fallbackLocale) {
66
+ const supportedHeaderLocale = headerLocales.find(loc =>
67
+ this.options.supportedLocales.includes(loc)
68
+ );
69
+ if (supportedHeaderLocale) {
70
+ locale = supportedHeaderLocale;
71
+ }
72
+ }
73
+ }
74
+
75
+ const context: SSRLocaleContext = {
76
+ locale,
77
+ direction: this.translator.isRTL(locale) ? 'rtl' : 'ltr',
78
+ preferredLocales,
79
+ };
80
+ if (request.userAgent) {
81
+ context.userAgent = request.userAgent;
82
+ }
83
+ if (request.headers?.['accept-language']) {
84
+ context.acceptLanguage = request.headers['accept-language'];
85
+ }
86
+ return context;
87
+ }
88
+
89
+ /**
90
+ * Generate HTML lang and dir attributes
91
+ */
92
+ generateHtmlAttributes(context: SSRLocaleContext): string {
93
+ return `lang="${context.locale}" dir="${context.direction}"`;
94
+ }
95
+
96
+ /**
97
+ * Generate meta tags for SEO
98
+ */
99
+ generateMetaTags(context: SSRLocaleContext, alternateUrls?: Record<string, string>): string[] {
100
+ const tags: string[] = [];
101
+
102
+ // Content language
103
+ tags.push(`<meta http-equiv="content-language" content="${context.locale}" />`);
104
+
105
+ // Alternate languages for SEO
106
+ if (alternateUrls) {
107
+ Object.entries(alternateUrls).forEach(([locale, url]) => {
108
+ tags.push(`<link rel="alternate" hreflang="${locale}" href="${url}" />`);
109
+ });
110
+ }
111
+
112
+ return tags;
113
+ }
114
+
115
+ /**
116
+ * Translate hyperscript code for SSR
117
+ */
118
+ translateForSSR(
119
+ hyperscriptCode: string,
120
+ targetLocale: string,
121
+ options: Partial<Omit<TranslationOptions, 'to'>> = {}
122
+ ): string {
123
+ return this.translator.translate(hyperscriptCode, {
124
+ from: 'en',
125
+ ...options,
126
+ to: targetLocale,
127
+ });
128
+ }
129
+
130
+ /**
131
+ * Generate client-side hydration data
132
+ */
133
+ generateHydrationData(context: SSRLocaleContext): object {
134
+ return {
135
+ __HYPERFIXI_I18N__: {
136
+ locale: context.locale,
137
+ direction: context.direction,
138
+ preferredLocales: context.preferredLocales,
139
+ supportedLocales: this.options.supportedLocales,
140
+ fallbackLocale: this.options.fallbackLocale,
141
+ },
142
+ };
143
+ }
144
+
145
+ private extractLocaleFromUrl(url: string): string | null {
146
+ const match = url.match(this.options.urlPattern);
147
+ return match ? match[1] : null;
148
+ }
149
+
150
+ private parseAcceptLanguage(acceptLanguage: string): string[] {
151
+ return acceptLanguage
152
+ .split(',')
153
+ .map(lang => {
154
+ const [locale, q] = lang.trim().split(';q=');
155
+ return {
156
+ locale: locale.trim(),
157
+ quality: q ? parseFloat(q) : 1.0,
158
+ };
159
+ })
160
+ .sort((a, b) => b.quality - a.quality)
161
+ .map(item => item.locale);
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Express middleware for SSR i18n
167
+ */
168
+ export function createExpressI18nMiddleware(
169
+ translator: HyperscriptTranslator,
170
+ options?: SSRLocaleOptions
171
+ ) {
172
+ const localeManager = new SSRLocaleManager(translator, options);
173
+
174
+ return (req: any, _res: any, next: any) => {
175
+ const localeContext = localeManager.extractLocale({
176
+ url: req.originalUrl || req.url,
177
+ headers: req.headers,
178
+ userAgent: req.get('User-Agent'),
179
+ });
180
+
181
+ // Add to request for later use
182
+ req.localeContext = localeContext;
183
+ req.i18n = {
184
+ translate: (code: string, targetLocale?: string) =>
185
+ localeManager.translateForSSR(code, targetLocale || localeContext.locale),
186
+ generateHtmlAttributes: () => localeManager.generateHtmlAttributes(localeContext),
187
+ generateMetaTags: (alternateUrls?: Record<string, string>) =>
188
+ localeManager.generateMetaTags(localeContext, alternateUrls),
189
+ generateHydrationData: () => localeManager.generateHydrationData(localeContext),
190
+ };
191
+
192
+ next();
193
+ };
194
+ }
195
+
196
+ /**
197
+ * Next.js API for SSR i18n
198
+ */
199
+ export function withI18n(
200
+ handler: any,
201
+ translator: HyperscriptTranslator,
202
+ options?: SSRLocaleOptions
203
+ ) {
204
+ const localeManager = new SSRLocaleManager(translator, options);
205
+
206
+ return async (req: any, res: any) => {
207
+ const localeContext = localeManager.extractLocale({
208
+ url: req.url,
209
+ headers: req.headers,
210
+ userAgent: req.headers['user-agent'],
211
+ });
212
+
213
+ req.localeContext = localeContext;
214
+ req.i18n = {
215
+ translate: (code: string, targetLocale?: string) =>
216
+ localeManager.translateForSSR(code, targetLocale || localeContext.locale),
217
+ generateHtmlAttributes: () => localeManager.generateHtmlAttributes(localeContext),
218
+ generateMetaTags: (alternateUrls?: Record<string, string>) =>
219
+ localeManager.generateMetaTags(localeContext, alternateUrls),
220
+ generateHydrationData: () => localeManager.generateHydrationData(localeContext),
221
+ };
222
+
223
+ return handler(req, res);
224
+ };
225
+ }