amateras 0.11.2 → 0.12.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 (48) hide show
  1. package/README.md +17 -16
  2. package/package.json +1 -1
  3. package/packages/core/src/index.ts +16 -6
  4. package/packages/core/src/lib/hmr.ts +4 -1
  5. package/packages/core/src/structure/ElementProto.ts +12 -5
  6. package/packages/core/src/structure/GlobalState.ts +7 -2
  7. package/packages/core/src/structure/NodeProto.ts +6 -0
  8. package/packages/core/src/structure/Proto.ts +18 -3
  9. package/packages/css/src/lib/cache.ts +4 -1
  10. package/packages/for/src/structure/For.ts +1 -0
  11. package/packages/i18n/src/index.ts +12 -40
  12. package/packages/i18n/src/structure/I18n.ts +90 -35
  13. package/packages/i18n/src/structure/I18nSession.ts +49 -0
  14. package/packages/i18n/src/structure/I18nTranslation.ts +17 -29
  15. package/packages/i18n/src/types.ts +3 -1
  16. package/packages/if/src/global.ts +3 -3
  17. package/packages/if/src/structure/Condition.ts +1 -0
  18. package/packages/if/src/structure/ConditionStatement.ts +4 -2
  19. package/packages/if/src/structure/ElseIf.ts +2 -1
  20. package/packages/if/src/structure/If.ts +2 -1
  21. package/packages/match/src/structure/Match.ts +1 -0
  22. package/packages/meta/src/index.ts +14 -2
  23. package/packages/prefetch/src/index.ts +2 -8
  24. package/packages/router/src/global.ts +1 -1
  25. package/packages/router/src/index.ts +8 -5
  26. package/packages/router/src/structure/Link.ts +2 -1
  27. package/packages/router/src/structure/Page.ts +20 -3
  28. package/packages/router/src/structure/Route.ts +3 -3
  29. package/packages/router/src/structure/RouteNode.ts +1 -1
  30. package/packages/router/src/structure/RouteSlot.ts +8 -15
  31. package/packages/router/src/structure/Router.ts +16 -10
  32. package/packages/signal/src/index.ts +16 -21
  33. package/packages/signal/src/lib/objectSignal.ts +21 -0
  34. package/packages/signal/src/structure/Signal.ts +10 -1
  35. package/packages/ui/src/index.ts +6 -1
  36. package/packages/ui/src/structure/Accordion.ts +67 -0
  37. package/packages/ui/src/structure/Radio.ts +3 -8
  38. package/packages/ui/src/structure/Slide.ts +6 -0
  39. package/packages/ui/src/structure/Slideshow.ts +23 -1
  40. package/packages/ui/src/structure/Tabs.ts +120 -0
  41. package/packages/ui/src/structure/TextBlock.ts +11 -0
  42. package/packages/ui/src/structure/Waterfall.ts +73 -0
  43. package/packages/ui/src/structure/WaterfallItem.ts +21 -0
  44. package/packages/utils/src/global.ts +7 -0
  45. package/packages/utils/src/lib/utils.ts +1 -4
  46. package/packages/utils/src/structure/UID.ts +1 -0
  47. package/packages/widget/src/index.ts +4 -7
  48. package/packages/widget/src/structure/Widget.ts +1 -1
