cabloy 5.1.241 → 5.1.243

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 (33) hide show
  1. package/.cabloy-version +1 -1
  2. package/.github/workflows/vona-test-sqlite3.yml +1 -1
  3. package/CHANGELOG.md +24 -0
  4. package/package.json +1 -1
  5. package/repo-docs/backend/demonstration-mode-guide.md +5 -2
  6. package/repo-docs/backend/i18n-guide.md +109 -11
  7. package/repo-docs/frontend/environment-config-guide.md +43 -0
  8. package/repo-docs/frontend/module-scope.md +88 -0
  9. package/repo-docs/frontend/system-startup-guide.md +31 -0
  10. package/vona/packages-cli/cli/package.json +1 -1
  11. package/vona/packages-cli/cli-set-api/package.json +1 -1
  12. package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.build.ts +11 -0
  13. package/vona/packages-cli/cli-set-api/test/binBuildFreshInputs.test.ts +152 -0
  14. package/vona/packages-vona/vona/package.json +1 -1
  15. package/vona/packages-vona/vona-core/package.json +1 -1
  16. package/vona/packages-vona/vona-core/src/lib/core/application.ts +2 -2
  17. package/vona/packages-vona/vona-mock/package.json +1 -1
  18. package/vona/pnpm-lock.yaml +1 -1
  19. package/vona/src/suite/a-demo/modules/demo-demonstration/src/bean/guard.demonstration.ts +1 -1
  20. package/vona/src/suite/a-demo/modules/demo-demonstration/test/guardDemonstration.test.ts +4 -3
  21. package/vona/src/suite-vendor/a-captcha/modules/a-captcha/package.json +1 -1
  22. package/vona/src/suite-vendor/a-captcha/modules/a-captcha/src/bean/bean.captcha.ts +271 -137
  23. package/vona/src/suite-vendor/a-captcha/modules/a-captcha/src/bean/cacheRedis.captcha.ts +199 -2
  24. package/vona/src/suite-vendor/a-captcha/modules/a-captcha/src/lib/captchaLua.ts +90 -0
  25. package/vona/src/suite-vendor/a-captcha/modules/a-captcha/src/types/captcha.ts +15 -1
  26. package/vona/src/suite-vendor/a-captcha/modules/a-captcha/src/types/captchaProvider.ts +38 -1
  27. package/vona/src/suite-vendor/a-captcha/package.json +1 -1
  28. package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
  29. package/vona/src/suite-vendor/a-vona/modules/a-permission/package.json +1 -1
  30. package/vona/src/suite-vendor/a-vona/modules/a-permission/test/permission.test.ts +1 -216
  31. package/vona/src/suite-vendor/a-vona/modules/a-ratelimit/package.json +1 -1
  32. package/vona/src/suite-vendor/a-vona/package.json +1 -1
  33. 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.241
1
+ 5.1.243
@@ -94,7 +94,7 @@ jobs:
94
94
  run: npm run init
95
95
  - name: run isolated test infrastructure gate
96
96
  # Bound concurrent PowerShell/Add-Type cold starts on Windows runners.
97
- run: node --test ${{ runner.os == 'Windows' && '--test-concurrency=2' || '' }} scripts/testClaimProcesses.test.ts scripts/runIsolatedTest.test.ts scripts/testClaimCleanup.test.ts scripts/testProcessGuardianWindows.test.ts vona/packages-vona/vona-core/src/lib/core/testClaim.test.ts vona/packages-vona/vona-core/src/lib/core/testClaimFilesystem.test.ts vona/packages-vona/vona-core/src/lib/framework/bootstrap.test.ts vona/packages-vona/vona-core/src/lib/framework/useApp.test.ts vona/packages-cli/cli-set-api/test/binBuildEntries.test.ts vona/packages-cli/cli-set-api/test/binTestPatterns.test.ts
97
+ run: node --test ${{ runner.os == 'Windows' && '--test-concurrency=2' || '' }} scripts/testClaimProcesses.test.ts scripts/runIsolatedTest.test.ts scripts/testClaimCleanup.test.ts scripts/testProcessGuardianWindows.test.ts vona/packages-vona/vona-core/src/lib/core/testClaim.test.ts vona/packages-vona/vona-core/src/lib/core/testClaimFilesystem.test.ts vona/packages-vona/vona-core/src/lib/framework/bootstrap.test.ts vona/packages-vona/vona-core/src/lib/framework/useApp.test.ts vona/packages-cli/cli-set-api/test/binBuildEntries.test.ts vona/packages-cli/cli-set-api/test/binBuildFreshInputs.test.ts vona/packages-cli/cli-set-api/test/binTestPatterns.test.ts
98
98
  - name: run A-Pay focused gate
