@ahriknow/lux 0.0.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 (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +127 -0
  3. package/README_zh-CN.md +127 -0
  4. package/dist/components/lux-button/index.iife.min.js +292 -0
  5. package/dist/components/lux-button/index.min.js +292 -0
  6. package/dist/components/lux-code/index.iife.min.js +290 -0
  7. package/dist/components/lux-code/index.min.js +290 -0
  8. package/dist/components/lux-dropdown/index.iife.min.js +162 -0
  9. package/dist/components/lux-dropdown/index.min.js +162 -0
  10. package/dist/components/lux-example/index.iife.min.js +88 -0
  11. package/dist/components/lux-example/index.min.js +88 -0
  12. package/dist/components/lux-icon/index.iife.min.js +22 -0
  13. package/dist/components/lux-icon/index.min.js +22 -0
  14. package/dist/components/lux-input/index.iife.min.js +238 -0
  15. package/dist/components/lux-input/index.min.js +238 -0
  16. package/dist/components/lux-layout/index.iife.min.js +90 -0
  17. package/dist/components/lux-layout/index.min.js +90 -0
  18. package/dist/components/lux-menu/index.iife.min.js +193 -0
  19. package/dist/components/lux-menu/index.min.js +193 -0
  20. package/dist/components/lux-scroll/index.iife.min.js +137 -0
  21. package/dist/components/lux-scroll/index.min.js +137 -0
  22. package/dist/components/lux-switch/index.iife.min.js +116 -0
  23. package/dist/components/lux-switch/index.min.js +116 -0
  24. package/dist/components/lux-table/index.iife.min.js +67 -0
  25. package/dist/components/lux-table/index.min.js +67 -0
  26. package/dist/lux.core.min.js +1 -0
  27. package/dist/lux.i18n.min.js +1 -0
  28. package/dist/lux.iife.js +1822 -0
  29. package/dist/lux.iife.js.map +1 -0
  30. package/dist/lux.iife.min.js +1 -0
  31. package/dist/lux.js +1792 -0
  32. package/dist/lux.js.map +1 -0
  33. package/dist/lux.min.js +1 -0
  34. package/dist/lux.router.min.js +1 -0
  35. package/dist/lux.template.min.js +1 -0
  36. package/dist/lux.theme.min.js +1 -0
  37. package/dist/themes/dark.css +130 -0
  38. package/dist/themes/light.css +128 -0
  39. package/package.json +64 -0
  40. package/src/components/lux-button/index.js +319 -0
  41. package/src/components/lux-code/index.js +382 -0
  42. package/src/components/lux-dropdown/index.js +256 -0
  43. package/src/components/lux-example/index.js +117 -0
  44. package/src/components/lux-icon/index.js +180 -0
  45. package/src/components/lux-input/index.js +363 -0
  46. package/src/components/lux-layout/index.js +222 -0
  47. package/src/components/lux-menu/index.js +283 -0
  48. package/src/components/lux-scroll/index.js +349 -0
  49. package/src/components/lux-switch/index.js +203 -0
  50. package/src/components/lux-table/index.js +105 -0
  51. package/src/core.js +7 -0
  52. package/src/element.js +477 -0
  53. package/src/i18n/format.js +108 -0
  54. package/src/i18n/index.js +102 -0
  55. package/src/i18n/locale.js +26 -0
  56. package/src/index.js +22 -0
  57. package/src/router.js +330 -0
  58. package/src/template.js +402 -0
  59. package/src/theme/color.js +148 -0
  60. package/src/theme/create.js +97 -0
  61. package/src/theme/index.js +2 -0
  62. package/src/theme/tokens.js +128 -0
  63. package/src/themes/dark.css +130 -0
  64. package/src/themes/light.css +128 -0
package/src/element.js ADDED
@@ -0,0 +1,477 @@
1
+ /**
2
+ * LuxElement — Base class for Web Components.
3
+ *
4
+ * Inherits: HTMLElement → LuxElement
5
+ * Features: reactive properties, Shadow DOM, lifecycle, controllers, update batching
6
+ */
7
+
8
+ import { html, render, isTemplateResult } from './template.js';
9
+
10
+ // ─── Update scheduling ───
11
+ const pendingUpdates = new Set();
12
+ let updateScheduled = false;
13
+
14
+ function scheduleMicroTask(fn) {
15
+ Promise.resolve().then(fn);
16
+ }
17
+
18
+ function enqueueUpdate(element) {
19
+ pendingUpdates.add(element);
20
+ if (!updateScheduled) {
21
+ updateScheduled = true;
22
+ scheduleMicroTask(() => {
23
+ updateScheduled = false;
24
+ const elements = [...pendingUpdates];
25
+ pendingUpdates.clear();
26
+ for (const el of elements) {
27
+ el._$performUpdate();
28
+ }
29
+ });
30
+ }
31
+ }
32
+
33
+ // ─── Default converter (attribute ↔ property) ───
34
+ const defaultConverter = {
35
+ toAttribute(value, type) {
36
+ switch (type) {
37
+ case Boolean:
38
+ return value ? '' : null;
39
+ case Object:
40
+ case Array:
41
+ return value == null ? null : JSON.stringify(value);
42
+ default:
43
+ return value;
44
+ }
45
+ },
46
+ fromAttribute(value, type) {
47
+ switch (type) {
48
+ case Boolean:
49
+ return value !== null;
50
+ case Number:
51
+ return Number(value);
52
+ case Object:
53
+ case Array:
54
+ try {
55
+ return JSON.parse(value);
56
+ } catch {
57
+ return null;
58
+ }
59
+ default:
60
+ return value;
61
+ }
62
+ },
63
+ };
64
+
65
+ // ─── LuxElement ───
66
+ export class LuxElement extends HTMLElement {
67
+ // ─── Static: properties declaration ───
68
+ static properties = {};
69
+ static styles = undefined;
70
+
71
+ // ─── Static: finalization ───
72
+ static finalized = false;
73
+ static elementProperties = new Map();
74
+
75
+ // ─── Static: __prepare() — subclass isolation ───
76
+ static __prepare() {
77
+ if (this.hasOwnProperty('elementProperties')) {
78
+ return;
79
+ }
80
+ // Finalize any superclasses first
81
+ const superCtor = Object.getPrototypeOf(this);
82
+ if (superCtor.finalize) {
83
+ superCtor.finalize();
84
+ }
85
+ // Copy superclass elementProperties into own Map
86
+ this.elementProperties = new Map(superCtor.elementProperties);
87
+ }
88
+
89
+ static get observedAttributes() {
90
+ this.finalize();
91
+ return this.__attributeToPropertyMap ? [...this.__attributeToPropertyMap.keys()] : [];
92
+ }
93
+
94
+ static finalize() {
95
+ if (this.hasOwnProperty('finalized')) {
96
+ return;
97
+ }
98
+ this.finalized = true;
99
+ this.__prepare();
100
+
101
+ // Create properties from the static properties block
102
+ if (this.hasOwnProperty('properties')) {
103
+ const props = this.properties;
104
+ const propKeys = [
105
+ ...Object.getOwnPropertyNames(props),
106
+ ...Object.getOwnPropertySymbols(props),
107
+ ];
108
+ for (const p of propKeys) {
109
+ this.createProperty(p, props[p]);
110
+ }
111
+ }
112
+
113
+ // Create the attribute-to-property map
114
+ this.__attributeToPropertyMap = new Map();
115
+ for (const [p, options] of this.elementProperties) {
116
+ const attr = this.__attributeNameForProperty(p, options);
117
+ if (attr !== undefined) {
118
+ this.__attributeToPropertyMap.set(attr, p);
119
+ }
120
+ }
121
+
122
+ this.elementStyles = this.styles;
123
+ }
124
+
125
+ static __attributeNameForProperty(name, options) {
126
+ const attribute = options.attribute;
127
+ return attribute === false
128
+ ? undefined
129
+ : typeof attribute === 'string'
130
+ ? attribute
131
+ : typeof name === 'string'
132
+ ? name.replace(/([A-Z])/g, '-$1').toLowerCase()
133
+ : undefined;
134
+ }
135
+
136
+ // ─── Static: createProperty ───
137
+ static createProperty(name, options = {}) {
138
+ this.__prepare();
139
+ if (this.elementProperties.has(name)) return;
140
+
141
+ if (options.state) {
142
+ options = { ...options, attribute: false };
143
+ }
144
+
145
+ this.elementProperties.set(name, options);
146
+
147
+ if (!options.noAccessor) {
148
+ const key = Symbol();
149
+ const descriptor = this.getPropertyDescriptor(name, key, options);
150
+ if (descriptor !== undefined) {
151
+ Object.defineProperty(this.prototype, name, descriptor);
152
+ }
153
+ }
154
+ }
155
+
156
+ static getPropertyDescriptor(name, key, options) {
157
+ const { type, reflect, converter } = options;
158
+ const toAttribute = converter?.toAttribute ?? defaultConverter.toAttribute;
159
+
160
+ return {
161
+ get() {
162
+ return this[key];
163
+ },
164
+ set(value) {
165
+ const oldValue = this[key];
166
+ this[key] = value;
167
+ this.requestUpdate(name, oldValue, options);
168
+ },
169
+ configurable: true,
170
+ enumerable: true,
171
+ };
172
+ }
173
+
174
+ // ─── Static: addInitializer ───
175
+ static _initializers = [];
176
+
177
+ static addInitializer(fn) {
178
+ this._initializers = [...(this._initializers || []), fn];
179
+ }
180
+
181
+ // ─── Static: shadowRootOptions ───
182
+ static shadowRootOptions = { mode: 'open' };
183
+
184
+ // ─── Constructor ───
185
+ constructor() {
186
+ super();
187
+
188
+ this.__updatePending = false;
189
+ this.__hasUpdated = false;
190
+ this.__controllers = new Set();
191
+ this.__reflectingProperty = null;
192
+
193
+ // Ensure finalized
194
+ if (!this.constructor.hasOwnProperty('finalized')) {
195
+ this.constructor.finalize();
196
+ }
197
+
198
+ // CloneNode support: save any properties set before upgrade
199
+ this.__saveInstanceProperties();
200
+
201
+ this.requestUpdate();
202
+
203
+ for (const fn of this.constructor._initializers || []) {
204
+ fn(this);
205
+ }
206
+ }
207
+
208
+ // ─── Instance properties save/replay (cloneNode support) ───
209
+ __saveInstanceProperties() {
210
+ const elementProperties = this.constructor.elementProperties;
211
+ if (!elementProperties) return;
212
+
213
+ for (const [name, options] of elementProperties) {
214
+ if (options.noAccessor) continue;
215
+
216
+ if (this.hasOwnProperty(name)) {
217
+ // Plain property set before setter was defined (cloneNode)
218
+ const value = this[name];
219
+ delete this[name];
220
+ // Store via the Symbol key
221
+ this.__instancePropertyValues = this.__instancePropertyValues || new Map();
222
+ this.__instancePropertyValues.set(name, value);
223
+ }
224
+ }
225
+ }
226
+
227
+ // ─── Lifecycle callbacks ───
228
+ connectedCallback() {
229
+ this.__controllers.forEach((c) => c.hostConnected?.());
230
+
231
+ // Ensure finalized
232
+ if (!this.constructor.hasOwnProperty('finalized')) {
233
+ this.constructor.finalize();
234
+ }
235
+
236
+ // Sync existing attributes to properties (upgrade scenario)
237
+ // attributeChangedCallback doesn't fire for pre-existing attributes
238
+ const attrMap = this.constructor.__attributeToPropertyMap;
239
+ if (attrMap) {
240
+ for (const [attrName, propName] of attrMap) {
241
+ const value = this.getAttribute(attrName);
242
+ if (value !== null) {
243
+ const options = this.constructor.elementProperties.get(propName);
244
+ if (options) {
245
+ const fromAttr =
246
+ options.converter?.fromAttribute ?? defaultConverter.fromAttribute;
247
+ this[propName] = fromAttr(value, options.type);
248
+ }
249
+ }
250
+ }
251
+ }
252
+
253
+ // Replay instance properties saved by __saveInstanceProperties (cloneNode)
254
+ if (this.__instancePropertyValues) {
255
+ for (const [name, value] of this.__instancePropertyValues) {
256
+ this[name] = value;
257
+ }
258
+ this.__instancePropertyValues = undefined;
259
+ }
260
+
261
+ // Ensure renderRoot is created
262
+ this.renderRoot;
263
+
264
+ // Trigger first update if not yet done
265
+ if (!this.__hasUpdated) {
266
+ this.requestUpdate();
267
+ }
268
+ }
269
+
270
+ disconnectedCallback() {
271
+ this.__controllers.forEach((c) => c.hostDisconnected?.());
272
+ }
273
+
274
+ attributeChangedCallback(name, _old, value) {
275
+ const propName = this.constructor.__attributeToPropertyMap?.get(name);
276
+ if (propName !== undefined && this.__reflectingProperty !== propName) {
277
+ const options = this.constructor.elementProperties.get(propName);
278
+ if (options) {
279
+ const converter =
280
+ typeof options.converter === 'function'
281
+ ? { fromAttribute: options.converter }
282
+ : options.converter?.fromAttribute !== undefined
283
+ ? options.converter
284
+ : defaultConverter;
285
+ this.__reflectingProperty = propName;
286
+ const convertedValue = converter.fromAttribute(value, options.type);
287
+ this[propName] = convertedValue;
288
+ this.__reflectingProperty = null;
289
+ }
290
+ }
291
+ }
292
+
293
+ // ─── RenderRoot & Styles ───
294
+ get renderRoot() {
295
+ if (!this._renderRoot) {
296
+ this._renderRoot = this.createRenderRoot();
297
+ this.__adoptStyles();
298
+ }
299
+ return this._renderRoot;
300
+ }
301
+
302
+ createRenderRoot() {
303
+ return this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions);
304
+ }
305
+
306
+ __adoptStyles() {
307
+ const styles = this.constructor.elementStyles;
308
+ if (!styles) return;
309
+
310
+ const items = Array.isArray(styles) ? styles : [styles];
311
+ const parts = [];
312
+ for (const item of items) {
313
+ if (typeof item === 'string') {
314
+ parts.push(item);
315
+ } else if (isTemplateResult(item)) {
316
+ parts.push(item.strings.join(''));
317
+ }
318
+ }
319
+ if (parts.length === 0) return;
320
+
321
+ if (this._renderRoot.adoptedStyleSheets !== undefined) {
322
+ const sheet = new CSSStyleSheet();
323
+ sheet.replaceSync(parts.join('\n'));
324
+ this._renderRoot.adoptedStyleSheets = [...this._renderRoot.adoptedStyleSheets, sheet];
325
+ } else {
326
+ const tag = document.createElement('style');
327
+ tag.textContent = parts.join('\n');
328
+ this._renderRoot.appendChild(tag);
329
+ }
330
+ }
331
+
332
+ // ─── Update lifecycle ───
333
+ enableUpdating(_requestedUpdate) {}
334
+
335
+ get updateComplete() {
336
+ return this.__updateCompletePromise ?? Promise.resolve();
337
+ }
338
+
339
+ get hasUpdated() {
340
+ return this.__hasUpdated;
341
+ }
342
+
343
+ get isUpdatePending() {
344
+ return this.__updatePending;
345
+ }
346
+
347
+ requestUpdate(name, oldValue, options) {
348
+ if (name !== undefined) {
349
+ const hasChanged = options?.hasChanged ?? ((v, o) => !Object.is(v, o));
350
+ if (!hasChanged(this[name], oldValue)) return;
351
+ }
352
+
353
+ if (!this.__updatePending) {
354
+ this.__updatePending = true;
355
+ enqueueUpdate(this);
356
+ }
357
+ }
358
+
359
+ async performUpdate() {
360
+ if (!this.isUpdatePending) return;
361
+
362
+ this.__updatePending = false;
363
+
364
+ this.willUpdate(new Map());
365
+
366
+ this.__controllers.forEach((c) => c.hostUpdate?.());
367
+
368
+ this.update(new Map());
369
+
370
+ this.__hasUpdated = true;
371
+
372
+ // Reflect properties to attributes after update
373
+ for (const [name, options] of this.constructor.elementProperties) {
374
+ if (options.reflect) {
375
+ const value = this[name];
376
+ const toAttribute = options.converter?.toAttribute ?? defaultConverter.toAttribute;
377
+ const attr = this.constructor.__attributeNameForProperty(name, options);
378
+ if (attr !== undefined) {
379
+ this.__reflectingProperty = name;
380
+ const attrValue = toAttribute(value, options.type);
381
+ if (attrValue === null || attrValue === undefined) {
382
+ this.removeAttribute(attr);
383
+ } else {
384
+ this.setAttribute(attr, String(attrValue));
385
+ }
386
+ this.__reflectingProperty = null;
387
+ }
388
+ }
389
+ }
390
+
391
+ this.__controllers.forEach((c) => c.hostUpdated?.());
392
+ this.firstUpdated(new Map());
393
+ this.updated(new Map());
394
+ }
395
+
396
+ _$performUpdate() {
397
+ this.performUpdate();
398
+ }
399
+
400
+ // ─── Lifecycle hooks ───
401
+ shouldUpdate(_changedProperties) {
402
+ return true;
403
+ }
404
+
405
+ willUpdate(_changedProperties) {}
406
+
407
+ update(_changedProperties) {
408
+ const result = this.render();
409
+ if (result) {
410
+ render(result, this.renderRoot, { host: this });
411
+ }
412
+ }
413
+
414
+ render() {
415
+ return html``;
416
+ }
417
+
418
+ firstUpdated(_changedProperties) {}
419
+ updated(_changedProperties) {}
420
+ propertyChangedCallback(_name, _oldValue, _newValue) {}
421
+
422
+ // ─── Controller API ───
423
+ addController(controller) {
424
+ this.__controllers.add(controller);
425
+ if (this.isConnected) {
426
+ controller.hostConnected?.();
427
+ }
428
+ }
429
+
430
+ removeController(controller) {
431
+ this.__controllers.delete(controller);
432
+ }
433
+
434
+ // ─── Utilities ───
435
+ emit(type, detail, options = {}) {
436
+ return this.dispatchEvent(
437
+ new CustomEvent(type, {
438
+ detail,
439
+ bubbles: true,
440
+ composed: true,
441
+ ...options,
442
+ })
443
+ );
444
+ }
445
+
446
+ $(selector) {
447
+ return this.renderRoot.querySelector(selector);
448
+ }
449
+
450
+ $$(selector) {
451
+ return this.renderRoot.querySelectorAll(selector);
452
+ }
453
+ }
454
+
455
+ // ─── Register component ───
456
+ export function registerComponent(name, component) {
457
+ if (!name.includes('-')) {
458
+ throw new Error(`Component name must contain a hyphen: ${name}`);
459
+ }
460
+ if (!customElements.get(name)) {
461
+ customElements.define(name, component);
462
+ }
463
+ }
464
+
465
+ // ─── createComponent (function-based components) ───
466
+ export function createComponent(renderFn, options = {}) {
467
+ const { properties = {} } = options;
468
+
469
+ class DynamicComponent extends LuxElement {
470
+ static properties = properties;
471
+ render() {
472
+ return renderFn.call(this, this);
473
+ }
474
+ }
475
+
476
+ return DynamicComponent;
477
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * ICU MessageFormat subset parser
3
+ *
4
+ * Supports:
5
+ * - Simple interpolation: "Hello, {name}"
6
+ * - Plural: "{count, plural, =0{No items} =1{# item} other{# items}}"
7
+ * - Select: "{gender, select, male{He} female{She} other{They}}"
8
+ */
9
+
10
+ /**
11
+ * Find the matching closing brace for an opening brace at position `start`.
12
+ */
13
+ function findMatchingBrace(str, start) {
14
+ let depth = 1;
15
+ for (let i = start + 1; i < str.length; i++) {
16
+ if (str[i] === '{') depth++;
17
+ else if (str[i] === '}') {
18
+ depth--;
19
+ if (depth === 0) return i;
20
+ }
21
+ }
22
+ return -1;
23
+ }
24
+
25
+ /**
26
+ * Extract all top-level {...} expressions from the template.
27
+ */
28
+ function extractExpressions(template) {
29
+ const parts = [];
30
+ let i = 0;
31
+ while (i < template.length) {
32
+ if (template[i] === '{') {
33
+ const end = findMatchingBrace(template, i);
34
+ if (end !== -1) {
35
+ parts.push({ start: i, end, expr: template.slice(i + 1, end) });
36
+ i = end + 1;
37
+ continue;
38
+ }
39
+ }
40
+ i++;
41
+ }
42
+ return parts;
43
+ }
44
+
45
+ export function formatMessage(template, values = {}) {
46
+ if (!template || typeof template !== 'string') return '';
47
+
48
+ const exprs = extractExpressions(template);
49
+ let result = '';
50
+ let lastIdx = 0;
51
+
52
+ for (const { start, end, expr } of exprs) {
53
+ result += template.slice(lastIdx, start);
54
+ const trimmed = expr.trim();
55
+
56
+ // Plural: {key, plural, =N{...} other{...}}
57
+ const pluralMatch = trimmed.match(/^(\w+),\s*plural\s*,\s*(.+)$/);
58
+ if (pluralMatch) {
59
+ const [, key, rules] = pluralMatch;
60
+ const count = Number(values[key]) || 0;
61
+ result += formatPlural(count, rules);
62
+ lastIdx = end + 1;
63
+ continue;
64
+ }
65
+
66
+ // Select: {key, select, val1{...} val2{...}}
67
+ const selectMatch = trimmed.match(/^(\w+),\s*select\s*,\s*(.+)$/);
68
+ if (selectMatch) {
69
+ const [, key, rules] = selectMatch;
70
+ const value = String(values[key] || '');
71
+ result += formatSelect(value, rules);
72
+ lastIdx = end + 1;
73
+ continue;
74
+ }
75
+
76
+ // Simple interpolation: {key}
77
+ result += values[trimmed] ?? template.slice(start, end + 1);
78
+ lastIdx = end + 1;
79
+ }
80
+
81
+ result += template.slice(lastIdx);
82
+ return result;
83
+ }
84
+
85
+ function formatPlural(count, rulesStr) {
86
+ const rules = parseRules(rulesStr);
87
+ const exact = rules[`=${count}`];
88
+ if (exact !== undefined) return exact.replace(/#/g, String(count));
89
+
90
+ const key = count === 1 ? 'one' : 'other';
91
+ const rule = rules[key] || rules['other'] || '';
92
+ return rule.replace(/#/g, String(count));
93
+ }
94
+
95
+ function formatSelect(value, rulesStr) {
96
+ const rules = parseRules(rulesStr);
97
+ return rules[value] || rules['other'] || '';
98
+ }
99
+
100
+ function parseRules(rulesStr) {
101
+ const rules = {};
102
+ const re = /(\w+|=\d+)\{([^}]*)\}/g;
103
+ let m;
104
+ while ((m = re.exec(rulesStr))) {
105
+ rules[m[1]] = m[2];
106
+ }
107
+ return rules;
108
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Lux i18n — Lightweight internationalization
3
+ *
4
+ * Usage:
5
+ * import { createI18n, msg, number, date } from 'lux/i18n';
6
+ *
7
+ * const i18n = createI18n({
8
+ * locale: 'zh-CN',
9
+ * messages: {
10
+ * 'zh-CN': { hello: '你好', welcome: '欢迎, {name}' },
11
+ * 'en': { hello: 'Hello', welcome: 'Welcome, {name}' },
12
+ * }
13
+ * });
14
+ *
15
+ * // In templates
16
+ * html`<h1>${msg('hello')}</h1>`
17
+ * html`<p>${msg('welcome', { name: 'Alice' })}</p>`
18
+ * html`<span>${msg('items', { count: 5 })}</span>`
19
+ *
20
+ * // Number/date formatting
21
+ * html`<span>${number(1234567)}</span>`
22
+ * html`<span>${date(new Date())}</span>`
23
+ */
24
+
25
+ import { formatMessage } from './format.js';
26
+ import { getDefaultLocale, formatNumber, formatDate } from './locale.js';
27
+
28
+ let currentLocale = '';
29
+ let currentMessages = {};
30
+ let listeners = [];
31
+
32
+ /**
33
+ * Get the current translation for a key.
34
+ */
35
+ export function msg(key, values = {}) {
36
+ const dict = currentMessages[currentLocale] || currentMessages;
37
+ const template = dict[key] ?? key;
38
+ return formatMessage(template, values);
39
+ }
40
+
41
+ /**
42
+ * Format a number using Intl.NumberFormat.
43
+ */
44
+ export function number(value, options) {
45
+ return formatNumber(
46
+ value,
47
+ typeof options === 'string' ? { style: options } : options,
48
+ currentLocale
49
+ );
50
+ }
51
+
52
+ /**
53
+ * Format a date using Intl.DateTimeFormat.
54
+ */
55
+ export function date(value, options) {
56
+ return formatDate(value, options, currentLocale);
57
+ }
58
+
59
+ /**
60
+ * Get the current locale string.
61
+ */
62
+ export function getLocale() {
63
+ return currentLocale;
64
+ }
65
+
66
+ /**
67
+ * Create an i18n instance.
68
+ *
69
+ * @param {Object} options
70
+ * @param {string} options.locale - Initial locale (default: navigator.language)
71
+ * @param {Object} messages - { 'zh-CN': {...}, 'en': {...} }
72
+ * @returns {Object} i18n instance
73
+ */
74
+ export function createI18n(options = {}) {
75
+ currentLocale = options.locale || getDefaultLocale();
76
+ currentMessages = options.messages || {};
77
+
78
+ function setLocale(locale) {
79
+ currentLocale = locale;
80
+ for (const fn of listeners) fn(locale);
81
+ }
82
+
83
+ function onLocaleChange(fn) {
84
+ listeners.push(fn);
85
+ return () => {
86
+ const i = listeners.indexOf(fn);
87
+ if (i >= 0) listeners.splice(i, 1);
88
+ };
89
+ }
90
+
91
+ return {
92
+ get locale() {
93
+ return currentLocale;
94
+ },
95
+ setLocale,
96
+ onLocaleChange,
97
+ msg,
98
+ number,
99
+ date,
100
+ getLocale,
101
+ };
102
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Locale detection
3
+ */
4
+
5
+ export function getDefaultLocale() {
6
+ if (typeof navigator !== 'undefined' && navigator.language) {
7
+ return navigator.language;
8
+ }
9
+ return 'en';
10
+ }
11
+
12
+ export function formatNumber(value, options = {}, locale) {
13
+ try {
14
+ return new Intl.NumberFormat(locale, options).format(value);
15
+ } catch {
16
+ return String(value);
17
+ }
18
+ }
19
+
20
+ export function formatDate(value, options = {}, locale) {
21
+ try {
22
+ return new Intl.DateTimeFormat(locale, options).format(value);
23
+ } catch {
24
+ return String(value);
25
+ }
26
+ }