cabloy 5.1.235 → 5.1.238

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 (69) hide show
  1. package/.cabloy-version +1 -1
  2. package/CHANGELOG.md +10 -0
  3. package/package.json +1 -1
  4. package/repo-docs/backend/broadcast-guide.md +2 -0
  5. package/repo-docs/backend/queue-guide.md +31 -0
  6. package/repo-docs/backend/schedule-guide.md +23 -0
  7. package/repo-docs/frontend/scripts.md +1 -1
  8. package/vona/packages-cli/cli/package.json +1 -1
  9. package/vona/packages-cli/cli-set-api/package.json +1 -1
  10. package/vona/packages-vona/vona/package.json +1 -1
  11. package/vona/packages-vona/vona-core/package.json +1 -1
  12. package/vona/packages-vona/vona-core/src/lib/bean/resource/locale/appLocale.ts +2 -2
  13. package/vona/packages-vona/vona-core/src/lib/bean/resource/locale/utils.ts +46 -1
  14. package/vona/packages-vona/vona-core/src/lib/framework/start.ts +4 -0
  15. package/vona/packages-vona/vona-core/src/lib/module/locales.ts +41 -22
  16. package/vona/packages-vona/vona-core/src/lib/utils/zod-enhance.ts +1 -1
  17. package/vona/packages-vona/vona-core/src/types/config/config.ts +2 -0
  18. package/vona/packages-vona/vona-core/src/types/config/locale.ts +11 -4
  19. package/vona/packages-vona/vona-core/src/types/interface/module.ts +1 -0
  20. package/vona/packages-vona/vona-mock/package.json +1 -1
  21. package/vona/src/backend/config/config/config.ts +9 -0
  22. package/vona/src/suite/a-home/modules/home-base/src/main.ts +1 -4
  23. package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
  24. package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
  25. package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
  26. package/vona/src/suite-vendor/a-vona/modules/a-hmr/package.json +1 -1
  27. package/vona/src/suite-vendor/a-vona/modules/a-hmr/src/service/watch.ts +1 -0
  28. package/vona/src/suite-vendor/a-vona/modules/a-locale/package.json +1 -1
  29. package/vona/src/suite-vendor/a-vona/modules/a-locale/src/config/config.ts +2 -4
  30. package/vona/src/suite-vendor/a-vona/modules/a-locale/src/main.ts +12 -30
  31. package/vona/src/suite-vendor/a-vona/modules/a-openapi/package.json +1 -1
  32. package/vona/src/suite-vendor/a-vona/modules/a-openapi/src/bean/bean.openapi.ts +6 -2
  33. package/vona/src/suite-vendor/a-vona/modules/a-openapi/src/service/openapi.ts +107 -48
  34. package/vona/src/suite-vendor/a-vona/modules/a-openapi/src/types/contract.ts +25 -0
  35. package/vona/src/suite-vendor/a-vona/modules/a-openapi/src/types/index.ts +1 -0
  36. package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/package.json +1 -1
  37. package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/src/lib/const/decorator.ts +1 -0
  38. package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/src/lib/decorator/index.ts +1 -0
  39. package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/src/lib/decorator/mcp.ts +28 -0
  40. package/vona/src/suite-vendor/a-vona/modules/a-openapiutils/src/types/api.ts +8 -0
  41. package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
  42. package/vona/src/suite-vendor/a-vona/modules/a-queue/package.json +1 -1
  43. package/vona/src/suite-vendor/a-vona/modules/a-queue/src/config/config.ts +2 -0
  44. package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/package.json +1 -1
  45. package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/src/service/rateLimit.ts +3 -1
  46. package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/src/types/rateLimit.ts +4 -0
  47. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  48. package/zova/packages-utils/zova-jsx/package.json +2 -2
  49. package/zova/packages-zova/zova/package.json +3 -3
  50. package/zova/packages-zova/zova-core/package.json +1 -1
  51. package/zova/packages-zova/zova-core/src/core/component/locale.ts +13 -3
  52. package/zova/packages-zova/zova-core/src/core/sys/config.ts +28 -1
  53. package/zova/packages-zova/zova-core/src/core/sys/locale.ts +24 -0
  54. package/zova/packages-zova/zova-core/src/core/sys/module.ts +16 -4
  55. package/zova/packages-zova/zova-core/src/core/sys/sys.ts +7 -2
  56. package/zova/packages-zova/zova-core/src/types/interface/module.ts +1 -0
  57. package/zova/packages-zova/zova-core/src/utils/zod-enhance.ts +1 -1
  58. package/zova/pnpm-lock.yaml +7 -7
  59. package/zova/quasar.config.ts +2 -0
  60. package/zova/src/suite/a-home/modules/home-base/src/config/locale/en-us.ts +1 -0
  61. package/zova/src/suite/a-home/modules/home-base/src/config/locale/zh-cn.ts +1 -0
  62. package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/render.locale.tsx +1 -1
  63. package/zova/src/suite-vendor/a-zova/modules/a-ssr/package.json +1 -1
  64. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/lib/ssr.ts +13 -1
  65. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/lib/ssrNavigation.ts +24 -0
  66. package/zova/src/suite-vendor/a-zova/modules/a-ssr/src/monkey.ts +8 -1
  67. package/zova/src/suite-vendor/a-zova/modules/a-ssr/test/lib/ssrNavigation.test.ts +54 -1
  68. package/zova/src/suite-vendor/a-zova/modules/a-zova/package.json +2 -2
  69. package/zova/src/suite-vendor/a-zova/package.json +3 -3
