@stackonward/cli 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +248 -0
- package/dist/index.js +14093 -603
- package/dist/index.js.map +1 -1
- package/dist/resources/deploy/ci/github/caller.yml +38 -0
- package/dist/resources/deploy/ci/onedev/import.yml +14 -0
- package/dist/resources/deploy/nginx/cache.conf +17 -0
- package/dist/resources/deploy/nginx/compression.conf +23 -0
- package/dist/resources/deploy/nginx/proxy.conf +15 -0
- package/dist/resources/deploy/profiles/go-service/compose.yaml +19 -0
- package/dist/resources/deploy/profiles/nuxt-node/Dockerfile +17 -0
- package/dist/resources/deploy/profiles/static-files/nginx/site.conf.template +55 -0
- package/dist/resources/deploy/profiles/static-nginx/Dockerfile +18 -0
- package/dist/resources/deploy/profiles/static-nginx/nginx/default.conf.template +28 -0
- package/dist/resources/deploy/targets/compose-host/deploy.config.example.json +114 -0
- package/dist/resources/deploy/traffic/nginx/stackonward-nginx-traffic +96 -0
- package/dist/resources/registry.json +30 -30
- package/dist/resources/templates/base/.claude/docs/architecture.md +4 -0
- package/dist/resources/templates/base/AGENTS.md +15 -0
- package/dist/resources/templates/base/CLAUDE.md +15 -0
- package/dist/resources/templates/go-backend/.dockerignore +25 -3
- package/dist/resources/templates/go-backend/deploy.config.json +26 -0
- package/dist/resources/templates/go-backend/docs/runtime-provenance.json +9 -6
- package/dist/resources/templates/go-backend/docs/source-provenance.json +33 -22
- package/dist/resources/templates/go-backend/internal/common/enums/register_locale.go +7 -0
- package/dist/resources/templates/go-backend/internal/common/enums/register_locale_test.go +30 -0
- package/dist/resources/templates/go-backend/internal/common/locale/locale.go +102 -76
- package/dist/resources/templates/go-backend/internal/common/locale/locale_test.go +168 -45
- package/dist/resources/templates/go-backend/internal/core/commandreplay/model.go +5 -5
- package/dist/resources/templates/go-backend/internal/core/commandreplay/persistence.go +20 -7
- package/dist/resources/templates/go-backend/internal/core/commandreplay/persistence_test.go +51 -22
- package/dist/resources/templates/go-backend/internal/core/commandreplay/repository.go +2 -2
- package/dist/resources/templates/go-backend/internal/core/commandreplay/service.go +13 -24
- package/dist/resources/templates/go-backend/internal/core/commandreplay/service_persistence_test.go +98 -13
- package/dist/resources/templates/go-backend/internal/core/commandreplay/service_test.go +61 -36
- package/dist/resources/templates/go-backend/internal/core/commandreplay/types.go +6 -6
- package/dist/resources/templates/go-backend/internal/core/integrationevent/config.go +24 -0
- package/dist/resources/templates/go-backend/internal/core/integrationevent/errors.go +53 -0
- package/dist/resources/templates/go-backend/internal/core/integrationevent/model.go +71 -0
- package/dist/resources/templates/go-backend/internal/core/integrationevent/persistence.go +193 -0
- package/dist/resources/templates/go-backend/internal/core/integrationevent/persistence_postgres_test.go +178 -0
- package/dist/resources/templates/go-backend/internal/core/integrationevent/persistence_test.go +168 -0
- package/dist/resources/templates/go-backend/internal/core/integrationevent/provider.go +16 -0
- package/dist/resources/templates/go-backend/internal/core/integrationevent/registry.go +69 -0
- package/dist/resources/templates/go-backend/internal/core/integrationevent/repository.go +17 -0
- package/dist/resources/templates/go-backend/internal/core/integrationevent/service.go +283 -0
- package/dist/resources/templates/go-backend/internal/core/integrationevent/service_test.go +433 -0
- package/dist/resources/templates/go-backend/internal/core/integrationevent/types.go +67 -0
- package/dist/resources/templates/go-backend/migrations/000001_infrastructure.down.sql +6 -0
- package/dist/resources/templates/go-backend/migrations/000001_infrastructure.up.sql +176 -16
- package/dist/resources/templates/go-backend/tests/infrastructure.sql +109 -6
- package/dist/resources/templates/nuxt/.claude/docs/stack.md +5 -1
- package/dist/resources/templates/nuxt/.dockerignore +19 -0
- package/dist/resources/templates/nuxt/deploy.config.json +27 -0
- package/dist/resources/templates/nuxt/package.json +8 -8
- package/package.json +5 -3
|
@@ -52,6 +52,10 @@ Use actual paths in [Stack](stack.md). Nuxt routes belong in `app/pages/`, serve
|
|
|
52
52
|
|
|
53
53
|
Request/response types stay beside the service/API owner. Component props/emits stay with the component. A type moves to a shared owner only when callers share semantics, not merely field names.
|
|
54
54
|
|
|
55
|
+
Directories, modules and packages carry owner context; child files and symbols keep only the minimum responsibility. Within an owner use short names such as `Header`, `Config`, `Status`, `Menu`, `EventInput` and `items`; across owners add only the shortest qualifier, such as `SiteHeader`, `LocaleMenu`, `GTMOptions` or `GA4Event`. Never exchange one redundant prefix for another: `MarketingHeader` -> `PublicHeader` remains forbidden.
|
|
56
|
+
|
|
57
|
+
Directories, packages and ordinary TypeScript modules use kebab-case; Vue composables use `useX.ts`; Vue component files and types use PascalCase; variables/functions use camelCase; only fixed invariants use `UPPER_SNAKE_CASE`. Framework routes and endpoints keep framework syntax. Generic transforms use business-free responsibility names. External identifiers such as `purchase`, `login` and `gateway_*` remain unchanged. Vague standalone or catch-all names such as `helper`, `utils`, `common` and `manager` are forbidden; choose a precise owner and responsibility. `temp`, `new` and `final` naming residue is forbidden. Search owners, siblings and usages before naming or renaming; update all references and delete superseded names together.
|
|
58
|
+
|
|
55
59
|
| Type | Owner |
|
|
56
60
|
| -------------------------- | --------------------------------------------- |
|
|
57
61
|
| HTTP request/response | API or server contract |
|
|
@@ -67,6 +67,21 @@ For non-trivial additions, declare `PLACEMENT/NAMING` (owner, name, kind, respon
|
|
|
67
67
|
|
|
68
68
|
External calls, user-selectable alternatives, variable business policy, replaceable technology, standard extension points and multi-tenant behavior require `EXTENSIBILITY-INTENT`: signal, current and expected implementations, confidence, pattern, interface/configuration first, and exact files a second adapter would change. Otherwise state `NO-ABSTRACTION-JUSTIFICATION` and its refactor trigger.
|
|
69
69
|
|
|
70
|
+
## Owner-Scoped Naming (CRITICAL)
|
|
71
|
+
|
|
72
|
+
Directories, modules and packages are namespaces. Child files and symbols **MUST** keep only the minimum responsibility-bearing name and **MUST NOT** repeat the owner or replace one redundant caller prefix with another (`MarketingHeader` -> `PublicHeader` remains forbidden).
|
|
73
|
+
|
|
74
|
+
- Inside an explicit owner, use short names such as `Header`, `Config`, `Status`, `Menu`, `EventInput` or `items`; across owners, add only the shortest required qualifier, such as `SiteHeader`, `LocaleMenu`, `GTMOptions` or `GA4Event`.
|
|
75
|
+
- Names describe responsibility, never the first caller, page, brand, task or conversation. Pure transforms use business-free names; business and protocol nouns remain only when they enforce that contract.
|
|
76
|
+
- Directories, packages and ordinary TypeScript modules use kebab-case; Vue composables use `useX.ts`; Vue component files and types use PascalCase; variables and functions use camelCase; only true fixed invariants use UPPER_SNAKE_CASE. Framework route and endpoint files follow framework syntax.
|
|
77
|
+
- External or business-owned identifiers such as `purchase`, `login` and `gateway_*` are semantic contracts, not code-name redundancy, and remain unchanged.
|
|
78
|
+
- Vague standalone or catch-all names such as `helper`, `utils`, `common` and `manager` are forbidden; choose a precise owner and responsibility. `temp`, `new` and `final` naming residue is forbidden. Search owners, siblings and usages before naming or renaming; update all references and delete superseded names together.
|
|
79
|
+
- Nuxt component directories remain namespaces: preserve default path-aware auto-naming and never set global `pathPrefix: false`. Local explicit imports may use the short name; cross-owner and global auto-imports use the path qualifier.
|
|
80
|
+
|
|
81
|
+
✅ `analytics/events.ts::EventInput`, `components/site/Header.vue` -> `SiteHeader`, `useTracking`, `GTMOptions`, `GA4Event`
|
|
82
|
+
|
|
83
|
+
❌ `types/marketing-analytics.ts`, `MarketingHeader`, `PublicHeader`, `GoogleTagManagerOptions`
|
|
84
|
+
|
|
70
85
|
---
|
|
71
86
|
|
|
72
87
|
# AI Execution Standard
|
|
@@ -67,6 +67,21 @@ For non-trivial additions, declare `PLACEMENT/NAMING` (owner, name, kind, respon
|
|
|
67
67
|
|
|
68
68
|
External calls, user-selectable alternatives, variable business policy, replaceable technology, standard extension points and multi-tenant behavior require `EXTENSIBILITY-INTENT`: signal, current and expected implementations, confidence, pattern, interface/configuration first, and exact files a second adapter would change. Otherwise state `NO-ABSTRACTION-JUSTIFICATION` and its refactor trigger.
|
|
69
69
|
|
|
70
|
+
## Owner-Scoped Naming (CRITICAL)
|
|
71
|
+
|
|
72
|
+
Directories, modules and packages are namespaces. Child files and symbols **MUST** keep only the minimum responsibility-bearing name and **MUST NOT** repeat the owner or replace one redundant caller prefix with another (`MarketingHeader` -> `PublicHeader` remains forbidden).
|
|
73
|
+
|
|
74
|
+
- Inside an explicit owner, use short names such as `Header`, `Config`, `Status`, `Menu`, `EventInput` or `items`; across owners, add only the shortest required qualifier, such as `SiteHeader`, `LocaleMenu`, `GTMOptions` or `GA4Event`.
|
|
75
|
+
- Names describe responsibility, never the first caller, page, brand, task or conversation. Pure transforms use business-free names; business and protocol nouns remain only when they enforce that contract.
|
|
76
|
+
- Directories, packages and ordinary TypeScript modules use kebab-case; Vue composables use `useX.ts`; Vue component files and types use PascalCase; variables and functions use camelCase; only true fixed invariants use UPPER_SNAKE_CASE. Framework route and endpoint files follow framework syntax.
|
|
77
|
+
- External or business-owned identifiers such as `purchase`, `login` and `gateway_*` are semantic contracts, not code-name redundancy, and remain unchanged.
|
|
78
|
+
- Vague standalone or catch-all names such as `helper`, `utils`, `common` and `manager` are forbidden; choose a precise owner and responsibility. `temp`, `new` and `final` naming residue is forbidden. Search owners, siblings and usages before naming or renaming; update all references and delete superseded names together.
|
|
79
|
+
- Nuxt component directories remain namespaces: preserve default path-aware auto-naming and never set global `pathPrefix: false`. Local explicit imports may use the short name; cross-owner and global auto-imports use the path qualifier.
|
|
80
|
+
|
|
81
|
+
✅ `analytics/events.ts::EventInput`, `components/site/Header.vue` -> `SiteHeader`, `useTracking`, `GTMOptions`, `GA4Event`
|
|
82
|
+
|
|
83
|
+
❌ `types/marketing-analytics.ts`, `MarketingHeader`, `PublicHeader`, `GoogleTagManagerOptions`
|
|
84
|
+
|
|
70
85
|
---
|
|
71
86
|
|
|
72
87
|
# AI Execution Standard
|
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
.git
|
|
2
|
+
.github
|
|
3
|
+
.claude
|
|
4
|
+
.husky
|
|
5
|
+
.idea
|
|
6
|
+
.vscode
|
|
2
7
|
.env
|
|
3
8
|
.env.*
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
9
|
+
.npmrc
|
|
10
|
+
*.key
|
|
11
|
+
*.pem
|
|
12
|
+
|
|
13
|
+
bin/
|
|
14
|
+
coverage/
|
|
15
|
+
coverage.out
|
|
16
|
+
coverage.html
|
|
17
|
+
node_modules/
|
|
18
|
+
**/node_modules/
|
|
19
|
+
tmp/
|
|
20
|
+
|
|
21
|
+
*.log
|
|
22
|
+
.DS_Store
|
|
23
|
+
|
|
24
|
+
AGENTS.md
|
|
25
|
+
CLAUDE.md
|
|
26
|
+
README.md
|
|
27
|
+
deploy.config.json
|
|
28
|
+
docs/
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"name": "{{projectName}}",
|
|
4
|
+
"profile": "go-service",
|
|
5
|
+
"source": {
|
|
6
|
+
"verify": {
|
|
7
|
+
"executable": "make",
|
|
8
|
+
"args": ["check"]
|
|
9
|
+
},
|
|
10
|
+
"build": {
|
|
11
|
+
"executable": "make",
|
|
12
|
+
"args": ["build"]
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"image": {
|
|
16
|
+
"dockerfile": "Dockerfile",
|
|
17
|
+
"context": ".",
|
|
18
|
+
"repository": "{{projectName}}",
|
|
19
|
+
"platform": "linux/amd64"
|
|
20
|
+
},
|
|
21
|
+
"environments": {
|
|
22
|
+
"production": {
|
|
23
|
+
"target": "registry-only"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -112,8 +112,9 @@
|
|
|
112
112
|
},
|
|
113
113
|
{
|
|
114
114
|
"target": "migrations/000001_infrastructure.up.sql",
|
|
115
|
-
"target_template_sha256": "
|
|
116
|
-
"transformation": "
|
|
115
|
+
"target_template_sha256": "fe971c4bd743fda85d557c5f9634dae6347f5032414cb24d6e3f85dc5468457b",
|
|
116
|
+
"transformation": "source-adapted",
|
|
117
|
+
"details": "Defines eleven business-neutral infrastructure owner tables: seven retained OneX baseline tables, command replay with mandatory principal-scoped five-field identity, and three StackOnward-owned integration-event inbox, subscriber-receipt and aggregate-checkpoint tables. The three integration-event tables and their transition guards are not attributed to the listed OneX origins. Four log partition parents retain default partitions; outbox identity/schema immutability and operation-log finalization guards remain. The platform event-catalog binding trigger stays excluded because independent producers assign schema_version without platform database queries.",
|
|
117
118
|
"origins": [
|
|
118
119
|
{
|
|
119
120
|
"source": "backend/migrations/000001_platform_baseline.up.sql",
|
|
@@ -127,8 +128,9 @@
|
|
|
127
128
|
},
|
|
128
129
|
{
|
|
129
130
|
"target": "migrations/000001_infrastructure.down.sql",
|
|
130
|
-
"target_template_sha256": "
|
|
131
|
-
"transformation": "
|
|
131
|
+
"target_template_sha256": "7e7b9e9682128e98eeee02cbbf4eafd4df255d67727896669e12244b76bc4cfc",
|
|
132
|
+
"transformation": "source-adapted",
|
|
133
|
+
"details": "Reverses the eleven business-neutral infrastructure owner tables and six associated guard functions; dependent partitions, triggers and sequences are removed through table cascade. The three integration-event tables and their guards are StackOnward-owned rather than derived from the listed OneX origins. The shared pgcrypto extension remains installed.",
|
|
132
134
|
"origins": [
|
|
133
135
|
{
|
|
134
136
|
"source": "backend/migrations/000001_platform_baseline.up.sql",
|
|
@@ -160,8 +162,9 @@
|
|
|
160
162
|
},
|
|
161
163
|
{
|
|
162
164
|
"target": "tests/infrastructure.sql",
|
|
163
|
-
"target_template_sha256": "
|
|
164
|
-
"transformation": "
|
|
165
|
+
"target_template_sha256": "342171c4af1ba35ab174439cb3673489b48f4e0073fa91d24939c9251e88f0a7",
|
|
166
|
+
"transformation": "source-adapted",
|
|
167
|
+
"details": "Template-owned verification asserts the exact eleven business-neutral infrastructure owner tables, five-field principal-scoped command replay uniqueness and required principal values, integration-event global identity and append-only storage, subscriber receipt isolation and terminal transitions, aggregate checkpoint source isolation and contiguous advancement, plus retained outbox and operation-log invariants.",
|
|
165
168
|
"origins": []
|
|
166
169
|
}
|
|
167
170
|
],
|
|
@@ -430,9 +430,10 @@
|
|
|
430
430
|
{
|
|
431
431
|
"source": "backend/internal/core/commandreplay/model.go",
|
|
432
432
|
"target": "internal/core/commandreplay/model.go",
|
|
433
|
-
"transformation": "
|
|
433
|
+
"transformation": "source-adapted",
|
|
434
|
+
"details": "Makes principal_type and principal_ref mandatory members of command replay identity. The database unique key is the five-field tuple (namespace, principal_type, principal_ref, command_scope, idempotency_key), isolating identical idempotency keys across trusted principals.",
|
|
434
435
|
"source_sha256": "e610b620e96cabafad53ca1265455bd7fececcf6db73783beebe26bf6adf0c49",
|
|
435
|
-
"target_template_sha256": "
|
|
436
|
+
"target_template_sha256": "537673b5ada9d6f8e375d76e65e76764162622f2867485e25b858cf15c8da823"
|
|
436
437
|
},
|
|
437
438
|
{
|
|
438
439
|
"source": "backend/internal/core/commandreplay/model_test.go",
|
|
@@ -451,16 +452,18 @@
|
|
|
451
452
|
{
|
|
452
453
|
"source": "backend/internal/core/commandreplay/persistence.go",
|
|
453
454
|
"target": "internal/core/commandreplay/persistence.go",
|
|
454
|
-
"transformation": "
|
|
455
|
+
"transformation": "source-adapted",
|
|
456
|
+
"details": "Scopes repository lookup, conflict handling and the length-prefixed PostgreSQL advisory lock key by the five-field tuple (namespace, principal_type, principal_ref, command_scope, idempotency_key).",
|
|
455
457
|
"source_sha256": "6b495eb7d886dea43f50fb5457c99450cf75ee3180ab138dc4b6f71d5e912308",
|
|
456
|
-
"target_template_sha256": "
|
|
458
|
+
"target_template_sha256": "e335b9085acec0a83789e4e115ab0a04e2a6db8bae97384ab81a9d78e39debac"
|
|
457
459
|
},
|
|
458
460
|
{
|
|
459
461
|
"source": "backend/internal/core/commandreplay/persistence_test.go",
|
|
460
462
|
"target": "internal/core/commandreplay/persistence_test.go",
|
|
461
|
-
"transformation": "
|
|
463
|
+
"transformation": "source-adapted",
|
|
464
|
+
"details": "Verifies database lookup, conflict targets and advisory locks use the complete five-field principal scope and do not collide across distinct principals.",
|
|
462
465
|
"source_sha256": "58228cdb9f0fcf22da36e620d565d810fe3c618e061a68e61a140da44bacac3b",
|
|
463
|
-
"target_template_sha256": "
|
|
466
|
+
"target_template_sha256": "0947eab5e124ebd4168a78cc3a2ed9957462a6fed1d3967c4672f590644db760"
|
|
464
467
|
},
|
|
465
468
|
{
|
|
466
469
|
"source": "backend/internal/core/commandreplay/provider.go",
|
|
@@ -472,37 +475,42 @@
|
|
|
472
475
|
{
|
|
473
476
|
"source": "backend/internal/core/commandreplay/repository.go",
|
|
474
477
|
"target": "internal/core/commandreplay/repository.go",
|
|
475
|
-
"transformation": "
|
|
478
|
+
"transformation": "source-adapted",
|
|
479
|
+
"details": "Extends replay lookup and scope-lock contracts with required principal type and principal reference so every repository operation addresses the same five-field identity.",
|
|
476
480
|
"source_sha256": "90a0e737c8df5f65e6f1897c82b3087ef35a6cbf1e46bd4ca291ce18a686610f",
|
|
477
|
-
"target_template_sha256": "
|
|
481
|
+
"target_template_sha256": "4e52704849a4bbc1842e812d6a6244d0860f23a73a92ae7b27694cf1fa2bc617"
|
|
478
482
|
},
|
|
479
483
|
{
|
|
480
484
|
"source": "backend/internal/core/commandreplay/service.go",
|
|
481
485
|
"target": "internal/core/commandreplay/service.go",
|
|
482
|
-
"transformation": "
|
|
486
|
+
"transformation": "source-adapted",
|
|
487
|
+
"details": "Requires trusted principal type and reference at the service boundary and propagates the complete five-field scope through lock, lookup, create and duplicate-reload paths.",
|
|
483
488
|
"source_sha256": "e6773b4fa604133333ca193d73c7ede037abf7d00df2293df4618e22cd2b616c",
|
|
484
|
-
"target_template_sha256": "
|
|
489
|
+
"target_template_sha256": "266dd3787265091404af83be76c26d887b5115abddaf97f3ce3b76b3c456b093"
|
|
485
490
|
},
|
|
486
491
|
{
|
|
487
492
|
"source": "backend/internal/core/commandreplay/service_persistence_test.go",
|
|
488
493
|
"target": "internal/core/commandreplay/service_persistence_test.go",
|
|
489
|
-
"transformation": "
|
|
494
|
+
"transformation": "source-adapted",
|
|
495
|
+
"details": "Verifies persisted replay isolation for identical command and idempotency values owned by different trusted principals, including concurrent acquisition behavior.",
|
|
490
496
|
"source_sha256": "1e5e2fd1b720f069f926c5cfc009e243606b0e87bccd6773a71ee53cc8b7e02b",
|
|
491
|
-
"target_template_sha256": "
|
|
497
|
+
"target_template_sha256": "47ecb12805fe8efa1d59cbcf22bc03ae971f8bb2725a418d2cfc416b27785a19"
|
|
492
498
|
},
|
|
493
499
|
{
|
|
494
500
|
"source": "backend/internal/core/commandreplay/service_test.go",
|
|
495
501
|
"target": "internal/core/commandreplay/service_test.go",
|
|
496
|
-
"transformation": "
|
|
502
|
+
"transformation": "source-adapted",
|
|
503
|
+
"details": "Verifies principal fields are mandatory and that replay, conflict and external-lease behavior consistently use the five-field principal scope.",
|
|
497
504
|
"source_sha256": "f022ef1d41d9d296af275ef335ac87431c5f55307d19c7d9252a1b7e3f71116b",
|
|
498
|
-
"target_template_sha256": "
|
|
505
|
+
"target_template_sha256": "24ff2e89c58fe24447051fac0b03000a6e608317366a5ccbe73761570cde47af"
|
|
499
506
|
},
|
|
500
507
|
{
|
|
501
508
|
"source": "backend/internal/core/commandreplay/types.go",
|
|
502
509
|
"target": "internal/core/commandreplay/types.go",
|
|
503
|
-
"transformation": "
|
|
510
|
+
"transformation": "source-adapted",
|
|
511
|
+
"details": "Makes principal type and principal reference required request and normalized values because both fields participate in command replay identity.",
|
|
504
512
|
"source_sha256": "14c038c1f4cf4ff076e0b71727276604550dd62557e7e2c72b8e4d38aa179c04",
|
|
505
|
-
"target_template_sha256": "
|
|
513
|
+
"target_template_sha256": "53c7278ebea7ed97ef2817c30feb8a60846336b8d79886d08a046396d81b36fa"
|
|
506
514
|
},
|
|
507
515
|
{
|
|
508
516
|
"source": "backend/internal/core/config/config.go",
|
|
@@ -2175,9 +2183,10 @@
|
|
|
2175
2183
|
{
|
|
2176
2184
|
"source": "backend/internal/common/enums/register_locale.go",
|
|
2177
2185
|
"target": "internal/common/enums/register_locale.go",
|
|
2178
|
-
"transformation": "
|
|
2186
|
+
"transformation": "source-adapted",
|
|
2187
|
+
"details": "Expands business-neutral locale presentation metadata to the exact 19 internal short codes exposed by the locale Resource Bundle; no alias codes are registered.",
|
|
2179
2188
|
"source_sha256": "3334a68915c6a6c10b5ff1e36bc523e4111a962b451779165cfb70c0e6e4837d",
|
|
2180
|
-
"target_template_sha256": "
|
|
2189
|
+
"target_template_sha256": "62ab6b920cd7e394c99dd0855bc0479e70183bc0f631e0c2e5a83826791a694a"
|
|
2181
2190
|
},
|
|
2182
2191
|
{
|
|
2183
2192
|
"source": "backend/internal/common/enums/register_mailbox.go",
|
|
@@ -2546,16 +2555,18 @@
|
|
|
2546
2555
|
{
|
|
2547
2556
|
"source": "backend/internal/common/locale/locale.go",
|
|
2548
2557
|
"target": "internal/common/locale/locale.go",
|
|
2549
|
-
"transformation": "
|
|
2558
|
+
"transformation": "source-adapted",
|
|
2559
|
+
"details": "Replaces permissive case-folded aliases and fallback normalization with one exact 19-entry Resource Bundle mapping between canonical external tags and internal short codes. It provides deterministic round trips and text direction, marks only Arabic as RTL, and rejects unknown, non-canonical or colliding locale keys.",
|
|
2550
2560
|
"source_sha256": "17b5db8f7f0d0667cc88e37351435214e04ad98fe214b22c1c857e77ef7cb02d",
|
|
2551
|
-
"target_template_sha256": "
|
|
2561
|
+
"target_template_sha256": "bafde441fe8a3d7d0e6db8c413a9d562030cfb2145357fc2406b6d831b2f77a5"
|
|
2552
2562
|
},
|
|
2553
2563
|
{
|
|
2554
2564
|
"source": "backend/internal/common/locale/locale_test.go",
|
|
2555
2565
|
"target": "internal/common/locale/locale_test.go",
|
|
2556
|
-
"transformation": "
|
|
2566
|
+
"transformation": "source-adapted",
|
|
2567
|
+
"details": "Verifies all 19 canonical external-tag and short-code round trips and directions, plus fail-closed rejection of aliases, case variants, whitespace, punctuation, unknown keys, blank text and duplicate canonical targets.",
|
|
2557
2568
|
"source_sha256": "67c1b6b955603c3797d8231b916ce5ee3f55c7463a600f6bc931a9e2ad706cbc",
|
|
2558
|
-
"target_template_sha256": "
|
|
2569
|
+
"target_template_sha256": "5f76e478cdaa12a2e6bcea3ddb8501e7473530efff0f628d4f77d82364809946"
|
|
2559
2570
|
},
|
|
2560
2571
|
{
|
|
2561
2572
|
"source": "backend/internal/common/mediaref/mediaref.go",
|
|
@@ -18,8 +18,15 @@ var localeEnumMetadataByCode = map[string]localeEnumMetadata{
|
|
|
18
18
|
"ja": {label: "日本語", color: "magenta"},
|
|
19
19
|
"ko": {label: "한국어", color: "green"},
|
|
20
20
|
"pt": {label: "Português", color: "orange"},
|
|
21
|
+
"ru": {label: "Русский", color: "purple"},
|
|
21
22
|
"hi": {label: "हिंदी", color: "lime"},
|
|
22
23
|
"ar": {label: "العربية", color: "volcano"},
|
|
24
|
+
"id": {label: "Bahasa Indonesia", color: "blue"},
|
|
25
|
+
"th": {label: "ไทย", color: "green"},
|
|
26
|
+
"vi": {label: "Tiếng Việt", color: "cyan"},
|
|
27
|
+
"tr": {label: "Türkçe", color: "red"},
|
|
28
|
+
"pl": {label: "Polski", color: "magenta"},
|
|
29
|
+
"nl": {label: "Nederlands", color: "geekblue"},
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
func commonLocaleEnumItems() []EnumItem {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
package enums
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"reflect"
|
|
5
|
+
"testing"
|
|
6
|
+
|
|
7
|
+
commonlocale "{{module}}/internal/common/locale"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
func TestCommonLocaleEnumItemsCoverSupportedCodes(t *testing.T) {
|
|
11
|
+
items := commonLocaleEnumItems()
|
|
12
|
+
wantCodes := commonlocale.SupportedCodes()
|
|
13
|
+
if len(items) != len(wantCodes) {
|
|
14
|
+
t.Fatalf("commonLocaleEnumItems() count = %d, want %d", len(items), len(wantCodes))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
gotCodes := make([]string, 0, len(items))
|
|
18
|
+
for _, item := range items {
|
|
19
|
+
gotCodes = append(gotCodes, item.Value)
|
|
20
|
+
if item.Label == "" {
|
|
21
|
+
t.Errorf("locale %q has empty label", item.Value)
|
|
22
|
+
}
|
|
23
|
+
if item.Color == "" {
|
|
24
|
+
t.Errorf("locale %q has empty color", item.Value)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if !reflect.DeepEqual(gotCodes, wantCodes) {
|
|
28
|
+
t.Fatalf("commonLocaleEnumItems() values = %#v, want %#v", gotCodes, wantCodes)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -2,6 +2,7 @@ package locale
|
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"encoding/json"
|
|
5
|
+
"fmt"
|
|
5
6
|
"strings"
|
|
6
7
|
)
|
|
7
8
|
|
|
@@ -10,124 +11,143 @@ const (
|
|
|
10
11
|
AdminDefaultCode = "cn"
|
|
11
12
|
)
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
14
|
+
// Direction 表示语言内容的排版方向。
|
|
15
|
+
type Direction string
|
|
16
|
+
|
|
17
|
+
const (
|
|
18
|
+
DirectionLTR Direction = "ltr"
|
|
19
|
+
DirectionRTL Direction = "rtl"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
// Mapping 定义规范外部语言标签与内部短码之间的唯一映射。
|
|
23
|
+
type Mapping struct {
|
|
24
|
+
ExternalTag string
|
|
25
|
+
ShortCode string
|
|
26
|
+
Direction Direction
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
var
|
|
29
|
+
var mappings = []Mapping{
|
|
30
|
+
{ExternalTag: "en", ShortCode: "en", Direction: DirectionLTR},
|
|
31
|
+
{ExternalTag: "zh-Hans", ShortCode: "cn", Direction: DirectionLTR},
|
|
32
|
+
{ExternalTag: "zh-Hant", ShortCode: "tw", Direction: DirectionLTR},
|
|
33
|
+
{ExternalTag: "ja", ShortCode: "ja", Direction: DirectionLTR},
|
|
34
|
+
{ExternalTag: "ko", ShortCode: "ko", Direction: DirectionLTR},
|
|
35
|
+
{ExternalTag: "es", ShortCode: "es", Direction: DirectionLTR},
|
|
36
|
+
{ExternalTag: "fr", ShortCode: "fr", Direction: DirectionLTR},
|
|
37
|
+
{ExternalTag: "de", ShortCode: "de", Direction: DirectionLTR},
|
|
38
|
+
{ExternalTag: "pt-BR", ShortCode: "pt", Direction: DirectionLTR},
|
|
39
|
+
{ExternalTag: "it", ShortCode: "it", Direction: DirectionLTR},
|
|
40
|
+
{ExternalTag: "ru", ShortCode: "ru", Direction: DirectionLTR},
|
|
41
|
+
{ExternalTag: "ar", ShortCode: "ar", Direction: DirectionRTL},
|
|
42
|
+
{ExternalTag: "hi", ShortCode: "hi", Direction: DirectionLTR},
|
|
43
|
+
{ExternalTag: "id", ShortCode: "id", Direction: DirectionLTR},
|
|
44
|
+
{ExternalTag: "th", ShortCode: "th", Direction: DirectionLTR},
|
|
45
|
+
{ExternalTag: "vi", ShortCode: "vi", Direction: DirectionLTR},
|
|
46
|
+
{ExternalTag: "tr", ShortCode: "tr", Direction: DirectionLTR},
|
|
47
|
+
{ExternalTag: "pl", ShortCode: "pl", Direction: DirectionLTR},
|
|
48
|
+
{ExternalTag: "nl", ShortCode: "nl", Direction: DirectionLTR},
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
var externalTagIndex, shortCodeIndex = buildIndexes()
|
|
29
52
|
|
|
30
|
-
func
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
53
|
+
func buildIndexes() (map[string]Mapping, map[string]Mapping) {
|
|
54
|
+
byExternalTag := make(map[string]Mapping, len(mappings))
|
|
55
|
+
byShortCode := make(map[string]Mapping, len(mappings))
|
|
56
|
+
for _, mapping := range mappings {
|
|
57
|
+
byExternalTag[mapping.ExternalTag] = mapping
|
|
58
|
+
byShortCode[mapping.ShortCode] = mapping
|
|
34
59
|
}
|
|
60
|
+
return byExternalTag, byShortCode
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Mappings 返回按稳定顺序排列的全部语言映射。
|
|
64
|
+
func Mappings() []Mapping {
|
|
65
|
+
result := make([]Mapping, len(mappings))
|
|
66
|
+
copy(result, mappings)
|
|
35
67
|
return result
|
|
36
68
|
}
|
|
37
69
|
|
|
70
|
+
// SupportedCodes 返回按稳定顺序排列的全部内部短码。
|
|
38
71
|
func SupportedCodes() []string {
|
|
39
|
-
result := make([]string, len(
|
|
40
|
-
|
|
72
|
+
result := make([]string, 0, len(mappings))
|
|
73
|
+
for _, mapping := range mappings {
|
|
74
|
+
result = append(result, mapping.ShortCode)
|
|
75
|
+
}
|
|
41
76
|
return result
|
|
42
77
|
}
|
|
43
78
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return "en"
|
|
49
|
-
case "cn", "zh", "zh-cn", "zh-hans", "zh-hans-cn":
|
|
50
|
-
return "cn"
|
|
51
|
-
case "tw", "hk", "zh-tw", "zh-hk", "zh-hant", "zh-hant-tw", "zh-hant-hk":
|
|
52
|
-
return "tw"
|
|
53
|
-
case "ja", "jp", "ja-jp":
|
|
54
|
-
return "ja"
|
|
55
|
-
case "ko", "ko-kr":
|
|
56
|
-
return "ko"
|
|
57
|
-
case "de", "de-de":
|
|
58
|
-
return "de"
|
|
59
|
-
case "es", "es-es":
|
|
60
|
-
return "es"
|
|
61
|
-
case "fr", "fr-fr":
|
|
62
|
-
return "fr"
|
|
63
|
-
case "it", "it-it":
|
|
64
|
-
return "it"
|
|
65
|
-
case "pt", "pt-br", "pt-pt":
|
|
66
|
-
return "pt"
|
|
67
|
-
case "hi", "in", "hi-in":
|
|
68
|
-
return "hi"
|
|
69
|
-
case "ar", "ar-sa":
|
|
70
|
-
return "ar"
|
|
71
|
-
}
|
|
72
|
-
return ""
|
|
79
|
+
// ShortCodeForExternalTag 将规范外部语言标签精确映射为内部短码。
|
|
80
|
+
func ShortCodeForExternalTag(externalTag string) (string, bool) {
|
|
81
|
+
mapping, ok := externalTagIndex[externalTag]
|
|
82
|
+
return mapping.ShortCode, ok
|
|
73
83
|
}
|
|
74
84
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
85
|
+
// ExternalTagForShortCode 将内部短码精确映射为规范外部语言标签。
|
|
86
|
+
func ExternalTagForShortCode(shortCode string) (string, bool) {
|
|
87
|
+
mapping, ok := shortCodeIndex[shortCode]
|
|
88
|
+
return mapping.ExternalTag, ok
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// DirectionForShortCode 返回内部短码对应的排版方向。
|
|
92
|
+
func DirectionForShortCode(shortCode string) (Direction, bool) {
|
|
93
|
+
mapping, ok := shortCodeIndex[shortCode]
|
|
94
|
+
return mapping.Direction, ok
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// NormalizeCode 将规范外部语言标签或内部短码精确转换为内部短码。
|
|
98
|
+
func NormalizeCode(value string) string {
|
|
99
|
+
if mapping, ok := externalTagIndex[value]; ok {
|
|
100
|
+
return mapping.ShortCode
|
|
78
101
|
}
|
|
79
|
-
if
|
|
80
|
-
return
|
|
102
|
+
if mapping, ok := shortCodeIndex[value]; ok {
|
|
103
|
+
return mapping.ShortCode
|
|
81
104
|
}
|
|
82
|
-
return
|
|
105
|
+
return ""
|
|
83
106
|
}
|
|
84
107
|
|
|
108
|
+
// IsShortCode 判断值是否为受支持的精确内部短码。
|
|
85
109
|
func IsShortCode(value string) bool {
|
|
86
|
-
|
|
87
|
-
if code == "" || code != strings.ToLower(code) {
|
|
88
|
-
return false
|
|
89
|
-
}
|
|
90
|
-
_, ok := supportedCodeSet[code]
|
|
110
|
+
_, ok := shortCodeIndex[value]
|
|
91
111
|
return ok
|
|
92
112
|
}
|
|
93
113
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return map[string]string{}
|
|
97
|
-
}
|
|
114
|
+
// NormalizeTextMap 将语言键精确转换为内部短码并拒绝无效或冲突输入。
|
|
115
|
+
func NormalizeTextMap(input map[string]string) (map[string]string, error) {
|
|
98
116
|
result := make(map[string]string, len(input))
|
|
99
117
|
for key, value := range input {
|
|
100
118
|
code := NormalizeCode(key)
|
|
119
|
+
if code == "" {
|
|
120
|
+
return nil, fmt.Errorf("unsupported locale key %q", key)
|
|
121
|
+
}
|
|
101
122
|
text := strings.TrimSpace(value)
|
|
102
|
-
if
|
|
103
|
-
|
|
123
|
+
if text == "" {
|
|
124
|
+
return nil, fmt.Errorf("locale %q has empty text", key)
|
|
104
125
|
}
|
|
105
126
|
if _, exists := result[code]; exists {
|
|
106
|
-
|
|
127
|
+
return nil, fmt.Errorf("duplicate locale mapping for short code %q", code)
|
|
107
128
|
}
|
|
108
129
|
result[code] = text
|
|
109
130
|
}
|
|
110
|
-
return result
|
|
131
|
+
return result, nil
|
|
111
132
|
}
|
|
112
133
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return map[string]T{}
|
|
116
|
-
}
|
|
134
|
+
// NormalizeLocaleRootedMap 将语言根键精确转换为内部短码并拒绝无效或冲突输入。
|
|
135
|
+
func NormalizeLocaleRootedMap[T any](input map[string]T) (map[string]T, error) {
|
|
117
136
|
result := make(map[string]T, len(input))
|
|
118
137
|
for key, value := range input {
|
|
119
138
|
code := NormalizeCode(key)
|
|
120
139
|
if code == "" {
|
|
121
|
-
|
|
140
|
+
return nil, fmt.Errorf("unsupported locale key %q", key)
|
|
122
141
|
}
|
|
123
142
|
if _, exists := result[code]; exists {
|
|
124
|
-
|
|
143
|
+
return nil, fmt.Errorf("duplicate locale mapping for short code %q", code)
|
|
125
144
|
}
|
|
126
145
|
result[code] = value
|
|
127
146
|
}
|
|
128
|
-
return result
|
|
147
|
+
return result, nil
|
|
129
148
|
}
|
|
130
149
|
|
|
150
|
+
// NormalizeRawTextMap 将 JSON 文本映射规范化为内部短码键。
|
|
131
151
|
func NormalizeRawTextMap(raw json.RawMessage) (json.RawMessage, error) {
|
|
132
152
|
if len(raw) == 0 {
|
|
133
153
|
return json.RawMessage(`{}`), nil
|
|
@@ -136,7 +156,13 @@ func NormalizeRawTextMap(raw json.RawMessage) (json.RawMessage, error) {
|
|
|
136
156
|
if err := json.Unmarshal(raw, &input); err != nil {
|
|
137
157
|
return nil, err
|
|
138
158
|
}
|
|
139
|
-
|
|
159
|
+
if input == nil {
|
|
160
|
+
return nil, fmt.Errorf("locale text map must be a JSON object")
|
|
161
|
+
}
|
|
162
|
+
normalized, err := NormalizeTextMap(input)
|
|
163
|
+
if err != nil {
|
|
164
|
+
return nil, err
|
|
165
|
+
}
|
|
140
166
|
payload, err := json.Marshal(normalized)
|
|
141
167
|
if err != nil {
|
|
142
168
|
return nil, err
|