package/README.md CHANGED
@@ -53,7 +53,8 @@ const Counter = $.widget(() => ({
53
53
 
54
54
  console.log('This template only run once.');
55
55
 
56
- $('button', $$ => { $(double$)
56
+ $('button', $$ => {
57
+ $([ double$ ])
57
58
  $$.on('click', () => count$.set(val => val + 1));
58
59
  })
59
60
  }
@@ -79,21 +80,21 @@ Amateras 能让你编写接近 HTML 排版的模板代码,实现了在原生 J
79
80
 
80
81
  | 模块库 | 体积 | Gzip | 简介 |
81
82
  | --- | --- | --- | --- |
82
- | core | 4.97 kB | 2.13 kB | 核心模块 |
83
- | widget | 5.33 kB | 2.31 kB | 组件模块 |
84
- | signal | 6.38 kB | 2.69 kB | 响应式数据模块 |
85
- | css | 6.52 kB | 2.85 kB | 样式模块 |
86
- | for | 6.00 kB | 2.47 kB | 控制流 For 模块 |
87
- | if | 7.64 kB | 3.11 kB | 控制流 If 模块 |
88
- | match | 6.26 kB | 2.53 kB | 控制流 Match 模块 |
89
- | router | 10.73 kB | 4.29 kB | 页面路由器模块 |
90
- | i18n | 6.97 kB | 2.88 kB | 多语言界面模块 |
91
- | idb | 10.24 kB | 4.14 kB | IndexedDB 模块 |
92
- | markdown | 12.44 kB | 5.06 kB | Markdown 转换 HTML 模块 |
93
- | prefetch | 5.53 kB | 2.40 kB | SSR 数据预取 |
94
- | meta | 5.05 kB | 2.17 kB | SSR 页面 `meta` 标签管理 |
95
- | ui | 7.80 kB | 3.26 kB | UI 组件模块 |
96
- | utils | 4.97 kB | 2.13 kB | 通用工具库 |
83
+ | core | 5.47 kB | 2.32 kB | 核心模块 |
84
+ | widget | 0.35 kB | 0.17 kB | 组件模块 |
85
+ | signal | 1.78 kB | 0.74 kB | 响应式数据模块 |
86
+ | css | 1.56 kB | 0.71 kB | 样式模块 |
87
+ | for | 1.05 kB | 0.35 kB | 控制流 For 模块 |
88
+ | if | 3.08 kB | 1.15 kB | 控制流 If 模块 |
89
+ | match | 1.31 kB | 0.39 kB | 控制流 Match 模块 |
90
+ | router | 6.03 kB | 2.24 kB | 页面路由器模块 |
91
+ | i18n | 2.92 kB | 0.98 kB | 多语言界面模块 |
92
+ | idb | 5.26 kB | 1.99 kB | IndexedDB 模块 |
93
+ | markdown | 7.48 kB | 2.93 kB | Markdown 转换 HTML 模块 |
94
+ | prefetch | 0.41 kB | 0.20 kB | SSR 数据预取 |
95
+ | meta | 0.18 kB | 0.08 kB | SSR 页面 `meta` 标签管理 |
96
+ | ui | 6.50 kB | 2.25 kB | UI 组件模块 |
97
+ | utils | 0.00 kB | 0.00 kB | 通用工具库 |
97
98
 
98
99
  ## 文档
99
100
  1. [基础入门](/docs/Basic.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amateras",
3
- "version": "0.11.2",
3
+ "version": "0.12.0",
4
4
  "description": "Amateras is a JavaScript library for building user interface.",
5
5
  "module": "index.ts",
6
6
  "type": "module",
@@ -1,10 +1,10 @@
1
1
  import { onclient, onserver } from '#env';
2
- import { hmr } from '#lib/hmr';
3
- import { _instanceof, _null, forEach, isArray, isFunction, isString, isUndefined } from '@amateras/utils';
2
+ import { _instanceof, _null, forEach, isArray, isFunction, isString, isUndefined, toArray } from '@amateras/utils';
4
3
  import './global';
5
4
  import { ElementProto } from './structure/ElementProto';
6
5
  import { Proto } from './structure/Proto';
7
6
  import { TextProto } from './structure/TextProto';
7
+ import { hmr } from '#lib/hmr';
8
8
 
9
9
  type ElementProtoArguments<C extends Constructor> =
10
10
  RequiredKeys<RemoveIndexSignature<ConstructorParameters<C>[0]>> extends never
@@ -158,11 +158,21 @@ export namespace $ {
158
158
  callback(match as any);
159
159
  return cases.get(condition)?.() ?? cases.get(symbol_default)?.();
160
160
  }
161
+
162
+ export const async = (fn: () => Promise<void>) => {
163
+ Proto.proto?.global.promises.add(fn());
164
+ }
165
+
161
166
  export const stylesheet = onclient() ? new CSSStyleSheet() : _null;
162
- export const styleMap = new Map<Constructor<ElementProto>, string>();
163
- export const style = (proto: Constructor<ElementProto> | null, css: string) => {
164
- if (proto) styleMap.set(proto, css);
165
- stylesheet?.insertRule(css);
167
+ export const styleMap = new Map<Constructor<ElementProto>, Set<string>>();
168
+ export const style = (proto: Constructor<ElementProto> | null, css: string | string[]) => {
169
+ let rules = toArray(css)
170
+ if (proto) {
171
+ let set = styleMap.get(proto) ?? new Set();
172
+ forEach(rules, rule => set.add(rule));
173
+ styleMap.set(proto, set);
174
+ }
175
+ if (stylesheet) forEach(rules, rule => stylesheet!.insertRule(rule));
166
176
  }
167
177
 
168
178
  if (stylesheet) document.adoptedStyleSheets.push(stylesheet);
@@ -6,7 +6,10 @@ import { TextProto } from "#structure/TextProto";
6
6
  import { _Array_from, _instanceof, _undefined, forEach, is } from "@amateras/utils";
7
7
  import { symbol_ProtoType } from "./symbols";
8
8
 
9
- const elementProtoMap = import.meta.hot?.data.protoMap ?? new Map<HTMLElement, Proto>()
9
+ const elementProtoMap = (() => {
10
+ if (import.meta.hot) return import.meta.hot.data.protoMap ?? new Map<HTMLElement, Proto>();
11
+ return new Map<HTMLElement, Proto>()
12
+ })()
10
13
  // save data before HMR
11
14
  if (import.meta.hot) import.meta.hot.dispose(data => {
12
15
  data.protoMap = elementProtoMap;
@@ -1,4 +1,4 @@
1
- import { _Array_from, _Object_entries, forEach, isNull, isUndefined, map } from "@amateras/utils";
1
+ import { _Array_from, _null, _Object_assign, _Object_entries, forEach, isNull, isUndefined, map } from "@amateras/utils";
2
2
  import { NodeProto } from "./NodeProto";
3
3
 
4
4
  const SELF_CLOSING_TAGNAMES = ['img', 'hr', 'br', 'input', 'link', 'meta'];
@@ -15,10 +15,12 @@ export class ElementProto<H extends HTMLElement = HTMLElement> extends NodeProto
15
15
  }
16
16
 
17
17
  on<K extends keyof HTMLElementEventMap>(type: K, listener: (event: HTMLElementEventMap[K] & { currentTarget: H }) => void) {
18
- this.ondom(node => {
18
+ let setListener = (node: Node) => {
19
19
  node.addEventListener(type, listener as any)
20
20
  this.disposers.add(() => node.removeEventListener(type, listener as any))
21
- });
21
+ }
22
+ if (this.node) setListener(this.node);
23
+ else this.ondom(setListener);
22
24
  }
23
25
 
24
26
  override toString(): string {
@@ -57,14 +59,15 @@ export class ElementProto<H extends HTMLElement = HTMLElement> extends NodeProto
57
59
 
58
60
  innerHTML(html: string) {
59
61
  this.#innerHTML = html;
62
+ if (this.node) this.node.innerHTML = html;
60
63
  }
61
64
 
62
65
  attr(): Map<string, string>;
63
- attr(attrName: string): string | undefined;
66
+ attr(attrName: string): string | null;
64
67
  attr(attrName: string, attrValue: string | null): this;
65
68
  attr(attrName?: string, attrValue?: string | null) {
66
69
  if (!arguments.length) return this.#attr;
67
- if (isUndefined(attrValue)) return this.#attr.get(attrName!);
70
+ if (isUndefined(attrValue)) return this.#attr.get(attrName!) ?? _null;
68
71
  if (isNull(attrValue)) {
69
72
  this.#attr.delete(attrName!);
70
73
  this.node?.removeAttribute(attrName!);
@@ -86,6 +89,10 @@ export class ElementProto<H extends HTMLElement = HTMLElement> extends NodeProto
86
89
  this.node?.classList.remove(...tokens);
87
90
  }
88
91
 
92
+ style(declarations: Partial<CSSStyleDeclaration>) {
93
+ if (this.node) _Object_assign(this.node.style, declarations);
94
+ }
95
+
89
96
  private token(method: 'add' | 'delete', name: string, ...tokens: string[]) {
90
97
  let value = this.#attr.get(name);
91
98
  let tokenArr = new Set(value?.split(' ') ?? []);
@@ -1,9 +1,14 @@
1
- import { forEach } from "@amateras/utils";
1
+ import { _Object_assign, forEach } from "@amateras/utils";
2
2
 
3
3
  export class GlobalState {
4
4
  static disposers = new Set<(global: GlobalState) => void>()
5
-
5
+ promises = new Set<Promise<any>>();
6
6
  dispose() {
7
+ this.promises.clear();
7
8
  forEach(GlobalState.disposers, disposer => disposer(this))
8
9
  }
10
+
11
+ static assign(obj: object) {
12
+ _Object_assign(GlobalState.prototype, obj);
13
+ }
9
14
  }
@@ -1,5 +1,6 @@
1
1
  import { _null } from "@amateras/utils";
2
2
  import { Proto } from "./Proto";
3
+ import { onclient } from "#env";
3
4
 
4
5
  export class NodeProto<N extends Node & ChildNode = Node & ChildNode> extends Proto {
5
6
  node: null | N = _null;
@@ -12,6 +13,11 @@ export class NodeProto<N extends Node & ChildNode = Node & ChildNode> extends Pr
12
13
  this.modifiers.add(callback);
13
14
  }
14
15
 
16
+ inDOM() {
17
+ if (onclient()) return document.contains(this.node);
18
+ return false;
19
+ }
20
+
15
21
  override removeNode(): void {
16
22
  this.node?.remove();
17
23
  }
@@ -42,10 +42,10 @@ export abstract class Proto {
42
42
  }).flat()
43
43
  }
44
44
 
45
- build(children = true): this {
45
+ build(cascading = true): this {
46
46
  this.clear(true);
47
47
  $.context(Proto, this, () => this.layout?.(this));
48
- if (children) forEach(this.protos, proto => {
48
+ if (cascading) forEach(this.protos, proto => {
49
49
  proto.build()
50
50
  });
51
51
  return this
@@ -73,7 +73,7 @@ export abstract class Proto {
73
73
  if (dispose) forEach(this.protos, proto => proto.dispose())
74
74
  }
75
75
 
76
- findAbove<T>(filter: (proto: Proto) => boolean | T | void | null): T | null {
76
+ findAbove<T extends Proto>(filter: (proto: Proto) => any): T | null {
77
77
  let parent = this.parent;
78
78
  if (parent) return filter(parent) ? parent as T : parent.findAbove(filter);
79
79
  return _null;
@@ -87,4 +87,19 @@ export abstract class Proto {
87
87
  }
88
88
  return _null;
89
89
  }
90
+
91
+ findBelowAll<T extends Proto = Proto>(filter: (proto: Proto) => boolean | void): T[] {
92
+ let matches: T[] = [];
93
+ for (let proto of this.protos) {
94
+ if (filter(proto)) matches.push(proto as T);
95
+ matches.push(...proto.findBelowAll(filter) as T[]);
96
+ }
97
+ return matches;
98
+ }
99
+
100
+ /**
101
+ * This method will be called when control flow proto is updated,
102
+ * it's useful when you need re-render content of component while content updated.
103
+ */
104
+ mutate() {}
90
105
  }
@@ -18,7 +18,10 @@ export const cssGlobalRuleSet = new Set<$CSSRule>();
18
18
  * This is very suitable for storing CSS objects in JSON format, as the length of the JSON string will not
19
19
  * affect the retrieve efficiency of the Map.
20
20
  */
21
- export const cssRuleByJSONMap: Map<string, $CSSRule> = import.meta.hot?.data.cssMap ?? new Map();
21
+ export const cssRuleByJSONMap: Map<string, $CSSRule> = $.call(() => {
22
+ if (import.meta.hot) return import.meta.hot.data.cssMap ?? new Map();
23
+ return new Map();
24
+ })
22
25
 
23
26
  if (import.meta.hot) {
24
27
  import.meta.hot.dispose(data => {
@@ -32,6 +32,7 @@ export class For<T extends object = object> extends ProxyProto {
32
32
  if (node.parentNode) prevNode = node;
33
33
  else prevNode = prevNode?.parentNode?.insertBefore(node, prevNode.nextSibling)
34
34
  })
35
+ this.parent?.mutate()
35
36
  }
36
37
 
37
38
  this.list$.subscribe(update);
@@ -1,69 +1,41 @@
1
1
  import { I18n } from "#structure/I18n";
2
- import { I18nDictionary, type I18nDictionaryContext, type I18nDictionaryContextImporter } from "#structure/I18nDictionary";
3
- import { I18nTranslation as _I18nTranslation, I18nTranslation, type I18nTranslationOptions } from "#structure/I18nTranslation";
4
- import { _instanceof, _Object_assign } from "@amateras/utils";
5
- import type { GetDictionaryContextByKey, I18nTranslationDirKey, I18nTranslationKey, I18nTranslationParams, Mixin, ResolvedAsyncDictionary } from "./types";
2
+ import { I18nTranslation as _I18nTranslation, I18nTranslation } from "#structure/I18nTranslation";
3
+ import { _instanceof, _null, _Object_assign } from "@amateras/utils";
6
4
  import { GlobalState } from "@amateras/core";
5
+ import type { I18nSession } from "#structure/I18nSession";
7
6
 
8
7
  declare global {
9
8
  export namespace $ {
10
- export interface I18nFunction<D extends I18nDictionaryContext = {}> {
11
- <K extends I18nTranslationKey<D>, P extends I18nTranslationParams<K, D>>(path: K, ...params: P extends Record<string, never> ? [] : [P]): I18nTranslation;
12
- i18n: I18n;
13
- locale(lang?: string): Promise<void>;
14
- add<F extends I18nDictionaryContext | I18nDictionaryContextImporter>(lang: string, dictionary: F): I18nFunction<Mixin<D, (F extends I18nDictionaryContextImporter ? ResolvedAsyncDictionary<F> : F)>>;
15
- delete(lang: string): this;
16
- dir<K extends I18nTranslationDirKey<D>>(path: K): I18nFunction<GetDictionaryContextByKey<K, D>>
17
- }
18
- export function i18n(defaultLocale: string): I18nFunction;
9
+ export function i18n(defaultLocale: string): I18n;
19
10
  export type I18nTranslation = _I18nTranslation;
20
11
 
21
12
  export interface TextProcessorValueMap {
22
13
  i18n: I18nTranslation
23
14
  }
24
15
  }
16
+
17
+ export interface GlobalEventHandlersEventMap {
18
+ localeupdate: Event;
19
+ }
25
20
  }
26
21
 
27
22
  declare module '@amateras/core' {
28
23
  export interface GlobalState {
29
24
  i18n: {
30
- promises: Promise<any>[]
25
+ session: I18nSession | null
31
26
  }
32
27
  }
33
28
  }
34
29
 
35
- _Object_assign(GlobalState.prototype, {
30
+ GlobalState.assign({
36
31
  i18n: {
37
- promises: []
32
+ session: _null
38
33
  }
39
34
  })
40
35
 
41
- GlobalState.disposers.add(global => {
42
- global.i18n.promises = [];
43
- })
44
-
45
36
  _Object_assign($, {
46
37
  i18n(defaultLocale: string) {
47
- const i18n = new I18n(defaultLocale);
48
- const i18nFn = (key: string, options?: I18nTranslationOptions) => i18n.translate(key, options);
49
- _Object_assign(i18nFn, {
50
- i18n,
51
- async locale(locale: string) {
52
- await i18n.setLocale(locale);
53
- },
54
- add(lang: string, context: I18nDictionaryContext | I18nDictionaryContextImporter) {
55
- i18n.map.set(lang, new I18nDictionary(context));
56
- return this;
57
- },
58
- delete(lang: string) {
59
- i18n.map.delete(lang);
60
- return this;
61
- },
62
- dir(path: string) {
63
- return (key: string, options?: I18nTranslationOptions) => i18n.translate(`${path}.${key}`, options)
64
- }
65
- })
66
- return i18nFn
38
+ return new I18n(defaultLocale)
67
39
  }
68
40
  })
69
41
 
@@ -1,51 +1,106 @@
1
- import { _instanceof, _null, map } from "@amateras/utils";
2
- import { I18nDictionary } from "#structure/I18nDictionary";
1
+ import { I18nDictionary, type I18nDictionaryContext, type I18nDictionaryContextImporter } from "#structure/I18nDictionary";
3
2
  import { I18nTranslation, type I18nTranslationOptions } from "./I18nTranslation";
3
+ import { I18nSession } from "./I18nSession";
4
+ import { onclient, Proto } from "@amateras/core";
5
+ import type { I18nTranslationKey, I18nTranslationParams, Mixin, ResolvedAsyncDictionary, I18nTranslationDirKey, GetDictionaryContextByKey } from "../types";
6
+ import { map } from "@amateras/utils";
4
7
 
5
- export class I18n {
6
- map = new Map<string, I18nDictionary>();
7
- #defaultLocale: string;
8
- translations = new Set<I18nTranslation>();
9
- locale: string;
8
+ export class I18n<D extends I18nDictionaryContext = {}> {
9
+ #locale: string;
10
+ dictionaries = new Map<string, I18nDictionary>();
11
+ defaultLocale: string;
12
+ sessions = new Set<I18nSession>();
13
+ session = new I18nSession(this);
14
+ path = '';
15
+ static key = '__locale__';
10
16
  constructor(defaultLocale: string) {
11
- this.#defaultLocale = defaultLocale;
12
- this.locale = defaultLocale;
17
+ this.defaultLocale = defaultLocale;
18
+ this.#locale = defaultLocale;
19
+ this.sessions.add(this.session);
13
20
  }
14
21
 
15
- defaultLocale(): string;
16
- defaultLocale(locale: string): this;
17
- defaultLocale(locale?: string) {
18
- if (!arguments.length) return this.#defaultLocale;
19
- if (locale) this.#defaultLocale = locale;
22
+ add(lang: string, dictionary: I18nDictionaryContext | I18nDictionaryContextImporter) {
23
+ this.dictionaries.set(lang, new I18nDictionary(dictionary));
24
+ return this as any;
25
+ }
26
+
27
+ delete(lang: string) {
28
+ this.dictionaries.delete(lang);
20
29
  return this;
21
30
  }
22
31
 
23
- async setLocale(): Promise<string>;
24
- async setLocale(locale: string): Promise<this>;
25
- async setLocale(locale?: string) {
26
- if (!arguments.length) return this.locale;
27
- if (locale) {
28
- let dictionary = this.map.get(locale);
29
- if (!dictionary) {
30
- locale = locale.split('-')[0]!;
31
- return this.setLocale(locale);
32
- }
33
- if (locale !== this.locale) {
34
- this.locale = locale;
35
- await Promise.all(map(this.translations, translation => translation.update()))
36
- }
32
+ t<K extends I18nTranslationKey<D>, P extends I18nTranslationParams<K, D>>(path: K, ...params: P extends Record<string, never> ? [] : [P]): I18nTranslation;
33
+ t(key: string, options?: I18nTranslationOptions) {
34
+ return new I18nTranslation(this.getSession(), this.getFullPath(key), options);
35
+ }
36
+
37
+ text<K extends I18nTranslationKey<D>, P extends I18nTranslationParams<K, D>>(path: K, ...params: P extends Record<string, never> ? [] : [P]): Promise<string>;
38
+ async text(key: string, options?: I18nTranslationOptions) {
39
+ let content = await this.getSession().fetch(this.getFullPath(key), options)
40
+ return content.text.reduce((acc, str, i) => acc + str + (content.args[i] || ''), '')
41
+ }
42
+
43
+ dir(path: string) {
44
+ let i18n = this;
45
+ return {
46
+ t: (key: string, options: any) => i18n.t(`${path}.${key}` as any, options),
47
+ text: (key: string, options: any) => i18n.text(`${path}.${key}` as any, options),
48
+ dir: (postPath: string) => i18n.dir(`${path}.${postPath}`)
49
+ } as unknown as I18nDir;
50
+ }
51
+
52
+ locale(): string;
53
+ locale(locale: string): Promise<void>;
54
+ locale(locale?: string) {
55
+ if (!arguments.length) {
56
+ this.readStoreLocale();
57
+ return this.#locale;
37
58
  }
38
- return this;
59
+ if (!locale) return;
60
+ let dictionary = this.dictionaries.get(locale);
61
+ if (!dictionary) {
62
+ let splited = locale.split('-');
63
+ if (splited.length === 1) return;
64
+ return this.locale(splited[0]!);
65
+ }
66
+ this.#locale = locale;
67
+ this.writeStoreLocale(locale);
68
+ return Promise.all(map(this.sessions, session => session.locale(locale)));
69
+ }
70
+
71
+ private getFullPath(key: string) {
72
+ return this.path ? `${this.path}.${key}` : key;
39
73
  }
40
74
 
41
- dictionary(locale = this.locale) {
42
- if (!locale) return _null;
43
- const dictionary = this.map.get(locale);
44
- return dictionary;
75
+ private getSession() {
76
+ let parentProto = Proto.proto;
77
+ if (parentProto) {
78
+ let session = parentProto.global.i18n.session ?? new I18nSession(this);
79
+ parentProto.global.i18n.session = session;
80
+ return session;
81
+ }
82
+ else return this.session;
45
83
  }
46
84
 
47
- translate(key: string, options?: I18nTranslationOptions) {
48
- return new I18nTranslation(this, key, options);
85
+ private readStoreLocale() {
86
+ if (onclient()) this.#locale = localStorage.getItem(I18n.key) ?? this.defaultLocale;
87
+ }
88
+
89
+ private writeStoreLocale(locale: string) {
90
+ if (onclient()) localStorage.setItem(I18n.key, locale);
49
91
  }
50
92
  }
51
93
 
94
+ export interface I18n<D extends I18nDictionaryContext = {}> {
95
+ add<F extends I18nDictionaryContext | I18nDictionaryContextImporter>(lang: string, dictionary: F): I18n<Mixin<D, (F extends I18nDictionaryContextImporter ? ResolvedAsyncDictionary<F> : F)>>;
96
+ delete(lang: string): this;
97
+ t<K extends I18nTranslationKey<D>, P extends I18nTranslationParams<K, D>>(path: K, ...params: P extends Record<string, never> ? [] : [P]): I18nTranslation;
98
+ text<K extends I18nTranslationKey<D>, P extends I18nTranslationParams<K, D>>(path: K, ...params: P extends Record<string, never> ? [] : [P]): Promise<string>;
99
+ dir<K extends I18nTranslationDirKey<D>>(path: K): I18nDir<GetDictionaryContextByKey<K, D>>
100
+ }
101
+
102
+ export interface I18nDir<D extends I18nDictionaryContext = {}> {
103
+ t<K extends I18nTranslationKey<D>, P extends I18nTranslationParams<K, D>>(path: K, ...params: P extends Record<string, never> ? [] : [P]): I18nTranslation;
104
+ text<K extends I18nTranslationKey<D>, P extends I18nTranslationParams<K, D>>(path: K, ...params: P extends Record<string, never> ? [] : [P]): Promise<string>;
105
+ dir<K extends I18nTranslationDirKey<D>>(path: K): I18n<GetDictionaryContextByKey<K, D>>
106
+ }
@@ -0,0 +1,49 @@
1
+ import { isUndefined, map } from "@amateras/utils";
2
+ import type { I18nTranslationResult } from "../types";
3
+ import type { I18n } from "./I18n";
4
+ import type { I18nTranslation, I18nTranslationOptions } from "./I18nTranslation";
5
+ import { onclient } from "@amateras/core";
6
+
7
+ export class I18nSession {
8
+ translations = new Set<I18nTranslation>()
9
+ i18n: I18n;
10
+ #locale: string;
11
+ constructor(i18n: I18n) {
12
+ this.i18n = i18n;
13
+ this.#locale = i18n.locale();
14
+ i18n.sessions.add(this);
15
+ }
16
+
17
+ async fetch(key: string, options?: I18nTranslationOptions): Promise<I18nTranslationResult> {
18
+ const dictionary = this.i18n.dictionaries.get(this.#locale);
19
+ if (!dictionary) return {text: [key], args: []};
20
+ const translate = await dictionary.find(key);
21
+ if (isUndefined(translate)) return {text: [key], args: []};
22
+ const snippets = translate.split(/\$[a-zA-Z0-9_]+\$/);
23
+ if (snippets.length === 1 || !options) return {text: [translate], args: []}
24
+ const matches = translate.matchAll(/(\$([a-zA-Z0-9_]+)\$)/g);
25
+ return {text: snippets, args: map(matches as unknown as [string, string, string][], ([,,value]) => options[value])}
26
+ }
27
+
28
+ locale(): string;
29
+ locale(locale: string): Promise<void>;
30
+ locale(locale?: string) {
31
+ if (!arguments.length) return this.#locale;
32
+ if (locale) {
33
+ let dictionary = this.i18n.dictionaries.get(locale);
34
+ if (!dictionary) {
35
+ let splited = locale.split('-');
36
+ if (splited.length === 1) return;
37
+ return this.locale(splited[0]!);
38
+ }
39
+ }
40
+ if (locale && locale !== this.#locale) {
41
+ this.#locale = locale;
42
+ return new Promise<void>(async (resolve) => {
43
+ await Promise.all(map(this.translations, translation => translation.update()));
44
+ resolve();
45
+ if (onclient()) dispatchEvent(new Event('localeupdate'));
46
+ })
47
+ }
48
+ }
49
+ }
@@ -1,17 +1,17 @@
1
- import type { I18n } from "#structure/I18n";
2
1
  import { ProxyProto } from "@amateras/core";
3
- import { forEach, isUndefined, map } from "@amateras/utils";
2
+ import { forEach } from "@amateras/utils";
3
+ import type { I18nSession } from "./I18nSession";
4
4
 
5
5
  export class I18nTranslation extends ProxyProto {
6
- i18n: I18n;
6
+ session: I18nSession;
7
7
  key: string;
8
8
  options: I18nTranslationOptions | undefined;
9
- constructor(i18n: I18n, key: string, options?: I18nTranslationOptions) {
9
+ constructor(session: I18nSession, key: string, options?: I18nTranslationOptions) {
10
10
  super()
11
- this.i18n = i18n;
11
+ this.session = session;
12
12
  this.key = key;
13
13
  this.options = options;
14
- this.i18n.translations.add(this);
14
+ session.translations.add(this);
15
15
  }
16
16
 
17
17
  override build(): this {
@@ -20,30 +20,18 @@ export class I18nTranslation extends ProxyProto {
20
20
  }
21
21
 
22
22
  async update() {
23
- const {key, i18n, options} = this;
24
- const contentUpdate = (content: string[], args: any[] = []) => {
25
- this.layout = () => {
26
- // make this array become Template String Array;
27
- //@ts-ignore
28
- content.raw = content;
29
- $(content as any, ...args);
30
- }
31
- forEach(this.protos, proto => proto.removeNode());
32
- super.build();
33
- this.node?.replaceWith(...this.toDOM());
34
- }
35
- update: {
36
- const dictionary = i18n.dictionary();
37
- if (!dictionary) { contentUpdate([key]); break update }
38
- const request = dictionary.find(key);
39
- this.global.i18n.promises.push(request);
40
- const translate = await request;
41
- if (isUndefined(translate)) break update;
42
- const snippets = translate.split(/\$[a-zA-Z0-9_]+\$/);
43
- if (snippets.length === 1 || !options) { contentUpdate([translate]); break update }
44
- const matches = translate.matchAll(/(\$([a-zA-Z0-9_]+)\$)/g);
45
- contentUpdate(snippets, map(matches as unknown as [string, string, string][], ([,,value]) => options[value]));
23
+ const request = this.session.fetch(this.key, this.options)
24
+ this.global.promises.add(request);
25
+ const {text, args} = await request;
26
+ this.layout = () => {
27
+ // make this array become Template String Array;
28
+ //@ts-ignore
29
+ text.raw = text;
30
+ $(text as any, ...args);
46
31
  }
32
+ forEach(this.protos, proto => proto.removeNode());
33
+ super.build();
34
+ this.node?.replaceWith(...this.toDOM());
47
35
  return this;
48
36
  }
49
37
  }
@@ -1,5 +1,7 @@
1
1
  import type { I18nDictionaryContext, I18nDictionaryContextImporter } from "#structure/I18nDictionary";
2
2
 
3
+ export type I18nTranslationResult = { text: string[], args: any[] }
4
+
3
5
  export type ResolvedAsyncDictionary<F extends I18nDictionaryContextImporter> = Awaited<ReturnType<F>>['default'];
4
6
 
5
7
  export type I18nTranslationKey<T> =
@@ -39,7 +41,7 @@ export type FindParam<T extends string> =
39
41
  T extends `${string}$${infer Param}$${infer Rest}`
40
42
  ? Param extends `${string}${' '}${string}`
41
43
  ? Prettify<{} & FindParam<Rest>>
42
- : Prettify<Record<Param, $.Layout> & FindParam<Rest>>
44
+ : Prettify<Record<Param, any> & FindParam<Rest>>
43
45
  : {}
44
46
 
45
47
  export type FindTranslationByKey<K extends string, T extends I18nDictionaryContext> =
@@ -2,14 +2,14 @@ import type { Condition } from "#structure/Condition";
2
2
  import * as _Else from "#structure/Else";
3
3
  import * as _ElseIf from "#structure/ElseIf";
4
4
  import * as _If from "#structure/If";
5
- import type { Signal } from "@amateras/signal";
5
+ import type { SignalType } from "@amateras/signal";
6
6
 
7
7
  declare global {
8
8
  export var If: typeof _If.If
9
9
  export var Else: typeof _Else.Else
10
10
  export var ElseIf: typeof _ElseIf.ElseIf
11
11
 
12
- export function $(statement: typeof _If.If, signal: Signal<any>, layout: _If.IfLayout): Condition;
13
- export function $(statement: typeof _ElseIf.ElseIf, signal: Signal<any>, layout: _ElseIf.ElseIfLayout): Condition;
12
+ export function $<T>(statement: typeof _If.If, signal: SignalType<T>, layout: _If.IfLayout<T>): Condition;
13
+ export function $<T>(statement: typeof _ElseIf.ElseIf, signal: SignalType<T>, layout: _ElseIf.ElseIfLayout<T>): Condition;
14
14
  export function $(statement: typeof _Else.Else, layout: _Else.ElseLayout): Condition;
15
15
  }
@@ -23,6 +23,7 @@ export class Condition extends ProxyProto {
23
23
  this.statement = matchProto ?? _null;
24
24
  forEach(this.statements, proto => proto !== matchProto && proto.removeNode())
25
25
  this.node?.replaceWith(...this.toDOM());
26
+ this.parent?.mutate();
26
27
  }
27
28
  // build statements proto and subscribe expression signal
28
29
  forEach(this.statements, proto => {