99
99
  run: npm run test -- test-pay/test/paymentSession.test.ts test-pay/test/payMock.test.ts test-pay/test/paypalProvider.test.ts test-pay/test/paypalLifecycle.test.ts test-pay/test/stripeProvider.test.ts test-pay/test/stripeLifecycle.test.ts test-pay/test/outbox.test.ts test-pay/test/webhook.test.ts
100
100
  working-directory: vona
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.1.243
4
+
5
+ ### Bug Fixes
6
+
7
+ - Fix warning logging.
8
+ - Fix logout in demonstration mode.
9
+ - Fix Rollup build caching.
10
+
11
+ ### Improvements
12
+
13
+ - Update the demonstration mode guide.
14
+
15
+ ## 5.1.242
16
+
17
+ ### Bug Fixes
18
+
19
+ - Correct test failures.
20
+
21
+ ### Improvements
22
+
23
+ - Enhance CAPTCHA functionality and expand test coverage.
24
+ - Clarify locale provider examples and metadata commands in the documentation.
25
+ - Update dependency lockfile.
26
+
3
27
  ## 5.1.241
4
28
 
5
29
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cabloy",
3
- "version": "5.1.241",
3
+ "version": "5.1.243",
4
4
  "gitHead": "2c5c19284bab738e492856189acb6fad74b8a7b7",
5
5
  "description": "A Node.js fullstack framework",
