pulse-js-framework 1.9.3 → 1.10.1

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 (46) hide show
  1. package/compiler/parser/_extract.js +393 -0
  2. package/compiler/parser/blocks.js +361 -0
  3. package/compiler/parser/core.js +306 -0
  4. package/compiler/parser/expressions.js +386 -0
  5. package/compiler/parser/imports.js +108 -0
  6. package/compiler/parser/index.js +47 -0
  7. package/compiler/parser/state.js +155 -0
  8. package/compiler/parser/style.js +445 -0
  9. package/compiler/parser/view.js +632 -0
  10. package/compiler/parser.js +15 -2372
  11. package/compiler/parser.js.original +2376 -0
  12. package/package.json +29 -1
  13. package/runtime/a11y/announcements.js +213 -0
  14. package/runtime/a11y/contrast.js +125 -0
  15. package/runtime/a11y/focus.js +412 -0
  16. package/runtime/a11y/index.js +35 -0
  17. package/runtime/a11y/preferences.js +121 -0
  18. package/runtime/a11y/utils.js +164 -0
  19. package/runtime/a11y/validation.js +258 -0
  20. package/runtime/a11y/widgets.js +545 -0
  21. package/runtime/a11y.js +15 -1840
  22. package/runtime/a11y.js.original +1844 -0
  23. package/runtime/animation.js +535 -0
  24. package/runtime/dom-advanced.js +116 -37
  25. package/runtime/graphql/cache.js +69 -0
  26. package/runtime/graphql/client.js +563 -0
  27. package/runtime/graphql/hooks.js +492 -0
  28. package/runtime/graphql/index.js +62 -0
  29. package/runtime/graphql/subscriptions.js +241 -0
  30. package/runtime/graphql.js +12 -1322
  31. package/runtime/graphql.js.original +1326 -0
  32. package/runtime/i18n.js +434 -0
  33. package/runtime/index.js +20 -0
  34. package/runtime/logger.js +5 -1
  35. package/runtime/persistence.js +492 -0
  36. package/runtime/router/core.js +956 -0
  37. package/runtime/router/guards.js +90 -0
  38. package/runtime/router/history.js +204 -0
  39. package/runtime/router/index.js +36 -0
  40. package/runtime/router/lazy.js +180 -0
  41. package/runtime/router/utils.js +226 -0
  42. package/runtime/router.js +12 -1600
  43. package/runtime/router.js.original +1605 -0
  44. package/runtime/sse.js +393 -0
  45. package/runtime/sw.js +250 -0
  46. package/sw/index.js +240 -0