package/.cabloy-version CHANGED
@@ -1 +1 @@
1
- 5.1.235
1
+ 5.1.238
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.1.238
4
+
5
+ No user-facing changes.
6
+
7
+ ## 5.1.236
8
+
9
+ ### Bug Fixes
10
+
11
+ - Fix `code-inspector-plugin`.
12
+
3
13
  ## 5.1.235
4
14
 
5
15
  ### Improvements
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cabloy",
3
- "version": "5.1.235",
3
+ "version": "5.1.238",
4
4
  "gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
5
5
  "description": "A Node.js fullstack framework",
6
6
  "keywords": [
@@ -8,6 +8,8 @@ Broadcast lets one worker emit a message that multiple worker processes can rece
8
8
 
9
9
  This is important because some business logic is not point-to-point background work. It is multi-worker coordination work.
10
10
 
11
+ The current broadcast transport uses nonpersistent Redis Pub/Sub, not BullMQ jobs. It has no retained job history or queue-style retention options such as `removeOnComplete` / `removeOnFail`.
12
+
11
13
  ## Create a broadcast
12
14
 
13
15
  Example: create a broadcast named `echo` in module `training-student`.
@@ -188,6 +188,37 @@ class QueueAdd {}
188
188
 
189
189
  That means queue behavior is not only asynchronous. It also participates in concurrency, isolation, and consistency policy.
190
190
 
191
+ ### Finished-job retention
192
+
193
+ The global `a-queue` worker defaults are `removeOnComplete: { count: 0 }` and `removeOnFail: { count: 0 }`. Jobs are removed immediately on success or final failure, not on intermediate failures that will be retried. This removes finished job records, not the BullMQ events stream: `pushAsync` still receives completion results and final errors through `QueueEvents`.
194
+
195
+ For queue-specific retention, set `options.worker` explicitly. Worker removal options take BullMQ `KeepJobs` objects, not booleans; for example, retain up to 100 completed and 100 failed jobs:
196
+
197
+ ```typescript
198
+ @Queue({
199
+ options: {
200
+ worker: {
201
+ removeOnComplete: { count: 100 },
202
+ removeOnFail: { count: 100 },
203
+ },
204
+ },
205
+ })
206
+ class QueueAdd {}
207
+ ```
208
+
209
+ For ordinary `push` / `pushAsync` jobs, `options.job` can instead set `removeOnComplete` / `removeOnFail` to `false` (retain without automatic removal) or a bounded object such as `{ count: 100 }`. Job-level values override worker defaults; per-push `jobOptions` override the corresponding `options.job` values:
210
+
211
+ ```typescript
212
+ await this.scope.queue.add.push(data, {
213
+ jobOptions: {
214
+ removeOnComplete: { count: 100 },
215
+ removeOnFail: false,
216
+ },
217
+ });
218
+ ```
219
+
220
+ Changing the defaults does not automatically purge previously retained jobs. Retain records when later job lookups or manual retries are needed. Custom `jobId` duplicate suppression lasts only while that job exists; after removal, the same ID can be submitted again. Bounded retention is not an exact TTL or a permanent deduplication guarantee.
221
+
191
222
  ## Configure queues in app config
192
223
 
193
224
  Queue options can also be overridden in app config.
@@ -57,6 +57,29 @@ Schedule parameters can also include:
57
57
 
58
58
  This matters because recurring jobs often have the same consistency concerns as ordinary queue jobs.
59
59
 
60
+ ### Finished-run retention
61
+
62
+ Scheduled runs inherit the selected queue's worker policy, including the global `a-queue` defaults: remove immediately on success or final failure, but not during intermediate retries. See [Finished-job retention](/backend/queue-guide#finished-job-retention) for worker overrides and operational caveats.
63
+
64
+ Schedules call BullMQ `upsertJobScheduler` directly, bypassing the normal queue-push `options.job` merge. To override retention for a schedule, use `templateOptions`, for example to retain up to 100 successful and 100 failed runs:
65
+
66
+ ```typescript
67
+ @Schedule({
68
+ repeat: { every: 3000 },
69
+ templateOptions: {
70
+ removeOnComplete: { count: 100 },
71
+ removeOnFail: { count: 100 },
72
+ },
73
+ })
74
+ export class ScheduleLog extends BeanBase implements IScheduleExecute {
75
+ async execute() {
76
+ console.log(Date.now());
77
+ }
78
+ }
79
+ ```
80
+
81
+ These options govern finished runs, not the recurring scheduler definition; changing them does not automatically purge old retained runs.
82
+
60
83
  ## Enable/disable and environment scoping
61
84
 
62
85
  Like queues, schedules can be enabled or disabled and scoped to flavor or mode.
@@ -53,7 +53,7 @@ The `dev:zova:*` commands start the Zova development server, which uses `9000` i
53
53
 
54
54
  Cabloy Basic and Cabloy Start enable `code-inspector-plugin` in Zova development builds. In the active edition's repository, start either `npm run dev:zova:admin` or `npm run dev:zova:web`, then open the corresponding Zova standalone SSR page. The wrappers select `cabloyBasicAdmin` / `cabloyBasicWeb` in Basic and `cabloyStartAdmin` / `cabloyStartWeb` in Start. Admin and Web share the worktree's development ports; run them sequentially, not concurrently.
55
55
 
56
- Hold **Option+Shift** on macOS or **Alt+Shift** on Windows/Linux, move over an element, and click to open its authored TSX location. This works with TSX in Controllers and split Render beans; it does not require converting them into Vue SFCs. Fragments have no independent DOM node, and custom-component roots may point to implementation markup rather than the caller's component tag.
56
+ Hold **Option+Shift** on macOS or **Alt+Shift** on Windows/Linux, move over an element, and click to open its authored TSX location. This works with TSX in Controllers and split Render beans; it does not require converting them into Vue SFCs. Fragments have no independent DOM node. In both editions, instrumentation skips PascalCase and member-expression component tags and retains their native TSX descendants. This avoids passing inspector attributes to wrappers such as `ClientOnly` and `VDefaultsProvider` that have no inheritable DOM root; component invocation tags are not direct location targets.
57
57
 
58
58
  The application configuration in `zova/quasar.config.ts` uses the browser URI handler `vscode://file/{file}:{line}:{column}`. VS Code must be installed and registered for that scheme; the browser may ask for permission to open it. To use another compatible editor, change `behavior.target` to its supported URI scheme. Actual editor opening depends on that local browser/OS setup.
59
59
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-cli",
3
- "version": "1.1.172",
3
+ "version": "1.1.173",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona cli",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-cli-set-api",
3
- "version": "1.1.170",
3
+ "version": "1.1.171",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona cli-set-api",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona",
3
- "version": "5.1.117",
3
+ "version": "5.1.118",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "Vona is an intuitive, elegant and powerful Node.js framework for rapidly developing enterprise applications of any size",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-core",
3
- "version": "5.1.51",
3
+ "version": "5.1.52",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona",
6
6
  "keywords": [
@@ -5,11 +5,11 @@ import type { ILocaleRecord, IModuleLocale, IModuleLocaleText } from './type.ts'
5
5
  import { cast } from '../../../../types/utils/cast.ts';
6
6
  import { BeanSimple } from '../../beanSimple.ts';
7
7
  import { LocaleModuleNameSeparator } from './type.ts';
8
- import { isLocaleMagic } from './utils.ts';
8
+ import { isLocaleMagic, resolveLocale } from './utils.ts';
9
9
 
10
10
  export class AppLocale extends BeanSimple {
11
11
  get current(): keyof ILocaleRecord {
12
- return this.ctx ? this.ctx.locale : 'en-us';
12
+ return this.ctx ? this.ctx.locale : resolveLocale(undefined, this.app.config.locale);
13
13
  }
14
14
 
15
15
  set current(value: keyof ILocaleRecord) {
@@ -1,5 +1,6 @@
1
+ import type { ConfigLocale } from '../../../../types/config/locale.ts';
1
2
  import type { IBeanScopeLocale } from '../../type.ts';
2
- import type { ILocaleMagic } from './type.ts';
3
+ import type { ILocaleMagic, ILocaleRecord } from './type.ts';
3
4
 
4
5
  import { cast } from '../../../../types/utils/cast.ts';
5
6
  import { useApp } from '../../../framework/useApp.ts';
@@ -59,3 +60,47 @@ export function text(strings: any, ...expressions: any): any {
59
60
  export function isLocaleMagic(str: string | object) {
60
61
  return typeof str === 'object' && cast(str).toJSON;
61
62
  }
63
+
64
+ export function canonicalizeLocale(value: unknown): string {
65
+ return typeof value === 'string' ? value.trim().replaceAll('_', '-').toLowerCase() : '';
66
+ }
67
+
68
+ export function validateLocaleConfig(config: ConfigLocale): void {
69
+ const items = config?.items;
70
+ if (!items || typeof items !== 'object' || Object.keys(items).length === 0) {
71
+ throw new Error('config.locale.items must contain at least one enabled locale');
72
+ }
73
+ for (const key of Object.keys(items)) {
74
+ if (!key || canonicalizeLocale(key) !== key || typeof items[key] !== 'string') {
75
+ throw new Error(`config.locale.items contains an invalid locale: ${key}`);
76
+ }
77
+ }
78
+ if (
79
+ typeof config.default !== 'string' ||
80
+ !Object.hasOwn(items, config.default) ||
81
+ canonicalizeLocale(config.default) !== config.default
82
+ ) {
83
+ throw new Error('config.locale.default must be an enabled config.locale.items key');
84
+ }
85
+ }
86
+
87
+ /** Resolve an enabled input without allowing aliases to expand the configured set. */
88
+ export function resolveEnabledLocale(
89
+ value: unknown,
90
+ config: ConfigLocale,
91
+ aliases: Record<string, string> = {},
92
+ ): keyof ILocaleRecord | undefined {
93
+ validateLocaleConfig(config);
94
+ let locale = canonicalizeLocale(value);
95
+ if (Object.hasOwn(aliases, locale)) locale = canonicalizeLocale(aliases[locale]);
96
+ return Object.hasOwn(config.items, locale) ? (locale as keyof ILocaleRecord) : undefined;
97
+ }
98
+
99
+ /** Active-language selection only; explicit translation lookup remains independent. */
100
+ export function resolveLocale(
101
+ value: unknown,
102
+ config: ConfigLocale,
103
+ aliases?: Record<string, string>,
104
+ ): keyof ILocaleRecord {
105
+ return resolveEnabledLocale(value, config, aliases) ?? config.default;
106
+ }
@@ -97,6 +97,10 @@ async function __prepareConfig(app: VonaApplication): Promise<VonaConfigOptional
97
97
  const configItemsRes = await Promise.all(configItemsPromise);
98
98
  for (const configItem of configItemsRes) {
99
99
  if (configItem) {
100
+ // An explicit language subset replaces the preceding policy, not its individual keys.
101
+ if (configItem.locale && Object.hasOwn(configItem.locale, 'items') && config.locale) {
102
+ delete config.locale.items;
103
+ }
100
104
  deepExtend(config, configItem);
101
105
  }
102
106
  }
@@ -1,34 +1,56 @@
1
1
  import type { IModule } from '@cabloy/module-info';
2
2
 
3
- import type { TypeModuleResourceLocales, VonaLocaleOptional } from '../../types/index.ts';
3
+ import type {
4
+ ILocaleProviderResource,
5
+ TypeModuleResourceLocales,
6
+ VonaLocaleOptional,
7
+ } from '../../types/index.ts';
4
8
  import type { VonaApplication } from '../core/application.ts';
5
9
 
6
10
  import localesDefault from '../core/locales.ts';
7
11
  import { deepExtend } from '../utils/util.ts';
8
12
 
9
13
  export default async function (app: VonaApplication, modules: Record<string, IModule>) {
10
- // all locales
11
- app.meta.locales = localesDefault;
14
+ // Each application owns its registry; never add provider/project data to imported resources.
15
+ app.meta.locales = deepExtend({}, localesDefault);
12
16
  app.meta.localeModules = {};
13
17
 
14
- // load locales
15
- await loadLocales();
18
+ const localesAll = await app.options.locales();
19
+ // Preserve existing project global/module override semantics.
20
+ for (const locale in localesAll.localesDefault) {
21
+ _initLocales(locale, localesAll.localesDefault[locale]);
22
+ }
23
+ for (const locale in localesAll.localesModules) {
24
+ _initLocales(locale, localesAll.localesModules[locale]);
25
+ }
16
26
 
17
- async function loadLocales() {
18
- const localesAll = await app.options.locales();
19
- // project locales default
20
- for (const locale in localesAll.localesDefault) {
21
- _initLocales(locale, localesAll.localesDefault[locale]);
22
- }
23
- // project locales modules
24
- for (const locale in localesAll.localesModules) {
25
- _initLocales(locale, localesAll.localesModules[locale]);
27
+ // Providers precede all ordinary dictionaries, independent of their module order.
28
+ for (const moduleName of Object.keys(modules)) {
29
+ const module = modules[moduleName];
30
+ if (!module.info.capabilities?.locale) continue;
31
+ const locales = module.resource.localesProvider;
32
+ if (!locales) continue;
33
+ for (const locale of Object.keys(locales)) {
34
+ const resource = locales[locale] as ILocaleProviderResource;
35
+ const globals = Object.fromEntries(
36
+ Object.entries(resource).filter(
37
+ ([key, value]) => key !== 'modules' && typeof value === 'string',
38
+ ),
39
+ );
40
+ app.meta.locales[locale] = Object.assign({}, globals, app.meta.locales[locale]);
41
+ for (const target of Object.keys(resource.modules ?? {})) {
42
+ // Metadata membership is sufficient; no target import, install or dependency is needed.
43
+ if (!Object.hasOwn(modules, target)) continue;
44
+ _registerLocale(target, locale, resource.modules![target]);
45
+ }
26
46
  }
27
- // app cache
28
- app.meta.hmrCacheLocaleModules = deepExtend({}, app.meta.localeModules);
29
- // module locales
30
- for (const moduleName in modules) {
31
- const module = modules[moduleName];
47
+ }
48
+
49
+ // Preserve project/provider precedence when ordinary target dictionaries are reloaded.
50
+ app.meta.hmrCacheLocaleModules = deepExtend({}, app.meta.localeModules);
51
+ for (const moduleName of Object.keys(modules)) {
52
+ const module = modules[moduleName];
53
+ if (!module.info.capabilities?.locale) {
32
54
  _registerLocales(moduleName, module.resource.locales);
33
55
  }
34
56
  }
@@ -36,7 +58,6 @@ export default async function (app: VonaApplication, modules: Record<string, IMo
36
58
  function _initLocales(locale: string, locales: VonaLocaleOptional | undefined) {
37
59
  if (!locales) return;
38
60
  if (!locales.modules) {
39
- // override
40
61
  app.meta.locales[locale] = Object.assign({}, app.meta.locales[locale], locales);
41
62
  } else {
42
63
  const moduleMap = locales.modules;
@@ -54,9 +75,7 @@ export default async function (app: VonaApplication, modules: Record<string, IMo
54
75
  }
55
76
 
56
77
  function _registerLocale(moduleName: string, locale: string, moduleLocales: object) {
57
- // locales: not override
58
78
  app.meta.locales[locale] = Object.assign({}, moduleLocales, app.meta.locales[locale]);
59
- // localeModules
60
79
  if (!app.meta.localeModules[moduleName]) app.meta.localeModules[moduleName] = {};
61
80
  app.meta.localeModules[moduleName][locale] = Object.assign(
62
81
  {},
@@ -11,7 +11,7 @@ import type { VonaApplication } from '../core/application.ts';
11
11
  import { useApp } from '../framework/useApp.ts';
12
12
 
13
13
  export type ZodLocaleError = () => { localeError: z.core.$ZodErrorMap };
14
- export type ZodLocaleErrors = Record<keyof ILocaleRecord, ZodLocaleError>;
14
+ export type ZodLocaleErrors = Partial<Record<keyof ILocaleRecord, ZodLocaleError>>;
15
15
 
16
16
  const queryCoercion = new AsyncLocalStorage<boolean>();
17
17
 
@@ -2,6 +2,7 @@ import type { VonaConfigMeta } from '@cabloy/module-info';
2
2
 
3
3
  import type { ConfigLogger, IBeanScopeConfig } from '../../index.ts';
4
4
  import type { PowerPartial } from '../utils/powerPartial.ts';
5
+ import type { ConfigLocale } from './locale.ts';
5
6
 
6
7
  // @ts-ignore ignore the throw type check of 'development'
7
8
  export interface VonaConfig {
@@ -31,6 +32,7 @@ export interface VonaConfig {
31
32
  maxIpsCount: number;
32
33
  };
33
34
  logger: ConfigLogger;
35
+ locale: ConfigLocale;
34
36
  //
35
37
  modules: IBeanScopeConfig;
36
38
  }
@@ -2,13 +2,20 @@ import type { ILocaleRecord } from '../../lib/bean/resource/locale/type.ts';
2
2
  import type { IBeanScopeLocale } from '../../lib/bean/type.ts';
3
3
  import type { PowerPartial } from '../utils/powerPartial.ts';
4
4
 
5
- // export const localeDefault = {
6
- // modules: {},
7
- // };
5
+ export interface ConfigLocale {
6
+ items: Partial<Record<keyof ILocaleRecord, string>>;
7
+ default: keyof ILocaleRecord;
8
+ }
8
9
 
9
10
  export interface VonaLocale {
10
11
  modules: IBeanScopeLocale;
11
12
  }
12
13
 
13
14
  export type VonaLocaleOptional = PowerPartial<VonaLocale>;
14
- export type VonaLocaleOptionalMap = Record<keyof ILocaleRecord, VonaLocaleOptional>;
15
+ export type VonaLocaleOptionalMap = Partial<Record<keyof ILocaleRecord, VonaLocaleOptional>>;
16
+
17
+ // Only capability.locale modules interpret this envelope as cross-module resources.
18
+ export interface ILocaleProviderResource {
19
+ modules?: Record<string, Record<string, string>>;
20
+ [key: string]: string | Record<string, Record<string, string>> | undefined;
21
+ }
@@ -17,6 +17,7 @@ export interface IModuleResource {
17
17
  Main: new () => IModuleMain;
18
18
  Monkey: new () => IMonkeyModule & IMonkeySystem;
19
19
  locales: TypeModuleResourceLocales;
20
+ localesProvider: TypeModuleResourceLocales;
20
21
  errors: TypeModuleResourceErrors;
21
22
  config: TypeModuleResourceConfig;
22
23
  constants: unknown;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-mock",
3
- "version": "6.1.51",
3
+ "version": "6.1.52",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona mock",
6
6
  "keywords": [
@@ -39,6 +39,15 @@ export default async function (app: VonaApplication) {
39
39
  mode: env.META_MODE,
40
40
  };
41
41
 
42
+ // Language availability is application policy, independent of installed dictionaries.
43
+ config.locale = {
44
+ items: {
45
+ 'en-us': 'LanguageEnglish',
46
+ 'zh-cn': 'LanguageChinese',
47
+ },
48
+ default: 'en-us',
49
+ };
50
+
42
51
  // instance
43
52
  config.instance = {
44
53
  getInstanceName: undefined,
@@ -10,10 +10,7 @@ export class Main extends BeanSimple implements IModuleMain {
10
10
  'en-us': en,
11
11
  'zh-cn': zhCN,
12
12
  };
13
- this.app.util.setLocaleErrors(
14
- localeErrors,
15
- this.app.scope('a-locale').config.locale.defaultLocale,
16
- );
13
+ this.app.util.setLocaleErrors(localeErrors, this.app.config.locale.default);
17
14
  }
18
15
 
19
16
  async configLoaded(_config: any) {}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-rbac",
3
- "version": "5.0.16",
3
+ "version": "5.0.17",
4
4
  "description": "",
5
5
  "keywords": [
6
6
  "Vona Module"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-suite-a-cabloy",
3
- "version": "5.1.68",
3
+ "version": "5.1.69",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "author": "",
@@ -10,7 +10,7 @@
10
10
  "vona-module-a-cabloy": "workspace:^",
11
11
  "vona-module-a-layoutprofile": "^5.0.2",
12
12
  "vona-module-a-markdown": "^5.0.3",
13
- "vona-module-a-rbac": "^5.0.16",
13
+ "vona-module-a-rbac": "^5.0.17",
14
14
  "vona-module-a-socket": "workspace:^",
15
15
  "vona-module-a-ssr": "workspace:^",
16
16
  "vona-module-a-ssrhmr": "workspace:^"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-core",
3
- "version": "5.1.67",
3
+ "version": "5.1.68",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-hmr",
3
- "version": "5.1.14",
3
+ "version": "5.1.15",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "keywords": [
@@ -15,6 +15,7 @@ const __pathesWatch = [
15
15
  'src/backend/config/config/*.tsx',
16
16
  '**/src/config/errors.ts',
17
17
  '**/src/config/locale/*.ts',
18
+ '**/src/locales/*.ts',
18
19
  '**/src/config/config.ts',
19
20
  '**/src/config/constants.ts',
20
21
  '**/src/controller/*.ts(x)?',
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-locale",
3
- "version": "5.1.12",
3
+ "version": "5.1.13",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "keywords": [
@@ -1,13 +1,12 @@
1
- import type { ILocaleRecord, IProtocolKeyRecord, VonaApplication } from 'vona';
1
+ import type { IProtocolKeyRecord, VonaApplication } from 'vona';
2
2
 
3
3
  import { $protocolKey } from 'vona';
4
4
 
5
5
  export interface I18nConfigLocale {
6
- defaultLocale: keyof ILocaleRecord;
7
6
  queryField?: keyof IProtocolKeyRecord;
8
7
  headerField?: keyof IProtocolKeyRecord;
9
8
  cookieField?: string;
10
- localeAlias: any;
9
+ localeAlias: Record<string, string>;
11
10
  writeCookie: boolean;
12
11
  cookieMaxAge: number;
13
12
  cookieDomain?: string;
@@ -31,7 +30,6 @@ export interface I18nConfig {
31
30
  export function config(_app: VonaApplication) {
32
31
  return {
33
32
  locale: {
34
- defaultLocale: 'en-us',
35
33
  queryField: $protocolKey('x-vona-locale'),
36
34
  headerField: $protocolKey('x-vona-locale'),
37
35
  cookieField: 'locale',
@@ -1,6 +1,6 @@
1
1
  import type { IModuleMain, VonaContext } from 'vona';
2
2
 
3
- import { BeanSimple } from 'vona';
3
+ import { BeanSimple, resolveEnabledLocale, resolveLocale, validateLocaleConfig } from 'vona';
4
4
 
5
5
  import type { I18nConfigLocale, I18nConfigTz } from './config/config.ts';
6
6
 
@@ -15,11 +15,12 @@ export class Main extends BeanSimple implements IModuleMain {
15
15
  async moduleLoading() {}
16
16
  async moduleLoaded() {
17
17
  const options = this.app.scope(__ThisModule__).config;
18
+ validateLocaleConfig(this.app.config.locale);
18
19
  this.app.context.__getLocale = function (this: VonaContext) {
19
20
  return __getLocale(this, options.locale);
20
21
  };
21
22
  this.app.context.__setLocale = function (this: VonaContext, locale: string) {
22
- return __setLocale(this, locale);
23
+ return __setLocale(this, locale, options.locale);
23
24
  };
24
25
  this.app.context.__getTz = function (this: VonaContext) {
25
26
  return __getTz(this, options.tz);
@@ -32,8 +33,8 @@ export class Main extends BeanSimple implements IModuleMain {
32
33
  async configLoaded(_config: any) {}
33
34
  }
34
35
 
35
- function __setLocale(ctx: VonaContext, locale: string) {
36
- ctx[SymbolLocale] = locale;
36
+ function __setLocale(ctx: VonaContext, locale: string, options: I18nConfigLocale) {
37
+ ctx[SymbolLocale] = resolveLocale(locale, ctx.app.config.locale, options.localeAlias);
37
38
  ctx[SymbolLocaleOrigin] = 'set';
38
39
  }
39
40
 
@@ -42,7 +43,7 @@ function __getLocale(ctx: VonaContext, options: I18nConfigLocale) {
42
43
  return ctx[SymbolLocale];
43
44
  }
44
45
 
45
- const localeNames = Object.keys(ctx.app.meta.locales);
46
+ const policy = ctx.app.config.locale;
46
47
 
47
48
  const queryField = options.queryField;
48
49
  const headerField = options.headerField;
@@ -89,9 +90,10 @@ function __getLocale(ctx: VonaContext, options: I18nConfigLocale) {
89
90
  }
90
91
  if (languages.length > 0) {
91
92
  for (let i = 0; i < languages.length; i++) {
92
- const lang = formatLocale(languages[i]);
93
- if (localeNames.includes(lang) || options.localeAlias[lang]) {
94
- locale = lang;
93
+ const lang = resolveEnabledLocale(languages[i], policy, options.localeAlias);
94
+ if (lang) {
95
+ // Keep the input until final resolution so an alias is applied only once.
96
+ locale = languages[i];
95
97
  localeOrigin = 'header';
96
98
  break;
97
99
  }
@@ -104,23 +106,8 @@ function __getLocale(ctx: VonaContext, options: I18nConfigLocale) {
104
106
  }
105
107
  }
106
108
 
107
- // all missing, set it to defaultLocale
108
- if (!locale) {
109
- locale = options.defaultLocale;
110
- localeOrigin = 'default';
111
- }
112
-
113
- // cookie alias
114
- if ((locale as string) in options.localeAlias) {
115
- locale = options.localeAlias[locale as string];
116
- }
117
-
118
- locale = formatLocale(locale);
119
-
120
- // validate locale
121
- if (!localeNames.includes(locale)) {
122
- locale = options.defaultLocale;
123
- }
109
+ if (!locale) localeOrigin = 'default';
110
+ locale = resolveLocale(locale, policy, options.localeAlias);
124
111
 
125
112
  // if header not send, set the locale cookie
126
113
  if (cookieField && options.writeCookie && cookieLocale !== locale && !ctx.headerSent) {
@@ -217,11 +204,6 @@ function updateCookie(ctx: VonaContext, options: I18nConfigLocale | I18nConfigTz
217
204
  ctx.cookies.set(options.cookieField!, locale, cookieOptions);
218
205
  }
219
206
 
220
- function formatLocale(locale) {
221
- // support zh_CN, en_US => zh-CN, en-US
222
- return parseTokenSafe(locale).replace('_', '-').toLowerCase();
223
- }
224
-
225
207
  function parseTokenSafe(token?: string) {
226
208
  if (!token) return '';
227
209
  return token.replace(/[\\.*#%'"`;, ]/g, '');
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-module-a-openapi",
3
- "version": "5.1.26",
3
+ "version": "5.1.27",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "",
6
6
  "keywords": [