6
6
  "keywords": [
@@ -80,10 +80,13 @@ The Guard depends on Passport-resolved account state, so it makes this decision
80
80
 
81
81
  ## Exempt authentication routes
82
82
 
83
- Demonstration Mode deliberately does not apply to the standard authentication and OAuth routes below. This keeps sign-in, sign-out, registration, account association and migration, OAuth callbacks, and token flows available while the write policy is active.
83
+ Demonstration Mode deliberately does not apply to the standard authentication and OAuth routes below. This keeps sign-in, registration, account association and migration, OAuth callbacks, and token flows available while the write policy is active.
84
+
85
+ In Cabloy Basic, and Cabloy Start, `POST /home/user/passport/logout` is **not exempt**. While Demonstration Mode is enabled, only authenticated, active, non-anonymous whitelisted users may log out; other users receive `403` with `demo-demonstration:1001`.
86
+
87
+ This prevents ordinary visitors sharing a demo account from revoking other visitors' logins. Whitelisted users remain able to log out. The underlying logout semantics are unchanged: logout revokes the shared login state for the current account and authentication record, not just the current browser session. When Demonstration Mode is disabled, this Guard does not restrict logout.
84
88
 
85
89
  ```text
86
- /home/user/passport/logout
87
90
  /home/user/passport/register
88
91
  /home/user/passport/login
89
92
  /home/user/passport/login/:module/:providerName/:clientName?
@@ -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.$scope.locale.config.locale.defaultLocale;
133
+ const localeDefault = this.app.config.locale.default;
90
134
  ```
91
135
 
92
136
  ## Locale configuration and resolution order
93
137
 
94
- The `a-locale` module exposes locale settings such as:
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
- - `defaultLocale`
97
- - `queryField`
98
- - `headerField`
99
- - `cookieField`
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
- The current locale is resolved in this order:
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 locale
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
- This is important because locale is not guessed from only one source.
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.
@@ -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:
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona-cli",
3
- "version": "1.1.173",
3
+ "version": "1.1.174",
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.171",
3
+ "version": "1.1.172",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona cli-set-api",
6
6
  "keywords": [
@@ -203,6 +203,13 @@ export class CliBinBuild extends BeanCliBase {
203
203
 
204
204
  const sourceMap = process.env.BUILD_SOURCEMAP === 'true';
205
205
 
206
+ // Explicit roots bypass the application's .vona exclusion and prevent stale emit-cache fallback.
207
+ const tsconfig = path.join(projectPath, '.vona/tsconfig.build.json');
208
+ await fse.writeJson(tsconfig, {
209
+ extends: '../tsconfig.json',
210
+ files: ['./bootstrap.ts', './app.ts', './env.ts', './config.ts', './modules-meta.ts'],
211
+ });
212
+
206
213
  const replaceValues = generateConfigDefine(env, ['NODE_ENV', 'META_MODE', 'META_FLAVOR']);
207
214
  for (const relativeName in modulesMeta.modules) {
208
215
  const module = modulesMeta.modules[relativeName];
@@ -265,11 +272,15 @@ export class CliBinBuild extends BeanCliBase {
265
272
  json(),
266
273
  commonjs(),
267
274
  typescript({
275
+ tsconfig,
268
276
  module: 'nodenext',
269
277
  compilerOptions: {
270
278
  noCheck: true,
279
+ noEmit: false,
271
280
  declaration: false,
281
+ declarationMap: false,
272
282
  composite: false,
283
+ incremental: false,
273
284
  sourceMap,
274
285
  },
275
286
  outputToFilesystem: false,
@@ -0,0 +1,152 @@
1
+ import assert from 'node:assert/strict';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { dirname, join } from 'node:path';
6
+ import { test } from 'node:test';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const buildUrl = new URL('../src/lib/bean/cli.bin.build.ts', import.meta.url).href;
10
+
11
+ // Keep the actual bundler and transpilers; only isolate CLI initialization.
12
+ const source = `
13
+ import assert from 'node:assert/strict';
14
+ import { readFileSync, readdirSync, writeFileSync } from 'node:fs';
15
+ import { registerHooks } from 'node:module';
16
+ import { join } from 'node:path';
17
+ import { pathToFileURL } from 'node:url';
18
+ const buildUrl = ${JSON.stringify(buildUrl)};
19
+ const projectPath = process.argv[1];
20
+ registerHooks({
21
+ resolve(specifier, context, nextResolve) {
22
+ if (context.parentURL === buildUrl) {
23
+ const mock = specifier === '@cabloy/cli'
24
+ ? 'export class BeanCliBase {}'
25
+ : specifier === './toolsBin/generateVonaMeta.ts'
26
+ ? 'export function generateVonaMeta() { throw new Error("Unexpected generation"); }'
27
+ : undefined;
28
+ if (mock) return { url: 'data:text/javascript,' + encodeURIComponent(mock), shortCircuit: true };
29
+ }
30
+ return nextResolve(specifier, context);
31
+ },
32
+ });
33
+ const { CliBinBuild } = await import(buildUrl);
34
+ const cli = Object.create(CliBinBuild.prototype);
35
+ const cachePath = process.argv[2];
36
+ const poisoned = readFileSync(cachePath, 'utf8');
37
+ for (const [index, flavor] of ['normal', 'ci', 'normal'].entries()) {
38
+ const env = {
39
+ META_MODE: 'prod',
40
+ META_FLAVOR: flavor,
41
+ SERVER_LISTEN_DISABLE: flavor === 'ci' ? 'true' : '',
42
+ generation: index,
43
+ };
44
+ writeFileSync(join(projectPath, '.vona/env.ts'),
45
+ 'export default ' + JSON.stringify(env) + ' as const;');
46
+ writeFileSync(join(projectPath, '.vona/config.ts'),
47
+ 'export default { generation: ' + index + ' };');
48
+ const outDir = join(projectPath, 'dist/build-' + index);
49
+ await cli._rollup(projectPath, env, { modules: {} }, outDir);
50
+ const app = await import(pathToFileURL(join(outDir, 'app.js')).href);
51
+ assert.deepEqual(app.env, env, 'must compile current generated environment, not cached CI output');
52
+ assert.deepEqual(await app.config(), { generation: index });
53
+ assert.equal(app.view, 'fresh-view', 'must retain inherited JSX and legacy decorator compilation');
54
+ assert.equal(readFileSync(cachePath, 'utf8'), poisoned, 'legacy cache must remain in place');
55
+ if (process.env.BUILD_SOURCEMAP === 'true') {
56
+ const maps = readdirSync(outDir).filter(name => name.endsWith('.js.map'));
57
+ assert.ok(maps.length > 0);
58
+ assert.ok(maps.some(name =>
59
+ JSON.parse(readFileSync(join(outDir, name), 'utf8')).sources
60
+ .some(source => source.endsWith('/.vona/env.ts'))));
61
+ }
62
+ }
63
+ `;
64
+
65
+ for (const sourceMap of [false, true]) {
66
+ test(`production build uses fresh generated inputs despite CI cache (sourceMap=${sourceMap})`, () => {
67
+ const projectPath = realpathSync(mkdtempSync(join(tmpdir(), 'vona-bin-build-fresh-')));
68
+ try {
69
+ mkdirSync(join(projectPath, '.vona'));
70
+ mkdirSync(join(projectPath, 'src/backend'), { recursive: true });
71
+ symlinkSync(
72
+ fileURLToPath(new URL('../../../node_modules', import.meta.url)),
73
+ join(projectPath, 'node_modules'),
74
+ process.platform === 'win32' ? 'junction' : 'dir',
75
+ );
76
+ writeFileSync(join(projectPath, 'package.json'), JSON.stringify({ type: 'module' }));
77
+ writeFileSync(
78
+ join(projectPath, 'tsconfig.base.json'),
79
+ JSON.stringify({
80
+ compilerOptions: {
81
+ target: 'ES2020',
82
+ module: 'NodeNext',
83
+ moduleResolution: 'NodeNext',
84
+ incremental: true,
85
+ composite: true,
86
+ declaration: true,
87
+ declarationMap: false,
88
+ jsx: 'react',
89
+ jsxFactory: 'h',
90
+ experimentalDecorators: true,
91
+ emitDecoratorMetadata: true,
92
+ skipLibCheck: true,
93
+ },
94
+ }),
95
+ );
96
+ writeFileSync(
97
+ join(projectPath, 'tsconfig.json'),
98
+ JSON.stringify({
99
+ extends: './tsconfig.base.json',
100
+ compilerOptions: { noEmit: true, composite: false, declaration: false },
101
+ exclude: ['.vona', 'dist', 'node_modules'],
102
+ }),
103
+ );
104
+ writeFileSync(
105
+ join(projectPath, 'src/backend/view.tsx'),
106
+ `function h(tag: string) { return tag; }
107
+ function mark(target: any) { target.value = 'fresh-view'; }
108
+ @mark
109
+ class View { static value: string; }
110
+ export const view = <fixture /> === 'fixture' ? View.value : 'invalid';`,
111
+ );
112
+ writeFileSync(
113
+ join(projectPath, '.vona/app.ts'),
114
+ `export { default as env } from './env.ts';
115
+ export { view } from '../src/backend/view.tsx';
116
+ export async function config() { return (await import('./config.ts')).default; }`,
117
+ );
118
+ writeFileSync(join(projectPath, '.vona/bootstrap.ts'), "export { env } from './app.ts';");
119
+ writeFileSync(join(projectPath, '.vona/modules-meta.ts'), 'export default {};');
120
+ const cachePath = join(
121
+ projectPath,
122
+ '.rollup.cache',
123
+ join(projectPath, '.vona/env.js').replace(/^([a-z]+):/i, '$1'),
124
+ );
125
+ mkdirSync(dirname(cachePath), { recursive: true });
126
+ writeFileSync(
127
+ cachePath,
128
+ 'export default { META_MODE: "prod", META_FLAVOR: "ci", SERVER_LISTEN_DISABLE: "true", generation: "poisoned" };',
129
+ );
130
+ const result = spawnSync(
131
+ process.execPath,
132
+ ['--input-type=module', '--eval', source, projectPath, cachePath],
133
+ {
134
+ cwd: projectPath,
135
+ encoding: 'utf8',
136
+ timeout: 60_000,
137
+ env: {
138
+ ...process.env,
139
+ BUILD_MINIFY: 'false',
140
+ BUILD_SOURCEMAP: String(sourceMap),
141
+ },
142
+ },
143
+ );
144
+ assert.ifError(result.error);
145
+ assert.equal(result.signal, null, result.stderr);
146
+ assert.equal(result.status, 0, result.stderr);
147
+ assert.doesNotMatch(result.stderr, /\[plugin typescript\]/i, result.stderr);
148
+ } finally {
149
+ rmSync(projectPath, { recursive: true, force: true });
150
+ }
151
+ });
152
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vona",
3
- "version": "5.1.120",
3
+ "version": "5.1.122",
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.52",
3
+ "version": "5.1.53",
4
4
  "gitHead": "a79189b882c17af5911573896a781bbb0046d37d",
5
5
  "description": "vona",
6
6
  "keywords": [