@@ -0,0 +1,434 @@
1
+ /**
2
+ * Pulse i18n Module
3
+ * Internationalization with reactive locale switching, pluralization, and interpolation.
4
+ *
5
+ * @module pulse-js-framework/runtime/i18n
6
+ */
7
+
8
+ import { pulse, computed } from './pulse.js';
9
+ import { loggers } from './logger.js';
10
+ import { RuntimeError } from './errors.js';
11
+ import { DANGEROUS_KEYS } from './security.js';
12
+
13
+ const log = loggers.pulse;
14
+
15
+ // =============================================================================
16
+ // CONSTANTS
17
+ // =============================================================================
18
+
19
+ const DEFAULT_OPTIONS = {
20
+ locale: 'en',
21
+ fallbackLocale: 'en',
22
+ messages: {},
23
+ pluralRules: null,
24
+ missing: null,
25
+ modifiers: null,
26
+ };
27
+
28
+ /**
29
+ * Default English pluralization: 0 = zero, 1 = one, 2+ = other
30
+ */
31
+ const DEFAULT_PLURAL_RULES = {
32
+ en: (count) => {
33
+ if (count === 0) return 0;
34
+ if (count === 1) return 1;
35
+ return 2;
36
+ },
37
+ };
38
+
39
+ // =============================================================================
40
+ // MODULE-LEVEL DEFAULT INSTANCE
41
+ // =============================================================================
42
+
43
+ let _defaultInstance = null;
44
+
45
+ // =============================================================================
46
+ // I18N ERROR
47
+ // =============================================================================
48
+
49
+ export class I18nError extends RuntimeError {
50
+ constructor(message, options = {}) {
51
+ super(message, { code: 'I18N_ERROR', ...options });
52
+ this.name = 'I18nError';
53
+ }
54
+
55
+ static isI18nError(error) {
56
+ return error instanceof I18nError;
57
+ }
58
+ }
59
+
60
+ // =============================================================================
61
+ // INTERNAL HELPERS
62
+ // =============================================================================
63
+
64
+ /**
65
+ * Resolve a dot-notated key in a nested object
66
+ * @private
67
+ */
68
+ function _resolveKey(messages, key) {
69
+ const parts = key.split('.');
70
+ let current = messages;
71
+
72
+ for (const part of parts) {
73
+ if (current === null || current === undefined || typeof current !== 'object') {
74
+ return undefined;
75
+ }
76
+ current = current[part];
77
+ }
78
+
79
+ return current;
80
+ }
81
+
82
+ /**
83
+ * Interpolate {param} placeholders in a message string
84
+ * @private
85
+ */
86
+ function _interpolate(message, params) {
87
+ if (!params || typeof message !== 'string') return message;
88
+
89
+ return message.replace(/\{(\w+)\}/g, (match, key) => {
90
+ if (key in params) {
91
+ return String(params[key]);
92
+ }
93
+ return match;
94
+ });
95
+ }
96
+
97
+ /**
98
+ * Apply modifiers (pipe syntax): 'key | upper'
99
+ * @private
100
+ */
101
+ function _applyModifiers(value, modifierStr, modifiers) {
102
+ if (!modifiers || !modifierStr) return value;
103
+
104
+ const parts = modifierStr.split('|').map(s => s.trim());
105
+ let result = value;
106
+
107
+ for (const mod of parts) {
108
+ if (mod && modifiers[mod]) {
109
+ result = modifiers[mod](result);
110
+ }
111
+ }
112
+
113
+ return result;
114
+ }
115
+
116
+ /**
117
+ * Parse key for modifiers: 'hello | upper' -> { key: 'hello', modifiers: 'upper' }
118
+ * @private
119
+ */
120
+ function _parseKey(key) {
121
+ const pipeIdx = key.indexOf(' | ');
122
+ if (pipeIdx === -1) {
123
+ return { key: key.trim(), modifierStr: null };
124
+ }
125
+ return {
126
+ key: key.substring(0, pipeIdx).trim(),
127
+ modifierStr: key.substring(pipeIdx + 3),
128
+ };
129
+ }
130
+
131
+ /**
132
+ * Get the plural form index for a locale and count
133
+ * @private
134
+ */
135
+ function _getPluralIndex(locale, count, customRules) {
136
+ // Check custom rules first
137
+ if (customRules && customRules[locale]) {
138
+ return customRules[locale](count);
139
+ }
140
+
141
+ // Check default rules
142
+ const lang = locale.split('-')[0];
143
+ if (DEFAULT_PLURAL_RULES[lang]) {
144
+ return DEFAULT_PLURAL_RULES[lang](count);
145
+ }
146
+
147
+ // Fallback: 0 = zero, 1 = one, 2+ = other
148
+ if (count === 0) return 0;
149
+ if (count === 1) return 1;
150
+ return 2;
151
+ }
152
+
153
+ // =============================================================================
154
+ // createI18n
155
+ // =============================================================================
156
+
157
+ /**
158
+ * Create an i18n instance with reactive locale switching
159
+ *
160
+ * @param {Object} [options] - Configuration options
161
+ * @returns {Object} I18n instance
162
+ */
163
+ export function createI18n(options = {}) {
164
+ const config = { ...DEFAULT_OPTIONS, ...options };
165
+
166
+ // Reactive locale
167
+ const locale = pulse(config.locale);
168
+ const messages = { ...config.messages };
169
+
170
+ /**
171
+ * Get available locales
172
+ * @returns {string[]}
173
+ */
174
+ function getAvailableLocales() {
175
+ return Object.keys(messages);
176
+ }
177
+
178
+ /**
179
+ * Translate a key with optional interpolation
180
+ *
181
+ * @param {string} key - Translation key (dot notation supported, pipe modifiers supported)
182
+ * @param {Object} [params] - Interpolation parameters
183
+ * @returns {string} Translated string
184
+ */
185
+ function t(key, params) {
186
+ const { key: resolvedKey, modifierStr } = _parseKey(key);
187
+ const currentLocale = locale.get();
188
+
189
+ // Look up in current locale
190
+ let message = _resolveKey(messages[currentLocale], resolvedKey);
191
+
192
+ // Fallback to fallback locale
193
+ if (message === undefined && config.fallbackLocale && config.fallbackLocale !== currentLocale) {
194
+ message = _resolveKey(messages[config.fallbackLocale], resolvedKey);
195
+ }
196
+
197
+ // Missing key handler
198
+ if (message === undefined) {
199
+ if (config.missing) {
200
+ return config.missing(currentLocale, resolvedKey);
201
+ }
202
+ log.warn(`Missing translation: "${resolvedKey}" for locale "${currentLocale}"`);
203
+ return resolvedKey;
204
+ }
205
+
206
+ // If message is not a string (e.g., nested object), return key
207
+ if (typeof message !== 'string') {
208
+ return resolvedKey;
209
+ }
210
+
211
+ // Interpolate
212
+ let result = _interpolate(message, params);
213
+
214
+ // Apply modifiers
215
+ if (modifierStr) {
216
+ result = _applyModifiers(result, modifierStr, config.modifiers);
217
+ }
218
+
219
+ return result;
220
+ }
221
+
222
+ /**
223
+ * Translate with pluralization
224
+ *
225
+ * @param {string} key - Translation key (message format: 'zero | one | other')
226
+ * @param {number} count - Count for pluralization
227
+ * @param {Object} [params] - Additional interpolation parameters
228
+ * @returns {string} Pluralized translated string
229
+ */
230
+ function tc(key, count, params) {
231
+ const currentLocale = locale.get();
232
+
233
+ let message = _resolveKey(messages[currentLocale], key);
234
+
235
+ if (message === undefined && config.fallbackLocale && config.fallbackLocale !== currentLocale) {
236
+ message = _resolveKey(messages[config.fallbackLocale], key);
237
+ }
238
+
239
+ if (message === undefined) {
240
+ if (config.missing) {
241
+ return config.missing(currentLocale, key);
242
+ }
243
+ log.warn(`Missing translation: "${key}" for locale "${currentLocale}"`);
244
+ return key;
245
+ }
246
+
247
+ if (typeof message !== 'string') return key;
248
+
249
+ // Split by ' | ' for plural forms
250
+ const forms = message.split(' | ').map(s => s.trim());
251
+ const pluralIndex = _getPluralIndex(currentLocale, count, config.pluralRules);
252
+ const selectedForm = forms[Math.min(pluralIndex, forms.length - 1)] || forms[forms.length - 1];
253
+
254
+ // Interpolate with count + additional params
255
+ return _interpolate(selectedForm, { count, ...params });
256
+ }
257
+
258
+ /**
259
+ * Check if a translation key exists
260
+ *
261
+ * @param {string} key - Translation key
262
+ * @param {string} [checkLocale] - Locale to check (defaults to current)
263
+ * @returns {boolean}
264
+ */
265
+ function te(key, checkLocale) {
266
+ const loc = checkLocale || locale.get();
267
+ return _resolveKey(messages[loc], key) !== undefined;
268
+ }
269
+
270
+ /**
271
+ * Get the raw message value (without interpolation)
272
+ *
273
+ * @param {string} key - Translation key
274
+ * @returns {*} Raw message value
275
+ */
276
+ function tm(key) {
277
+ const currentLocale = locale.get();
278
+ let message = _resolveKey(messages[currentLocale], key);
279
+
280
+ if (message === undefined && config.fallbackLocale) {
281
+ message = _resolveKey(messages[config.fallbackLocale], key);
282
+ }
283
+
284
+ return message;
285
+ }
286
+
287
+ /**
288
+ * Change the current locale
289
+ * @param {string} newLocale
290
+ */
291
+ function setLocale(newLocale) {
292
+ if (!messages[newLocale]) {
293
+ log.warn(`Locale "${newLocale}" not loaded. Available: ${getAvailableLocales().join(', ')}`);
294
+ }
295
+ locale.set(newLocale);
296
+ }
297
+
298
+ /**
299
+ * Dynamically load messages for a locale
300
+ *
301
+ * @param {string} loc - Locale code
302
+ * @param {Object} msgs - Message object
303
+ */
304
+ function loadMessages(loc, msgs) {
305
+ if (messages[loc]) {
306
+ // Deep merge
307
+ messages[loc] = _deepMerge(messages[loc], msgs);
308
+ } else {
309
+ messages[loc] = msgs;
310
+ }
311
+ log.info(`Loaded messages for locale "${loc}"`);
312
+ }
313
+
314
+ /**
315
+ * Format a number using Intl.NumberFormat
316
+ *
317
+ * @param {number} value - Number to format
318
+ * @param {Object} [opts] - Intl.NumberFormat options
319
+ * @returns {string}
320
+ */
321
+ function n(value, opts) {
322
+ try {
323
+ return new Intl.NumberFormat(locale.get(), opts).format(value);
324
+ } catch {
325
+ return String(value);
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Format a date using Intl.DateTimeFormat
331
+ *
332
+ * @param {Date|number} value - Date to format
333
+ * @param {Object} [opts] - Intl.DateTimeFormat options
334
+ * @returns {string}
335
+ */
336
+ function d(value, opts) {
337
+ try {
338
+ return new Intl.DateTimeFormat(locale.get(), opts).format(value);
339
+ } catch {
340
+ return String(value);
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Set this instance as the global default for useI18n()
346
+ */
347
+ function install() {
348
+ _defaultInstance = instance;
349
+ }
350
+
351
+ const instance = {
352
+ locale,
353
+ get availableLocales() { return getAvailableLocales(); },
354
+
355
+ t,
356
+ tc,
357
+ te,
358
+ tm,
359
+
360
+ setLocale,
361
+ loadMessages,
362
+ install,
363
+
364
+ n,
365
+ d,
366
+ };
367
+
368
+ return instance;
369
+ }
370
+
371
+ // =============================================================================
372
+ // HOOK: useI18n
373
+ // =============================================================================
374
+
375
+ /**
376
+ * Get the global i18n instance (set via createI18n().install())
377
+ *
378
+ * @returns {Object} { t, tc, locale, setLocale, availableLocales }
379
+ */
380
+ export function useI18n() {
381
+ if (!_defaultInstance) {
382
+ throw new I18nError(
383
+ 'No i18n instance installed. Call createI18n().install() first.',
384
+ { suggestion: 'Create an i18n instance and call install() before using useI18n()' }
385
+ );
386
+ }
387
+
388
+ return {
389
+ t: _defaultInstance.t,
390
+ tc: _defaultInstance.tc,
391
+ te: _defaultInstance.te,
392
+ tm: _defaultInstance.tm,
393
+ locale: _defaultInstance.locale,
394
+ setLocale: _defaultInstance.setLocale,
395
+ availableLocales: _defaultInstance.availableLocales,
396
+ n: _defaultInstance.n,
397
+ d: _defaultInstance.d,
398
+ };
399
+ }
400
+
401
+ // =============================================================================
402
+ // INTERNAL: Deep merge
403
+ // =============================================================================
404
+
405
+ const MAX_MERGE_DEPTH = 10;
406
+
407
+ function _deepMerge(target, source, depth = 0) {
408
+ if (depth > MAX_MERGE_DEPTH) {
409
+ log.warn('Maximum nesting depth exceeded in i18n message merge');
410
+ return target;
411
+ }
412
+
413
+ const result = { ...target };
414
+ for (const [key, value] of Object.entries(source)) {
415
+ if (DANGEROUS_KEYS.has(key)) continue;
416
+ if (value && typeof value === 'object' && !Array.isArray(value) &&
417
+ result[key] && typeof result[key] === 'object' && !Array.isArray(result[key])) {
418
+ result[key] = _deepMerge(result[key], value, depth + 1);
419
+ } else {
420
+ result[key] = value;
421
+ }
422
+ }
423
+ return result;
424
+ }
425
+
426
+ // =============================================================================
427
+ // DEFAULT EXPORT
428
+ // =============================================================================
429
+
430
+ export default {
431
+ createI18n,
432
+ useI18n,
433
+ I18nError,
434
+ };
package/runtime/index.js CHANGED
@@ -60,6 +60,21 @@ export * from './lru-cache.js';
60
60
  // DOM Adapter (for SSR/testing)
61
61
  export * from './dom-adapter.js';
62
62
 
63
+ // SSE (Server-Sent Events)
64
+ export * from './sse.js';
65
+
66
+ // Persistence adapters (IndexedDB, SessionStorage, Memory)
67
+ export * from './persistence.js';
68
+
69
+ // Animation system (Web Animations API)
70
+ export * from './animation.js';
71
+
72
+ // Internationalization (i18n)
73
+ export * from './i18n.js';
74
+
75
+ // Service worker (main thread registration)
76
+ export * from './sw.js';
77
+
63
78
  // Default exports for namespace imports
64
79
  export { default as PulseCore } from './pulse.js';
65
80
  export { default as PulseDOM } from './dom.js';
@@ -75,6 +90,11 @@ export { default as PulseSSR } from './ssr.js';
75
90
  export { default as PulseA11y } from './a11y.js';
76
91
  export { default as PulseNative } from './native.js';
77
92
  export { default as PulseLogger } from './logger.js';
93
+ export { default as PulseSSE } from './sse.js';
94
+ export { default as PulsePersistence } from './persistence.js';
95
+ export { default as PulseAnimation } from './animation.js';
96
+ export { default as PulseI18n } from './i18n.js';
97
+ export { default as PulseSW } from './sw.js';
78
98
 
79
99
  // Note: The following modules are intentionally NOT re-exported here
80
100
  // to enable tree-shaking. Import them directly when needed:
package/runtime/logger.js CHANGED
@@ -382,7 +382,11 @@ export const loggers = {
382
382
  get native() { return isProduction ? noopLogger : createDevLogger('Native', {}); },
383
383
  get hmr() { return isProduction ? noopLogger : createDevLogger('HMR', {}); },
384
384
  get cli() { return isProduction ? noopLogger : createDevLogger('CLI', {}); },
385
- get websocket() { return isProduction ? noopLogger : createDevLogger('WebSocket', {}); }
385
+ get websocket() { return isProduction ? noopLogger : createDevLogger('WebSocket', {}); },
386
+ get sse() { return isProduction ? noopLogger : createDevLogger('SSE', {}); },
387
+ get i18n() { return isProduction ? noopLogger : createDevLogger('I18n', {}); },
388
+ get animation() { return isProduction ? noopLogger : createDevLogger('Animation', {}); },
389
+ get sw() { return isProduction ? noopLogger : createDevLogger('SW', {}); }
386
390
  };
387
391
 
388
392
  export default logger;