pulse-js-framework 1.9.3 → 1.10.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/package.json +28 -1
- package/runtime/animation.js +535 -0
- package/runtime/dom-advanced.js +116 -37
- package/runtime/i18n.js +434 -0
- package/runtime/index.js +20 -0
- package/runtime/logger.js +5 -1
- package/runtime/persistence.js +492 -0
- package/runtime/sse.js +393 -0
- package/runtime/sw.js +250 -0
- package/sw/index.js +240 -0
package/runtime/dom-advanced.js
CHANGED
|
@@ -21,63 +21,142 @@ const log = loggers.dom;
|
|
|
21
21
|
*
|
|
22
22
|
* @param {*|Function} children - Children to render (static or reactive)
|
|
23
23
|
* @param {string|HTMLElement} target - Target selector or element
|
|
24
|
-
* @
|
|
24
|
+
* @param {Object} [options] - Portal options
|
|
25
|
+
* @param {string} [options.key] - Unique key for multiple portals to same target
|
|
26
|
+
* @param {boolean} [options.prepend=false] - Insert at beginning of target
|
|
27
|
+
* @param {Function} [options.onMount] - Called when children are mounted
|
|
28
|
+
* @param {Function} [options.onUnmount] - Called when children are unmounted
|
|
29
|
+
* @returns {Comment} Marker node with dispose(), moveTo(), getNodes() methods
|
|
25
30
|
*/
|
|
26
|
-
export function portal(children, target) {
|
|
31
|
+
export function portal(children, target, options = {}) {
|
|
27
32
|
const dom = getAdapter();
|
|
28
|
-
const {
|
|
33
|
+
const { key = null, prepend = false, onMount = null, onUnmount = null } = options;
|
|
34
|
+
|
|
35
|
+
let currentTarget = null;
|
|
36
|
+
let currentSelector = null;
|
|
37
|
+
let mountedNodes = [];
|
|
38
|
+
let disposed = false;
|
|
39
|
+
let disposeEffect = null;
|
|
40
|
+
|
|
41
|
+
// Resolve target
|
|
42
|
+
function _resolveTarget(tgt) {
|
|
43
|
+
const { element: resolvedTarget, selector } = resolveSelector(tgt, 'portal');
|
|
44
|
+
currentTarget = resolvedTarget;
|
|
45
|
+
currentSelector = selector;
|
|
46
|
+
return resolvedTarget;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const resolvedTarget = _resolveTarget(target);
|
|
29
50
|
|
|
30
51
|
if (!resolvedTarget) {
|
|
31
|
-
log.warn(`Portal target not found: "${
|
|
32
|
-
|
|
52
|
+
log.warn(`Portal target not found: "${currentSelector}"`);
|
|
53
|
+
const marker = dom.createComment(key ? `portal:${key}` : 'portal-target-not-found');
|
|
54
|
+
marker.dispose = () => {};
|
|
55
|
+
marker.moveTo = () => {};
|
|
56
|
+
marker.getNodes = () => [];
|
|
57
|
+
return marker;
|
|
33
58
|
}
|
|
34
59
|
|
|
35
|
-
const marker = dom.createComment('portal');
|
|
36
|
-
let mountedNodes = [];
|
|
60
|
+
const marker = dom.createComment(key ? `portal:${key}` : 'portal');
|
|
37
61
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
62
|
+
function _clearNodes() {
|
|
63
|
+
for (const node of mountedNodes) {
|
|
64
|
+
dom.removeNode(node);
|
|
65
|
+
if (node._pulseUnmount) {
|
|
66
|
+
for (const cb of node._pulseUnmount) cb();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (mountedNodes.length > 0) {
|
|
70
|
+
onUnmount?.();
|
|
71
|
+
}
|
|
72
|
+
mountedNodes = [];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function _mountNodes(nodes, tgt) {
|
|
76
|
+
for (const node of nodes) {
|
|
77
|
+
if (dom.isNode(node)) {
|
|
78
|
+
if (prepend && tgt.firstChild) {
|
|
79
|
+
dom.insertBefore(tgt, node, tgt.firstChild);
|
|
80
|
+
} else {
|
|
81
|
+
dom.appendChild(tgt, node);
|
|
46
82
|
}
|
|
83
|
+
mountedNodes.push(node);
|
|
47
84
|
}
|
|
48
|
-
|
|
85
|
+
}
|
|
86
|
+
if (mountedNodes.length > 0) {
|
|
87
|
+
onMount?.();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
49
90
|
|
|
91
|
+
function _renderChildren(tgt) {
|
|
92
|
+
if (typeof children === 'function') {
|
|
50
93
|
const result = children();
|
|
51
94
|
if (result) {
|
|
52
95
|
const nodes = Array.isArray(result) ? result : [result];
|
|
53
|
-
|
|
54
|
-
if (dom.isNode(node)) {
|
|
55
|
-
dom.appendChild(resolvedTarget, node);
|
|
56
|
-
mountedNodes.push(node);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
96
|
+
_mountNodes(nodes, tgt);
|
|
59
97
|
}
|
|
98
|
+
} else {
|
|
99
|
+
const nodes = Array.isArray(children) ? children : [children];
|
|
100
|
+
_mountNodes(nodes, tgt);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Handle reactive children
|
|
105
|
+
if (typeof children === 'function') {
|
|
106
|
+
disposeEffect = effect(() => {
|
|
107
|
+
if (disposed) return;
|
|
108
|
+
_clearNodes();
|
|
109
|
+
_renderChildren(currentTarget);
|
|
60
110
|
});
|
|
61
111
|
} else {
|
|
62
|
-
|
|
63
|
-
const nodes = Array.isArray(children) ? children : [children];
|
|
64
|
-
for (const node of nodes) {
|
|
65
|
-
if (dom.isNode(node)) {
|
|
66
|
-
dom.appendChild(resolvedTarget, node);
|
|
67
|
-
mountedNodes.push(node);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
112
|
+
_renderChildren(currentTarget);
|
|
70
113
|
}
|
|
71
114
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
115
|
+
/**
|
|
116
|
+
* Manually dispose portal (remove all portaled nodes)
|
|
117
|
+
*/
|
|
118
|
+
marker.dispose = function () {
|
|
119
|
+
if (disposed) return;
|
|
120
|
+
disposed = true;
|
|
121
|
+
_clearNodes();
|
|
122
|
+
if (disposeEffect) disposeEffect();
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Move portaled content to a new target
|
|
127
|
+
* @param {string|HTMLElement} newTarget - New target selector or element
|
|
128
|
+
*/
|
|
129
|
+
marker.moveTo = function (newTarget) {
|
|
130
|
+
if (disposed) return;
|
|
131
|
+
|
|
132
|
+
const resolved = _resolveTarget(newTarget);
|
|
133
|
+
if (!resolved) {
|
|
134
|
+
log.warn(`Portal moveTo target not found: "${currentSelector}"`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Re-mount existing nodes in new target
|
|
139
|
+
const existingNodes = [...mountedNodes];
|
|
140
|
+
// Remove from old target without unmount callbacks
|
|
141
|
+
for (const node of existingNodes) {
|
|
75
142
|
dom.removeNode(node);
|
|
76
|
-
if (node._pulseUnmount) {
|
|
77
|
-
for (const cb of node._pulseUnmount) cb();
|
|
78
|
-
}
|
|
79
143
|
}
|
|
80
|
-
|
|
144
|
+
mountedNodes = [];
|
|
145
|
+
|
|
146
|
+
// Re-mount in new target
|
|
147
|
+
_mountNodes(existingNodes, resolved);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get currently portaled nodes
|
|
152
|
+
* @returns {Array} Currently mounted nodes
|
|
153
|
+
*/
|
|
154
|
+
marker.getNodes = function () {
|
|
155
|
+
return [...mountedNodes];
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Attach cleanup for parent effect cascade
|
|
159
|
+
marker._pulseUnmount = [() => marker.dispose()];
|
|
81
160
|
|
|
82
161
|
return marker;
|
|
83
162
|
}
|
package/runtime/i18n.js
ADDED
|
@@ -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;
|