cabloy 5.1.240 → 5.1.242
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/.cabloy-version +1 -1
- package/CHANGELOG.md +24 -0
- package/package.json +1 -1
- package/repo-docs/backend/i18n-guide.md +109 -11
- package/repo-docs/backend/openapi-guide.md +18 -0
- package/repo-docs/frontend/environment-config-guide.md +43 -0
- package/repo-docs/frontend/module-scope.md +88 -0
- package/repo-docs/frontend/system-startup-guide.md +31 -0
- package/repo-e2e/specs/home-user-account.spec.ts +53 -8
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/pnpm-lock.yaml +17 -142
- package/vona/src/suite/a-commerce/modules/commerce-siteadmin/src/dto/operatorContext.ts +4 -1
- package/vona/src/suite/a-commerce/modules/commerce-siteadmin/src/service/operator.ts +1 -0
- package/vona/src/suite/a-commerce/modules/commerce-siteadmin/test/operatorAccess.test.ts +1 -0
- package/vona/src/suite/a-home/modules/home-user/src/bean/meta.version.ts +1 -0
- package/vona/src/suite/a-home/modules/home-user/src/controller/passport.ts +1 -0
- package/vona/src/suite/a-home/modules/home-user/src/dto/accountCurrent.ts +3 -0
- package/vona/src/suite/a-home/modules/home-user/src/dto/accountProfileUpdate.ts +9 -2
- package/vona/src/suite/a-home/modules/home-user/src/dto/passportUser.ts +3 -0
- package/vona/src/suite/a-home/modules/home-user/src/entity/user.ts +3 -0
- package/vona/src/suite/a-home/modules/home-user/src/service/account.ts +7 -3
- package/vona/src/suite/a-home/modules/home-user/src/service/userAdapter.ts +1 -0
- package/vona/src/suite/a-home/modules/home-user/test/account.test.ts +133 -8
- package/vona/src/suite/a-home/modules/home-user/test/accountOpenapi.test.ts +32 -5
- package/vona/src/suite/a-home/modules/home-user/test/passport.test.ts +159 -1
- package/vona/src/suite-vendor/a-cabloy/modules/a-rbac/package.json +1 -1
- package/vona/src/suite-vendor/a-cabloy/package.json +2 -2
- package/vona/src/suite-vendor/a-captcha/modules/a-captcha/package.json +1 -1
- package/vona/src/suite-vendor/a-captcha/modules/a-captcha/src/bean/bean.captcha.ts +271 -137
- package/vona/src/suite-vendor/a-captcha/modules/a-captcha/src/bean/cacheRedis.captcha.ts +199 -2
- package/vona/src/suite-vendor/a-captcha/modules/a-captcha/src/lib/captchaLua.ts +90 -0
- package/vona/src/suite-vendor/a-captcha/modules/a-captcha/src/types/captcha.ts +15 -1
- package/vona/src/suite-vendor/a-captcha/modules/a-captcha/src/types/captchaProvider.ts +38 -1
- package/vona/src/suite-vendor/a-captcha/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-permission/src/bean/bean.permission.ts +54 -8
- package/vona/src/suite-vendor/a-vona/modules/a-permission/test/permission.test.ts +12 -15
- package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-user/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-user/src/types/user.ts +1 -0
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
- package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/api/openapi/schemas.ts +68 -52
- package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/api/openapi/types.ts +530 -344
- package/zova/src/suite/a-commerce/modules/commerce-siteadmin/src/page/dashboard/controller.tsx +7 -1
- package/zova/src/suite/a-home/modules/home-api/src/api/openapi/types.ts +6 -1
- package/zova/src/suite/a-home/modules/home-base/src/config/locale/en-us.ts +1 -0
- package/zova/src/suite/a-home/modules/home-base/src/config/locale/zh-cn.ts +1 -0
- package/zova/src/suite/a-home/modules/home-layoutadmin/src/component/layoutAdmin/render.user.tsx +1 -1
- package/zova/src/suite/a-home/modules/home-layoutweb/src/component/layoutWeb/render.user.tsx +3 -1
- package/zova/src/suite/a-home/modules/home-passport/src/model/passport.ts +4 -0
- package/zova/src/suite/a-home/modules/home-user/src/config/locale/en-us.ts +1 -0
- package/zova/src/suite/a-home/modules/home-user/src/config/locale/zh-cn.ts +1 -0
- package/zova/src/suite/a-home/modules/home-user/src/model/account.ts +2 -1
- package/zova/src/suite/a-home/modules/home-user/src/page/account/controller.tsx +17 -3
- package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/test/aggregate.test.ts +0 -247
package/.cabloy-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
5.1.
|
|
1
|
+
5.1.242
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.1.242
|
|
4
|
+
|
|
5
|
+
### Bug Fixes
|
|
6
|
+
|
|
7
|
+
- Correct test failures.
|
|
8
|
+
|
|
9
|
+
### Improvements
|
|
10
|
+
|
|
11
|
+
- Enhance CAPTCHA functionality and expand test coverage.
|
|
12
|
+
- Clarify locale provider examples and metadata commands in the documentation.
|
|
13
|
+
- Update dependency lockfile.
|
|
14
|
+
|
|
15
|
+
## 5.1.241
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
- Display the user's display name.
|
|
20
|
+
- Add MCP permission support.
|
|
21
|
+
- Add guard-only permission checks.
|
|
22
|
+
|
|
23
|
+
### Improvements
|
|
24
|
+
|
|
25
|
+
- Update the OpenAPI guide.
|
|
26
|
+
|
|
3
27
|
## 5.1.240
|
|
4
28
|
|
|
5
29
|
### Improvements
|
package/package.json
CHANGED
|
@@ -28,7 +28,51 @@ Representative files include:
|
|
|
28
28
|
- `src/suite/a-training/modules/training-student/src/config/locale/en-us.ts`
|
|
29
29
|
- `src/suite/a-training/modules/training-student/src/config/locale/zh-cn.ts`
|
|
30
30
|
|
|
31
|
-
This makes localization part of the module model rather than a single global string table.
|
|
31
|
+
This makes localization part of the module model rather than a single global string table. Ordinary dictionaries are exported through `resource.locales`; metadata generation continues to read `src/config/locale`.
|
|
32
|
+
|
|
33
|
+
### Cross-module language providers
|
|
34
|
+
|
|
35
|
+
A language provider supplies translations for existing target modules without taking ownership of their message keys. The framework supports provider modules on both Vona and Zova; the Japanese (`ja-jp`) resources below illustrate an application-authored provider, not a bundled language pack. Author or install the providers your application needs. Installing a provider supplies resources; it does **not** enable that language in application policy.
|
|
36
|
+
|
|
37
|
+
A Vona provider declares `vonaModule.capabilities.locale: true` in its `package.json` and authors a separate export:
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
// src/locales/ja-jp.ts
|
|
41
|
+
export default {
|
|
42
|
+
modules: {
|
|
43
|
+
'training-student': {
|
|
44
|
+
StudentName: '受講生氏名',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// src/locales/index.ts
|
|
52
|
+
import jaJP from './ja-jp.ts';
|
|
53
|
+
|
|
54
|
+
export const localesProvider = {
|
|
55
|
+
'ja-jp': jaJP,
|
|
56
|
+
};
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The module's `src/index.ts` re-exports `localesProvider` from `./locales/index.ts`. Larger dictionaries can compose authored helper files such as `src/lib/locales/training-student.ts`.
|
|
60
|
+
|
|
61
|
+
The `modules` envelope is structural: its keys identify target modules, not a new business scope owned by the provider. Only targets present in effective module metadata receive contributions. Optional top-level **string** entries are global messages; `modules` itself is never a text key. Providers need no blanket dependencies on translated targets and must not import those targets merely to contribute text.
|
|
62
|
+
|
|
63
|
+
For a target's scoped key, registration precedence is **project target override → provider contribution → ordinary target dictionary**. Global collisions retain stable first-registration-wins behavior; scoped lookup takes priority over global lookup. Avoid relying on collisions between independent providers as a product customization mechanism.
|
|
64
|
+
|
|
65
|
+
### Generation and reload boundaries
|
|
66
|
+
|
|
67
|
+
From the repository root, refresh metadata for an existing module, replacing `<module>` with its relative module name:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npm run vona :tools:metadata <module>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Metadata generation still handles ordinary `src/config/locale` resources; it neither discovers nor generates the provider's `src/locales` map. Keep `localesProvider` and its entrypoint export authored. Do not add dummy provider `en-us` files or expect English-base key/scope inference for provider-owned resources. `:init:locale` initializes **ordinary** locale resources, not a provider scaffold. Do not hand-edit generated consumers to add provider dictionaries.
|
|
74
|
+
|
|
75
|
+
Provider files and imported helper edits/deletions take effect through the existing development reload/restart and application reinitialization path, not a new live registry API. Vona retains its `moduleLoading` → configuration/locales → `moduleLoaded` lifecycle; do not assume provider translations are available in an earlier `moduleLoading` hook. Frontend early registration and lazy-target behavior are described in [System Startup Guide](/frontend/system-startup-guide).
|
|
32
76
|
|
|
33
77
|
## Accessing locale resources
|
|
34
78
|
|
|
@@ -86,28 +130,57 @@ this.ctx.locale = 'en-us';
|
|
|
86
130
|
### Get default locale
|
|
87
131
|
|
|
88
132
|
```typescript
|
|
89
|
-
const localeDefault = this
|
|
133
|
+
const localeDefault = this.app.config.locale.default;
|
|
90
134
|
```
|
|
91
135
|
|
|
92
136
|
## Locale configuration and resolution order
|
|
93
137
|
|
|
94
|
-
The
|
|
138
|
+
The root application configuration owns the enabled languages and the sole configured default. For example, inside the existing root config factory in `vona/src/backend/config/config/config.ts`:
|
|
95
139
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
-
|
|
140
|
+
```typescript
|
|
141
|
+
config.locale = {
|
|
142
|
+
items: {
|
|
143
|
+
'en-us': 'LanguageEnglish',
|
|
144
|
+
'zh-cn': 'LanguageChinese',
|
|
145
|
+
'ja-jp': 'LanguageJapanese',
|
|
146
|
+
'zh-tw': 'LanguageTraditionalChinese',
|
|
147
|
+
},
|
|
148
|
+
default: 'en-us',
|
|
149
|
+
};
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
This is an explicit four-language policy example, not the ordinary Basic default: Basic enables `en-us`/`zh-cn` by default. Keep the matching frontend root policy aligned as described in [Environment and Config Guide](/frontend/environment-config-guide). Adding dictionaries or extending `ILocaleRecord` alone does not enable a language.
|
|
153
|
+
|
|
154
|
+
A later configuration layer's `locale.items` **replaces** the earlier enabled set; it does not add languages to it. The final map must be nonempty, have canonical lowercase hyphenated locale keys and string label values, and contain `locale.default` as an own key. Invalid policy fails validation rather than silently inventing a language set.
|
|
155
|
+
|
|
156
|
+
The `a-locale` module retains transport settings such as `queryField`, `headerField`, `cookieField`, cookie-write options and `localeAlias`. It is not the owner of enabled/default policy.
|
|
100
157
|
|
|
101
|
-
|
|
158
|
+
### Migrating the old default setting
|
|
159
|
+
|
|
160
|
+
Remove obsolete `a-locale` `locale.defaultLocale` configuration and replace it with root `config.locale.default`; explicitly define the enabled `config.locale.items` set and align the frontend policy. Change reads of `this.$scope.locale.config.locale.defaultLocale` to `this.app.config.locale.default`. The old module setting is not a second default or a compatibility override. Existing transport and timezone settings remain under `a-locale`.
|
|
161
|
+
|
|
162
|
+
### Active selection versus message fallback
|
|
163
|
+
|
|
164
|
+
The current locale takes the first nonempty input in this order:
|
|
102
165
|
|
|
103
166
|
- query field
|
|
104
167
|
- header field
|
|
105
168
|
- cookie field
|
|
106
169
|
- user locale
|
|
107
|
-
- `Accept-Language`
|
|
108
|
-
- default
|
|
170
|
+
- `Accept-Language` (the first supported candidate)
|
|
171
|
+
- configured root default
|
|
172
|
+
|
|
173
|
+
Selection normalizes input by trimming, replacing underscores with hyphens, and lowercasing. Aliases are resolved against the enabled own keys; an alias cannot enable a disabled language. Unknown or disabled input resolves to the configured default. In particular, an explicit but disabled query/header/cookie value does not bypass policy by falling through to another language source. Assigning `ctx.locale` uses the same enabled-policy boundary.
|
|
174
|
+
|
|
175
|
+
An explicit low-level lookup such as `StudentName.locale('ja-jp')` is different: it can read installed resources without activating Japanese for requests or language menus. Missing-message lookup follows this sequence:
|
|
176
|
+
|
|
177
|
+
1. requested-locale target dictionary;
|
|
178
|
+
2. requested-locale global dictionary;
|
|
179
|
+
3. `en-us` target dictionary;
|
|
180
|
+
4. `en-us` global dictionary;
|
|
181
|
+
5. the original message key.
|
|
109
182
|
|
|
110
|
-
|
|
183
|
+
The configured default controls **active-language selection**, not this missing-text fallback. There is no implicit `zh-tw` → `zh-cn` fallback. Keep missing text, disabled selection and uninstalled resources distinct when diagnosing a result.
|
|
111
184
|
|
|
112
185
|
## Adding a new language
|
|
113
186
|
|
|
@@ -218,6 +291,31 @@ this.scope.locale.NameApples_.locale('en-us', 'Mike', 2);
|
|
|
218
291
|
|
|
219
292
|
This is exact numeric-value selection, not automatic `Intl.PluralRules` or CLDR plural-category resolution. It makes common plural handling part of the localization model while allowing each locale to define its own wording.
|
|
220
293
|
|
|
294
|
+
## Text, formatting, and validation boundaries
|
|
295
|
+
|
|
296
|
+
A language pack provides text, not a general date/number/currency formatting redesign. The printf placeholders and exact numeric suffixes above do not supply locale-aware decimal separators, currency rules or calendar formatting. Use the application's existing formatter (for example, `Intl.NumberFormat` or a date library) with deliberate locale/timezone options where those semantics are required. Request timezone and active language are related inputs, not interchangeable values.
|
|
297
|
+
|
|
298
|
+
Cabloy Basic keeps its existing `home-base` Zod registrations for `en-us` → `en` and `zh-cn` → `zhCN`; installing Japanese or Traditional Chinese dictionaries does not add Zod error locales. In a real business application, import and register the Zod resources corresponding to the languages that application enables, on both backend and frontend.
|
|
299
|
+
|
|
300
|
+
For example, if the business application enables all four languages, extend its existing `home-base` adapters with:
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
import { en, ja, zhCN, zhTW } from 'zod/locales';
|
|
304
|
+
|
|
305
|
+
const localeErrors = {
|
|
306
|
+
'en-us': en,
|
|
307
|
+
'zh-cn': zhCN,
|
|
308
|
+
'ja-jp': ja,
|
|
309
|
+
'zh-tw': zhTW,
|
|
310
|
+
};
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
In the backend adapter's `moduleLoaded`, register the map with `this.app.util.setLocaleErrors(localeErrors, this.app.config.locale.default)`. In the frontend adapter, keep registration inside the existing `if (process.env.CLIENT)` boundary and call `this.app.util.setLocaleErrors(localeErrors, this.sys.config.locale.default)`. Import only the resources the business application's enabled policy requires; this example is not a request to change Basic's default registrations.
|
|
314
|
+
|
|
315
|
+
Dictionary text and Zod error text come from different owners; do not duplicate vendor validation messages inside a provider. Frontend registration is client-only, so this mapping alone does not promise identical SSR validation execution. See [Validation Guide](/backend/validation-guide) and [Zod Guide](/frontend/zod-guide).
|
|
316
|
+
|
|
317
|
+
Localized OpenAPI metadata remains backend contract truth. Frontend `$apiSchema` and `ModelResource` projections are locale-scoped reactive state: reacquire the current facade or derived schema after a locale change instead of retaining a snapshot from the previous locale. Compatible ordinary dirty fields can survive schema refresh, while an opaque custom renderer with translated options may reset to its server baseline; do not promise universal input preservation. See [API Schema Guide](/frontend/api-schema-guide) and [Model Resource Owner Pattern](/frontend/model-resource-owner-pattern).
|
|
318
|
+
|
|
221
319
|
## Timezone support
|
|
222
320
|
|
|
223
321
|
Vona also tracks timezone in the request context.
|
|
@@ -48,6 +48,24 @@ Representative capabilities include generating JSON for:
|
|
|
48
48
|
|
|
49
49
|
This is important because OpenAPI is treated as a first-class framework service, not as an external afterthought.
|
|
50
50
|
|
|
51
|
+
## MCP mapping and normal ingress admission
|
|
52
|
+
|
|
53
|
+
In Cabloy Basic, method/class `@mcp.tool()` metadata opts registered Controller Actions into MCP mapping using their existing request/response contracts. It is independent of ordinary OpenAPI publication: `@Api.exclude()` is not an MCP authorization boundary, and publishing an HTTP action does not opt it into MCP. Decorator opt-in alone does **not** expose an action on normal `/mcp`.
|
|
54
|
+
|
|
55
|
+
Normal shared ingress is owned by `cabloy-mcp` and accepts recognized dev/test/prod modes through the same OAuth and business-authorization boundary. Production is not rejected solely by mode; unknown modes, not-ready, closing, disabled or invalid required authority configuration still fail closed. Dedicated protected audit, audit-delivery gating and deployment-wide fine-grained quotas are deferred for this two-read MVP, not delivered capabilities or production prerequisites. Enabled-by-default behavior is unchanged: only literal `CABLOY_MCP_ENABLED=false` disables it. Construction admission permits exactly `training-student.controller.student` actions `lookup` and `summary`, for both listing and known-name direct calls. Optional training absent means an empty catalog, without fallback. The three synthetic catalog tools and anonymous standalone entry belong only to the `mcp-test` launcher, not shared `/mcp`.
|
|
56
|
+
|
|
57
|
+
OAuth scope does not grant business authority. Lookup and summary retain independent student select/view permissions, current-instance reads and minimized output; summary's six-field projection does not change its HTTP DTO. Calls retain ordinary Action guards with `innerAccess: false`.
|
|
58
|
+
|
|
59
|
+
Repository-relative source pointers:
|
|
60
|
+
|
|
61
|
+
- Opt-in metadata: `vona/src/suite-vendor/a-vona/modules/a-openapiutils/src/lib/decorator/mcp.ts`
|
|
62
|
+
- Contract mapper: `vona/src/suite/cabloy-mcp/modules/cabloy-mcp/src/lib/controllerTools.ts`
|
|
63
|
+
- Normal ingress middleware: `vona/src/suite/cabloy-mcp/modules/cabloy-mcp/src/bean/middlewareSystem.mcp.ts`
|
|
64
|
+
- Ingress checks: `vona/src/suite/cabloy-mcp/modules/cabloy-mcp/src/lib/mcpTransport.ts`
|
|
65
|
+
- Authenticated exchange and settlement: `vona/src/suite/cabloy-mcp/modules/cabloy-mcp/src/lib/businessTransport.ts`
|
|
66
|
+
- Exact admission and discovery: `vona/src/suite/cabloy-mcp/modules/cabloy-mcp/src/lib/sharedBusinessTools.ts`
|
|
67
|
+
- Application-close drain: `vona/src/suite/cabloy-mcp/modules/cabloy-mcp/src/monkey.ts`
|
|
68
|
+
|
|
51
69
|
## Validation and OpenAPI are linked
|
|
52
70
|
|
|
53
71
|
A major theme is that the same validation-oriented declaration surface also drives OpenAPI metadata generation.
|
|
@@ -99,6 +99,49 @@ A representative SSR admin development stack looks like:
|
|
|
99
99
|
|
|
100
100
|
The config side follows the same merge pattern with `config.ts`, `config.[meta].ts`, and `.local` variants.
|
|
101
101
|
|
|
102
|
+
## Locale enablement and defaults
|
|
103
|
+
|
|
104
|
+
Installed dictionaries, enabled languages, and the default language are separate concerns. Authoring or installing a language provider makes its translation resources available; it does not add a language choice or change the default. The Japanese and Traditional Chinese entries below illustrate application policy, not bundled language packs. Configure application policy explicitly in the frontend root config, `zova/src/front/config/config/config.ts`, or an applicable config layer:
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
import type { ZovaConfigOptional, ZovaSys } from 'zova';
|
|
108
|
+
|
|
109
|
+
export default function (_sys: ZovaSys) {
|
|
110
|
+
const config: ZovaConfigOptional = {};
|
|
111
|
+
|
|
112
|
+
config.locale = {
|
|
113
|
+
default: 'ja-jp',
|
|
114
|
+
storeKey: 'locale',
|
|
115
|
+
items: {
|
|
116
|
+
'en-us': 'LanguageEnglish',
|
|
117
|
+
'ja-jp': 'LanguageJapanese',
|
|
118
|
+
'zh-cn': 'LanguageChinese',
|
|
119
|
+
'zh-tw': 'LanguageTraditionalChinese',
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return config;
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
This is a locale-policy example, not a replacement for the other settings in an existing config file. Additional locale IDs need reachable `ILocaleRecord` declarations, supplied by installed modules or authored interface merging. A type declaration alone neither supplies translations nor enables a language.
|
|
128
|
+
|
|
129
|
+
- `items` is the enabled locale-code-to-label-key map. The values are message keys used by language selectors, not dictionaries or package names. A configured subset is valid; it need not contain every language declared in `ILocaleRecord`.
|
|
130
|
+
- `default` is the application default and must be an own key of the nonempty `items` map. Invalid policy fails validation rather than being repaired from installed providers.
|
|
131
|
+
- `storeKey` is the frontend locale cookie key. Keep it deliberate when changing preference compatibility; it does not determine which languages are enabled. SSR cookie access still follows the effective SSR profile.
|
|
132
|
+
|
|
133
|
+
Locale IDs in policy use lowercase, hyphenated form such as `ja-jp` and `zh-tw`. Active-locale input is normalized, so `JA_JP` can resolve to enabled `ja-jp`. Missing, unknown, or disabled selections fall back to `config.locale.default`, including stale saved preferences. Route-specific language menus can narrow the root enabled set, not expand it. This policy governs active-language selection, not an explicit low-level translation lookup such as `this.scope.locale.Name.locale('en-us')`.
|
|
134
|
+
|
|
135
|
+
Unlike ordinary additive config merging, **each config layer that supplies `locale.items` replaces the entire enabled map**. For example, a later layer with only `items: { 'ja-jp': 'LanguageJapanese' }` removes earlier English and Chinese choices. It must also leave `default` valid, either by retaining an already enabled default or setting `default: 'ja-jp'`. To add one language while preserving others, specify the complete desired map in that layer.
|
|
136
|
+
|
|
137
|
+
An enabled language may have no installed provider; missing text then uses the normal `en-us` / original-key fallback. Conversely, an installed but disabled language remains unavailable for active selection. Neither case is proof of translation completeness. The configured default does not change missing-message fallback; see [Module Scope](/frontend/module-scope#override-precedence-and-missing-message-fallback).
|
|
138
|
+
|
|
139
|
+
### Configure frontend and backend roots separately
|
|
140
|
+
|
|
141
|
+
For a paired application, also configure Vona's root `config.locale.items` and `config.locale.default` in `vona/src/backend/config/config/config.ts` or its applicable config layer. Keep the deployed frontend and backend policies aligned deliberately: neither stack automatically synchronizes its enabled set or default to the other. `APP_LOCALE_DEFAULT` supplies the frontend default only where the frontend config reads it; it does not enable the language or configure the backend root.
|
|
142
|
+
|
|
143
|
+
The backend default belongs to root `config.locale.default`, not the former module-level `a-locale` `defaultLocale`. Backend negotiation, aliases, cookie/protocol settings, and migration details are covered in [I18n Guide](/backend/i18n-guide). Providers contain shared static text, while the selected language remains app/request-scoped; language policy is not an authorization or tenant boundary.
|
|
144
|
+
|
|
102
145
|
## Flavor-aware capability differences
|
|
103
146
|
|
|
104
147
|
Different SSR flavors can intentionally expose different runtime capabilities rather than behaving identically.
|
|
@@ -85,6 +85,94 @@ const message2 = this.scope.locale.HelloWorld.locale('en-us');
|
|
|
85
85
|
|
|
86
86
|
Project-level locale resources can override module-level locale values, which is one of the key reasons scope stays useful even when the final app wants customized wording.
|
|
87
87
|
|
|
88
|
+
### Ordinary dictionaries and language providers
|
|
89
|
+
|
|
90
|
+
Frontend locale resources have two distinct authoring paths:
|
|
91
|
+
|
|
92
|
+
| Resource | Authored source | Module resource export | Purpose |
|
|
93
|
+
| -------------------------- | ---------------------------------------------------- | -------------------------- | -------------------------------------------------------- |
|
|
94
|
+
| Ordinary module dictionary | `src/config/locale/<locale>.ts` | Generated `locales` | Define the module's own message keys and default wording |
|
|
95
|
+
| Language provider | `src/locales/<locale>.ts` and `src/locales/index.ts` | Authored `localesProvider` | Supply translations for multiple target modules |
|
|
96
|
+
|
|
97
|
+
For example, `training-student` retains ownership of keys such as `Name` and their business meaning. An application-authored Japanese provider translates those keys; it does not acquire the student module's business ownership or generate its own `scope.locale.Name()`. The provider below is an authoring example, not a bundled language pack.
|
|
98
|
+
|
|
99
|
+
A provider declares its capability in the module's `package.json`:
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{
|
|
103
|
+
"zovaModule": {
|
|
104
|
+
"capabilities": {
|
|
105
|
+
"locale": true
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Provider resources use a `modules` envelope containing flat dictionaries keyed by target-module relative names:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
// Provider src/locales/ja-jp.ts
|
|
115
|
+
export default {
|
|
116
|
+
modules: {
|
|
117
|
+
'training-student': {
|
|
118
|
+
Name: '名前',
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Author the locale-code map explicitly and re-export it from the module entrypoint:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
// Provider src/locales/index.ts
|
|
128
|
+
import jaJp from './ja-jp.ts';
|
|
129
|
+
|
|
130
|
+
export const localesProvider = {
|
|
131
|
+
'ja-jp': jaJp,
|
|
132
|
+
};
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
// Provider src/index.ts; retain other existing exports
|
|
137
|
+
export { localesProvider } from './locales/index.ts';
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`modules` is resource structure, not a translatable message key. The runtime registers this example under `training-student`, not under the provider's own scope. Providers can also contribute top-level global strings, but global resources and scoped dictionaries occupy different lookup levels; a same-named global key is not a universal override of every module.
|
|
141
|
+
|
|
142
|
+
Larger providers can compose per-target files from `src/lib/locales/<target-module>.ts`, importing them into `src/locales/ja-jp.ts` with the actual `.ts` source suffix. Helper filenames are not locale IDs. Author or install the provider modules your application needs; framework support alone does not supply Japanese or Traditional Chinese dictionaries. Installing a provider does not enable its language; see [Locale enablement and defaults](/frontend/environment-config-guide#locale-enablement-and-defaults).
|
|
143
|
+
|
|
144
|
+
### Override precedence and missing-message fallback
|
|
145
|
+
|
|
146
|
+
For the same target module, language, and message key, precedence is:
|
|
147
|
+
|
|
148
|
+
```text
|
|
149
|
+
Project target-module override > provider > ordinary target dictionary
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Author project overrides in `zova/src/front/config/locale/<locale>.ts`, again using `modules: { 'training-student': { ... } }`, and include the locale in `zova/src/front/config/locales.ts`. Override only the messages the application needs to customize, rather than copying a whole provider. When providers contribute the same key, the first registration in stable module order wins; use project overrides for application-specific wording instead of relying on provider collisions.
|
|
153
|
+
|
|
154
|
+
Override precedence is separate from missing-message lookup. Ordinary text lookup tries:
|
|
155
|
+
|
|
156
|
+
```text
|
|
157
|
+
Target current language → global current language → target en-us → global en-us → original key
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`config.locale.default` selects the active language when a selection is absent or invalid. It **does not replace `en-us` as the missing-message fallback**. For example, choosing `ja-jp` as the default does not make a missing `zh-tw` message fall back to Japanese, nor is there an implicit `zh-tw` to `zh-cn` fallback.
|
|
161
|
+
|
|
162
|
+
### CLI and metadata boundaries
|
|
163
|
+
|
|
164
|
+
From the repository root, `npm run zova :init:locale <module>` initializes ordinary `src/config/locale/` resources and refreshes module metadata by default. It is not a generator for the provider layout above.
|
|
165
|
+
|
|
166
|
+
`npm run zova :tools:metadata <module>` scans only `src/config/locale/*.ts` for ordinary locale generation, producing `src/.metadata/locales.ts` and the target module's scope types. It does not scan provider `src/locales/` or `src/lib/locales/`, or infer provider-owned message keys from a `modules` envelope. The provider locale map and entrypoint `localesProvider` export remain authored source, preserved by normal metadata refresh. A single-language provider needs no dummy `en-us.ts` for type generation.
|
|
167
|
+
|
|
168
|
+
When adding an ordinary message key, update the target module's ordinary dictionaries first and refresh that target's metadata. Do not hand-edit generated files or bypass missing keys with type assertions. For language type declarations, placeholders, and numeric variants, see [I18n Guide](/backend/i18n-guide). Frontend declarations extend `ILocaleRecord` in `zova` and must remain reachable through the module's authored type exports.
|
|
169
|
+
|
|
170
|
+
### Text translation and formatting limits
|
|
171
|
+
|
|
172
|
+
Providers supply static text translations, not automatic translation of hard-coded strings or complete date, number, and currency localization. Text placeholders and exact numeric variants share the backend conventions described in [Formatting locale text](/backend/i18n-guide#formatting-locale-text); frontend code does not need another message-format system.
|
|
173
|
+
|
|
174
|
+
The existing `basic-date` utility uses Luxon `format` / `preset` options, and `basic-currency` uses separate `CurrencyOptions`. These utilities do not uniformly receive locale policy from `app.meta.locale.current`. Inspect the specific component and formatter when regional date, number, currency, or timezone presentation is required. Installing a provider or setting a default language alone does not complete that formatting work.
|
|
175
|
+
|
|
88
176
|
### Diagnose locale typing at its source
|
|
89
177
|
|
|
90
178
|
Use `this.scope.locale.SomeKey()` directly. Its type is generated from the module locale source and module metadata; do not add a local type assertion, compatibility helper, or cast merely to silence a missing-key error.
|
|
@@ -139,6 +139,37 @@ A representative lifecycle interpretation is:
|
|
|
139
139
|
- `sysReady` for long-lived runtime behavior that depends on the system being fully operational
|
|
140
140
|
- `sysClose` for teardown of system-level resources
|
|
141
141
|
|
|
142
|
+
## Locale providers during system startup
|
|
143
|
+
|
|
144
|
+
Locale providers are static module resources registered at the Sys level, not dictionaries appended separately in each SSR request or app instance. The relevant initialization order is:
|
|
145
|
+
|
|
146
|
+
1. Initialize project locale overrides from the frontend locale map.
|
|
147
|
+
2. Merge and validate the root config, including enabled languages and the default.
|
|
148
|
+
3. Load startup module resources, including modules with `zovaModule.capabilities.locale: true`.
|
|
149
|
+
4. Install locale providers and register their `localesProvider` resources **before installing the first preload module**.
|
|
150
|
+
5. Continue with preload, monkey, and sync installation; on the server, install the remaining modules through the existing SSR path.
|
|
151
|
+
6. Run `sysInitialize`, `sysInitialized`, and `sysReady` after module initialization.
|
|
152
|
+
|
|
153
|
+
Resource import and module installation are different stages. Startup resources may already be imported together, but provider installation completes before preload installation begins. This makes provider text available at the preload consumer's supported initialization boundary without requiring page code or an app-level provider hook to register it again.
|
|
154
|
+
|
|
155
|
+
### Translation targets remain metadata-filtered
|
|
156
|
+
|
|
157
|
+
A provider's `modules` envelope names translation targets, not dependencies to import. Registration checks target membership in the active module metadata map:
|
|
158
|
+
|
|
159
|
+
- A target absent from that map, including a project-disabled module, is skipped.
|
|
160
|
+
- A target present in the map can receive translations even if it has not been imported or installed yet.
|
|
161
|
+
- Registering that dictionary does not import or install the target. Client-side lazy targets remain lazy; their later ordinary dictionaries cannot replace earlier provider values.
|
|
162
|
+
|
|
163
|
+
This does not mean all SSR modules are lazy: the existing server startup path imports and installs remaining modules independently of provider registration. Do not add blanket target dependencies or eager target imports merely to contribute translations. Providers themselves are selected by the locale capability and do not need `preload: true` just to register text.
|
|
164
|
+
|
|
165
|
+
Project overrides therefore precede providers, which precede ordinary target dictionaries. For the authoring layout, ownership rules, and exact fallback order, see [Module Scope](/frontend/module-scope#ordinary-dictionaries-and-language-providers). For language enablement, see [Environment and Config Guide](/frontend/environment-config-guide#locale-enablement-and-defaults). Resource registration does not enable a language or change the configured default.
|
|
166
|
+
|
|
167
|
+
### Development reload and reinitialization
|
|
168
|
+
|
|
169
|
+
Frontend provider changes use the existing full-reload / Sys-reinitialization path, including changes to imported per-target helper files and removal of contributions. Closing the Sys disposes its locale maps; initialization starts again with project overrides, providers, and ordinary dictionaries. This avoids retaining deleted keys through append-only merges.
|
|
170
|
+
|
|
171
|
+
Do not treat `localesProvider` as a live dynamic-registration API or mutate shared locale maps in app/request hooks. Changing source resources requires the normal reload or rebuild/restart lifecycle for the running mode. Switching the active language is a separate app-scoped operation over registered resources; it does not install new providers. Backend reload behavior and the shared resource conventions are described in [I18n Guide](/backend/i18n-guide).
|
|
172
|
+
|
|
142
173
|
## When to use system startup
|
|
143
174
|
|
|
144
175
|
Use system startup when:
|
|
@@ -383,7 +383,7 @@ test(
|
|
|
383
383
|
);
|
|
384
384
|
|
|
385
385
|
test(
|
|
386
|
-
'ATP-ACCOUNT-WEB-01:
|
|
386
|
+
'ATP-ACCOUNT-WEB-01: display name refreshes the Web menu without changing the login username',
|
|
387
387
|
{ tag: ['@account', '@web', '@flow'] },
|
|
388
388
|
async ({ page, request }, testInfo) => {
|
|
389
389
|
let account: RegisteredAccount | undefined;
|
|
@@ -424,14 +424,27 @@ test(
|
|
|
424
424
|
.poll(() => avatarPreview.evaluate(image => (image as HTMLImageElement).naturalWidth))
|
|
425
425
|
.toBeGreaterThan(0);
|
|
426
426
|
|
|
427
|
-
const
|
|
428
|
-
await
|
|
427
|
+
const username = page.getByRole('textbox', { name: 'User name', exact: true });
|
|
428
|
+
await expect(username).toHaveValue(account.username);
|
|
429
|
+
await expect(username).toHaveAttribute('readonly', '');
|
|
430
|
+
const displayName = page
|
|
429
431
|
.getByRole('group', { name: 'Display name', exact: true })
|
|
430
|
-
.getByRole('textbox')
|
|
431
|
-
|
|
432
|
+
.getByRole('textbox');
|
|
433
|
+
await expect(displayName).toHaveValue('');
|
|
434
|
+
const profileName = `e2e-fixture-home-account-profile-${testInfo.workerIndex}-${crypto.randomUUID()}`;
|
|
435
|
+
await displayName.fill(` ${profileName} `);
|
|
432
436
|
const timezone = page
|
|
433
437
|
.getByRole('group', { name: 'Time zone', exact: true })
|
|
434
438
|
.getByRole('textbox');
|
|
439
|
+
const inputWidths = await Promise.all(
|
|
440
|
+
[username, displayName, timezone].map(input =>
|
|
441
|
+
input.evaluate(element => element.getBoundingClientRect().width),
|
|
442
|
+
),
|
|
443
|
+
);
|
|
444
|
+
expect(inputWidths[0]).toBeGreaterThan(0);
|
|
445
|
+
for (const width of inputWidths.slice(1)) {
|
|
446
|
+
expect(width).toBeCloseTo(inputWidths[0], 0);
|
|
447
|
+
}
|
|
435
448
|
const browserTimezone = await page.evaluate(
|
|
436
449
|
() => Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
437
450
|
);
|
|
@@ -447,9 +460,21 @@ test(
|
|
|
447
460
|
const profileResponse = waitForApiResponse(page, 'PATCH', accountProfileApiPath);
|
|
448
461
|
const refreshedPassportResponse = waitForApiResponse(page, 'GET', passportCurrentApiPath);
|
|
449
462
|
await page.getByRole('button', { name: 'Save profile', exact: true }).click();
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
expect((await
|
|
463
|
+
const savedProfileResponse = await profileResponse;
|
|
464
|
+
expect(savedProfileResponse.ok()).toBeTruthy();
|
|
465
|
+
expect((await savedProfileResponse.json()).data).toMatchObject({
|
|
466
|
+
name: account.username,
|
|
467
|
+
displayName: profileName,
|
|
468
|
+
});
|
|
469
|
+
expect(savedProfileResponse.request().postDataJSON()).not.toHaveProperty('name');
|
|
470
|
+
const savedPassportResponse = await refreshedPassportResponse;
|
|
471
|
+
expect(savedPassportResponse.ok()).toBeTruthy();
|
|
472
|
+
expect((await savedPassportResponse.json()).data.user).toMatchObject({
|
|
473
|
+
name: account.username,
|
|
474
|
+
displayName: profileName,
|
|
475
|
+
});
|
|
476
|
+
await expect(username).toHaveValue(account.username);
|
|
477
|
+
await expect(displayName).toHaveValue(profileName);
|
|
453
478
|
await expect(page.getByText('Profile saved.', { exact: true })).toBeVisible();
|
|
454
479
|
await expect
|
|
455
480
|
.poll(() =>
|
|
@@ -473,6 +498,26 @@ test(
|
|
|
473
498
|
await expect(page).toHaveURL(accountPath);
|
|
474
499
|
await expect(page.getByRole('heading', { name: 'Account Settings' })).toBeVisible();
|
|
475
500
|
await expect(userMenu).not.toHaveAttribute('open', '');
|
|
501
|
+
await expect(username).toHaveValue(account.username);
|
|
502
|
+
await expect(displayName).toHaveValue(profileName);
|
|
503
|
+
const originalLogin = await loginAsAccount(request, account.username, account.password);
|
|
504
|
+
expect(String(originalLogin.id)).toBe(String(account.id));
|
|
505
|
+
|
|
506
|
+
await displayName.fill(' ');
|
|
507
|
+
const clearedProfileResponse = waitForApiResponse(page, 'PATCH', accountProfileApiPath);
|
|
508
|
+
const clearedPassportResponse = waitForApiResponse(page, 'GET', passportCurrentApiPath);
|
|
509
|
+
await page.getByRole('button', { name: 'Save profile', exact: true }).click();
|
|
510
|
+
const clearedProfile = await clearedProfileResponse;
|
|
511
|
+
expect(clearedProfile.ok()).toBeTruthy();
|
|
512
|
+
expect(clearedProfile.request().postDataJSON()).toMatchObject({ displayName: null });
|
|
513
|
+
const clearedProfileBody = (await clearedProfile.json()).data;
|
|
514
|
+
expect(clearedProfileBody.name).toBe(account.username);
|
|
515
|
+
expect(clearedProfileBody.displayName).toBeFalsy();
|
|
516
|
+
expect((await clearedPassportResponse).ok()).toBeTruthy();
|
|
517
|
+
await expect(displayName).toHaveValue('');
|
|
518
|
+
await expect(username).toHaveValue(account.username);
|
|
519
|
+
await expect(userMenu.locator('summary')).toContainText(account.username);
|
|
520
|
+
await expect(userMenu.locator('summary')).not.toContainText(profileName);
|
|
476
521
|
expect(pageErrors).toEqual([]);
|
|
477
522
|
expect(consoleErrors).toEqual([]);
|
|
478
523
|
} finally {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vona",
|
|
3
|
-
"version": "5.1.
|
|
3
|
+
"version": "5.1.121",
|
|
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": [
|