@endora-commerce/cli 0.100.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/LICENSE +21 -0
- package/README.md +37 -0
- package/dist/bin/endora.d.ts +28 -0
- package/dist/bin/endora.d.ts.map +1 -0
- package/dist/bin/endora.js +926 -0
- package/dist/bin/endora.js.map +1 -0
- package/dist/check/estate.d.ts +189 -0
- package/dist/check/estate.d.ts.map +1 -0
- package/dist/check/estate.js +1037 -0
- package/dist/check/estate.js.map +1 -0
- package/dist/check/hosts.d.ts +25 -0
- package/dist/check/hosts.d.ts.map +1 -0
- package/dist/check/hosts.js +1347 -0
- package/dist/check/hosts.js.map +1 -0
- package/dist/check/index.d.ts +60 -0
- package/dist/check/index.d.ts.map +1 -0
- package/dist/check/index.js +111 -0
- package/dist/check/index.js.map +1 -0
- package/dist/check/layout.d.ts +136 -0
- package/dist/check/layout.d.ts.map +1 -0
- package/dist/check/layout.js +262 -0
- package/dist/check/layout.js.map +1 -0
- package/dist/check/ledger.d.ts +98 -0
- package/dist/check/ledger.d.ts.map +1 -0
- package/dist/check/ledger.js +173 -0
- package/dist/check/ledger.js.map +1 -0
- package/dist/check/peer-owners.d.ts +116 -0
- package/dist/check/peer-owners.d.ts.map +1 -0
- package/dist/check/peer-owners.js +225 -0
- package/dist/check/peer-owners.js.map +1 -0
- package/dist/check/report.d.ts +33 -0
- package/dist/check/report.d.ts.map +1 -0
- package/dist/check/report.js +107 -0
- package/dist/check/report.js.map +1 -0
- package/dist/check/run.d.ts +147 -0
- package/dist/check/run.d.ts.map +1 -0
- package/dist/check/run.js +111 -0
- package/dist/check/run.js.map +1 -0
- package/dist/checks.d.ts +17 -0
- package/dist/checks.d.ts.map +1 -0
- package/dist/checks.js +17 -0
- package/dist/checks.js.map +1 -0
- package/dist/dev/index.d.ts +83 -0
- package/dist/dev/index.d.ts.map +1 -0
- package/dist/dev/index.js +298 -0
- package/dist/dev/index.js.map +1 -0
- package/dist/generate/divergence.d.ts +38 -0
- package/dist/generate/divergence.d.ts.map +1 -0
- package/dist/generate/divergence.js +237 -0
- package/dist/generate/divergence.js.map +1 -0
- package/dist/generate/index.d.ts +90 -0
- package/dist/generate/index.d.ts.map +1 -0
- package/dist/generate/index.js +369 -0
- package/dist/generate/index.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/inputs/declaration.d.ts +18 -0
- package/dist/inputs/declaration.d.ts.map +1 -0
- package/dist/inputs/declaration.js +64 -0
- package/dist/inputs/declaration.js.map +1 -0
- package/dist/inputs/env-file.d.ts +73 -0
- package/dist/inputs/env-file.d.ts.map +1 -0
- package/dist/inputs/env-file.js +134 -0
- package/dist/inputs/env-file.js.map +1 -0
- package/dist/inputs/prompt.d.ts +21 -0
- package/dist/inputs/prompt.d.ts.map +1 -0
- package/dist/inputs/prompt.js +59 -0
- package/dist/inputs/prompt.js.map +1 -0
- package/dist/inputs/resolve.d.ts +163 -0
- package/dist/inputs/resolve.d.ts.map +1 -0
- package/dist/inputs/resolve.js +290 -0
- package/dist/inputs/resolve.js.map +1 -0
- package/dist/install/host.d.ts +27 -0
- package/dist/install/host.d.ts.map +1 -0
- package/dist/install/host.js +90 -0
- package/dist/install/host.js.map +1 -0
- package/dist/install/index.d.ts +173 -0
- package/dist/install/index.d.ts.map +1 -0
- package/dist/install/index.js +793 -0
- package/dist/install/index.js.map +1 -0
- package/dist/install/wizard.d.ts +144 -0
- package/dist/install/wizard.d.ts.map +1 -0
- package/dist/install/wizard.js +362 -0
- package/dist/install/wizard.js.map +1 -0
- package/dist/lib/admin-artefacts.d.ts +70 -0
- package/dist/lib/admin-artefacts.d.ts.map +1 -0
- package/dist/lib/admin-artefacts.js +354 -0
- package/dist/lib/admin-artefacts.js.map +1 -0
- package/dist/lib/admin-surfaces.d.ts +298 -0
- package/dist/lib/admin-surfaces.d.ts.map +1 -0
- package/dist/lib/admin-surfaces.js +669 -0
- package/dist/lib/admin-surfaces.js.map +1 -0
- package/dist/lib/delegated-composer.d.ts +85 -0
- package/dist/lib/delegated-composer.d.ts.map +1 -0
- package/dist/lib/delegated-composer.js +241 -0
- package/dist/lib/delegated-composer.js.map +1 -0
- package/dist/lib/divergence-artefacts.d.ts +305 -0
- package/dist/lib/divergence-artefacts.d.ts.map +1 -0
- package/dist/lib/divergence-artefacts.js +828 -0
- package/dist/lib/divergence-artefacts.js.map +1 -0
- package/dist/lib/divergence.d.ts +337 -0
- package/dist/lib/divergence.d.ts.map +1 -0
- package/dist/lib/divergence.js +1005 -0
- package/dist/lib/divergence.js.map +1 -0
- package/dist/lib/docs-artefacts.d.ts +395 -0
- package/dist/lib/docs-artefacts.d.ts.map +1 -0
- package/dist/lib/docs-artefacts.js +781 -0
- package/dist/lib/docs-artefacts.js.map +1 -0
- package/dist/lib/emitted-exports.d.ts +21 -0
- package/dist/lib/emitted-exports.d.ts.map +1 -0
- package/dist/lib/emitted-exports.js +96 -0
- package/dist/lib/emitted-exports.js.map +1 -0
- package/dist/lib/emitted-freshness.d.ts +112 -0
- package/dist/lib/emitted-freshness.d.ts.map +1 -0
- package/dist/lib/emitted-freshness.js +288 -0
- package/dist/lib/emitted-freshness.js.map +1 -0
- package/dist/lib/entity-index-artefact.d.ts +95 -0
- package/dist/lib/entity-index-artefact.d.ts.map +1 -0
- package/dist/lib/entity-index-artefact.js +210 -0
- package/dist/lib/entity-index-artefact.js.map +1 -0
- package/dist/lib/instance-build-inputs.d.ts +108 -0
- package/dist/lib/instance-build-inputs.d.ts.map +1 -0
- package/dist/lib/instance-build-inputs.js +165 -0
- package/dist/lib/instance-build-inputs.js.map +1 -0
- package/dist/lib/module-docs.d.ts +473 -0
- package/dist/lib/module-docs.d.ts.map +1 -0
- package/dist/lib/module-docs.js +711 -0
- package/dist/lib/module-docs.js.map +1 -0
- package/dist/lib/module-package-subpaths.d.ts +56 -0
- package/dist/lib/module-package-subpaths.d.ts.map +1 -0
- package/dist/lib/module-package-subpaths.js +223 -0
- package/dist/lib/module-package-subpaths.js.map +1 -0
- package/dist/lib/module-packages.d.ts +200 -0
- package/dist/lib/module-packages.d.ts.map +1 -0
- package/dist/lib/module-packages.js +580 -0
- package/dist/lib/module-packages.js.map +1 -0
- package/dist/lib/module-population.d.ts +129 -0
- package/dist/lib/module-population.d.ts.map +1 -0
- package/dist/lib/module-population.js +172 -0
- package/dist/lib/module-population.js.map +1 -0
- package/dist/lib/module-roots.d.ts +337 -0
- package/dist/lib/module-roots.d.ts.map +1 -0
- package/dist/lib/module-roots.js +586 -0
- package/dist/lib/module-roots.js.map +1 -0
- package/dist/lib/nested-checkouts.d.ts +33 -0
- package/dist/lib/nested-checkouts.d.ts.map +1 -0
- package/dist/lib/nested-checkouts.js +160 -0
- package/dist/lib/nested-checkouts.js.map +1 -0
- package/dist/lib/platform-root.d.ts +43 -0
- package/dist/lib/platform-root.d.ts.map +1 -0
- package/dist/lib/platform-root.js +134 -0
- package/dist/lib/platform-root.js.map +1 -0
- package/dist/lib/platform-surface.d.ts +235 -0
- package/dist/lib/platform-surface.d.ts.map +1 -0
- package/dist/lib/platform-surface.js +393 -0
- package/dist/lib/platform-surface.js.map +1 -0
- package/dist/lib/port-registrations.d.ts +223 -0
- package/dist/lib/port-registrations.d.ts.map +1 -0
- package/dist/lib/port-registrations.js +532 -0
- package/dist/lib/port-registrations.js.map +1 -0
- package/dist/lib/read-size.d.ts +154 -0
- package/dist/lib/read-size.d.ts.map +1 -0
- package/dist/lib/read-size.js +182 -0
- package/dist/lib/read-size.js.map +1 -0
- package/dist/lib/registration-owners.d.ts +79 -0
- package/dist/lib/registration-owners.d.ts.map +1 -0
- package/dist/lib/registration-owners.js +77 -0
- package/dist/lib/registration-owners.js.map +1 -0
- package/dist/lib/release-index.d.ts +53 -0
- package/dist/lib/release-index.d.ts.map +1 -0
- package/dist/lib/release-index.js +162 -0
- package/dist/lib/release-index.js.map +1 -0
- package/dist/lib/repeating-timers.d.ts +79 -0
- package/dist/lib/repeating-timers.d.ts.map +1 -0
- package/dist/lib/repeating-timers.js +189 -0
- package/dist/lib/repeating-timers.js.map +1 -0
- package/dist/lib/source-text.d.ts +34 -0
- package/dist/lib/source-text.d.ts.map +1 -0
- package/dist/lib/source-text.js +80 -0
- package/dist/lib/source-text.js.map +1 -0
- package/dist/lib/specifiers.d.ts +20 -0
- package/dist/lib/specifiers.d.ts.map +1 -0
- package/dist/lib/specifiers.js +130 -0
- package/dist/lib/specifiers.js.map +1 -0
- package/dist/lib/sql-tables.d.ts +166 -0
- package/dist/lib/sql-tables.d.ts.map +1 -0
- package/dist/lib/sql-tables.js +464 -0
- package/dist/lib/sql-tables.js.map +1 -0
- package/dist/lib/switchable-modules.d.ts +54 -0
- package/dist/lib/switchable-modules.d.ts.map +1 -0
- package/dist/lib/switchable-modules.js +104 -0
- package/dist/lib/switchable-modules.js.map +1 -0
- package/dist/lib/tailwind-sources.d.ts +136 -0
- package/dist/lib/tailwind-sources.d.ts.map +1 -0
- package/dist/lib/tailwind-sources.js +307 -0
- package/dist/lib/tailwind-sources.js.map +1 -0
- package/dist/lib/ui-layer.d.ts +54 -0
- package/dist/lib/ui-layer.d.ts.map +1 -0
- package/dist/lib/ui-layer.js +57 -0
- package/dist/lib/ui-layer.js.map +1 -0
- package/dist/lib/workspace-packages.d.ts +186 -0
- package/dist/lib/workspace-packages.d.ts.map +1 -0
- package/dist/lib/workspace-packages.js +351 -0
- package/dist/lib/workspace-packages.js.map +1 -0
- package/dist/new-instance/deploy.d.ts +211 -0
- package/dist/new-instance/deploy.d.ts.map +1 -0
- package/dist/new-instance/deploy.js +1381 -0
- package/dist/new-instance/deploy.js.map +1 -0
- package/dist/new-instance/docs-toolchain.d.ts +66 -0
- package/dist/new-instance/docs-toolchain.d.ts.map +1 -0
- package/dist/new-instance/docs-toolchain.js +69 -0
- package/dist/new-instance/docs-toolchain.js.map +1 -0
- package/dist/new-instance/host.d.ts +124 -0
- package/dist/new-instance/host.d.ts.map +1 -0
- package/dist/new-instance/host.js +276 -0
- package/dist/new-instance/host.js.map +1 -0
- package/dist/new-instance/index.d.ts +118 -0
- package/dist/new-instance/index.d.ts.map +1 -0
- package/dist/new-instance/index.js +567 -0
- package/dist/new-instance/index.js.map +1 -0
- package/dist/new-instance/modules.d.ts +188 -0
- package/dist/new-instance/modules.d.ts.map +1 -0
- package/dist/new-instance/modules.js +392 -0
- package/dist/new-instance/modules.js.map +1 -0
- package/dist/new-instance/template.d.ts +505 -0
- package/dist/new-instance/template.d.ts.map +1 -0
- package/dist/new-instance/template.js +1886 -0
- package/dist/new-instance/template.js.map +1 -0
- package/dist/new-module/emit.d.ts +67 -0
- package/dist/new-module/emit.d.ts.map +1 -0
- package/dist/new-module/emit.js +1393 -0
- package/dist/new-module/emit.js.map +1 -0
- package/dist/new-module/host.d.ts +67 -0
- package/dist/new-module/host.d.ts.map +1 -0
- package/dist/new-module/host.js +224 -0
- package/dist/new-module/host.js.map +1 -0
- package/dist/new-module/index.d.ts +32 -0
- package/dist/new-module/index.d.ts.map +1 -0
- package/dist/new-module/index.js +193 -0
- package/dist/new-module/index.js.map +1 -0
- package/dist/new-module/spec.d.ts +188 -0
- package/dist/new-module/spec.d.ts.map +1 -0
- package/dist/new-module/spec.js +404 -0
- package/dist/new-module/spec.js.map +1 -0
- package/dist/new-module/text.d.ts +15 -0
- package/dist/new-module/text.d.ts.map +1 -0
- package/dist/new-module/text.js +22 -0
- package/dist/new-module/text.js.map +1 -0
- package/dist/new-storefront/dockerfile.d.ts +20 -0
- package/dist/new-storefront/dockerfile.d.ts.map +1 -0
- package/dist/new-storefront/dockerfile.js +131 -0
- package/dist/new-storefront/dockerfile.js.map +1 -0
- package/dist/new-storefront/gitignore.d.ts +23 -0
- package/dist/new-storefront/gitignore.d.ts.map +1 -0
- package/dist/new-storefront/gitignore.js +39 -0
- package/dist/new-storefront/gitignore.js.map +1 -0
- package/dist/new-storefront/index.d.ts +93 -0
- package/dist/new-storefront/index.d.ts.map +1 -0
- package/dist/new-storefront/index.js +329 -0
- package/dist/new-storefront/index.js.map +1 -0
- package/dist/new-storefront/npmrc.d.ts +98 -0
- package/dist/new-storefront/npmrc.d.ts.map +1 -0
- package/dist/new-storefront/npmrc.js +217 -0
- package/dist/new-storefront/npmrc.js.map +1 -0
- package/dist/new-storefront/reference.d.ts +189 -0
- package/dist/new-storefront/reference.d.ts.map +1 -0
- package/dist/new-storefront/reference.js +430 -0
- package/dist/new-storefront/reference.js.map +1 -0
- package/dist/new-storefront/rewrite.d.ts +171 -0
- package/dist/new-storefront/rewrite.d.ts.map +1 -0
- package/dist/new-storefront/rewrite.js +701 -0
- package/dist/new-storefront/rewrite.js.map +1 -0
- package/dist/release-index.json +284 -0
- package/dist/rules/action-route-permissions.d.ts +141 -0
- package/dist/rules/action-route-permissions.d.ts.map +1 -0
- package/dist/rules/action-route-permissions.js +556 -0
- package/dist/rules/action-route-permissions.js.map +1 -0
- package/dist/rules/bundle-pairing.d.ts +74 -0
- package/dist/rules/bundle-pairing.d.ts.map +1 -0
- package/dist/rules/bundle-pairing.js +281 -0
- package/dist/rules/bundle-pairing.js.map +1 -0
- package/dist/rules/channel-resolution.d.ts +17 -0
- package/dist/rules/channel-resolution.d.ts.map +1 -0
- package/dist/rules/channel-resolution.js +382 -0
- package/dist/rules/channel-resolution.js.map +1 -0
- package/dist/rules/command-coverage.d.ts +210 -0
- package/dist/rules/command-coverage.d.ts.map +1 -0
- package/dist/rules/command-coverage.js +714 -0
- package/dist/rules/command-coverage.js.map +1 -0
- package/dist/rules/container-imports.d.ts +60 -0
- package/dist/rules/container-imports.d.ts.map +1 -0
- package/dist/rules/container-imports.js +158 -0
- package/dist/rules/container-imports.js.map +1 -0
- package/dist/rules/default-language-prose.d.ts +212 -0
- package/dist/rules/default-language-prose.d.ts.map +1 -0
- package/dist/rules/default-language-prose.js +710 -0
- package/dist/rules/default-language-prose.js.map +1 -0
- package/dist/rules/diacritic-folds.d.ts +238 -0
- package/dist/rules/diacritic-folds.d.ts.map +1 -0
- package/dist/rules/diacritic-folds.js +681 -0
- package/dist/rules/diacritic-folds.js.map +1 -0
- package/dist/rules/entity-tenant-classification.d.ts +171 -0
- package/dist/rules/entity-tenant-classification.d.ts.map +1 -0
- package/dist/rules/entity-tenant-classification.js +323 -0
- package/dist/rules/entity-tenant-classification.js.map +1 -0
- package/dist/rules/entry-presence.d.ts +142 -0
- package/dist/rules/entry-presence.d.ts.map +1 -0
- package/dist/rules/entry-presence.js +339 -0
- package/dist/rules/entry-presence.js.map +1 -0
- package/dist/rules/entry-scope.d.ts +91 -0
- package/dist/rules/entry-scope.d.ts.map +1 -0
- package/dist/rules/entry-scope.js +404 -0
- package/dist/rules/entry-scope.js.map +1 -0
- package/dist/rules/env-inputs.d.ts +222 -0
- package/dist/rules/env-inputs.d.ts.map +1 -0
- package/dist/rules/env-inputs.js +951 -0
- package/dist/rules/env-inputs.js.map +1 -0
- package/dist/rules/kernel-boundary.d.ts +37 -0
- package/dist/rules/kernel-boundary.d.ts.map +1 -0
- package/dist/rules/kernel-boundary.js +195 -0
- package/dist/rules/kernel-boundary.js.map +1 -0
- package/dist/rules/nul-bytes.d.ts +233 -0
- package/dist/rules/nul-bytes.d.ts.map +1 -0
- package/dist/rules/nul-bytes.js +332 -0
- package/dist/rules/nul-bytes.js.map +1 -0
- package/dist/rules/platform-surface.d.ts +479 -0
- package/dist/rules/platform-surface.d.ts.map +1 -0
- package/dist/rules/platform-surface.js +749 -0
- package/dist/rules/platform-surface.js.map +1 -0
- package/dist/rules/port-catches.d.ts +225 -0
- package/dist/rules/port-catches.d.ts.map +1 -0
- package/dist/rules/port-catches.js +1374 -0
- package/dist/rules/port-catches.js.map +1 -0
- package/dist/rules/port-shape.d.ts +213 -0
- package/dist/rules/port-shape.d.ts.map +1 -0
- package/dist/rules/port-shape.js +670 -0
- package/dist/rules/port-shape.js.map +1 -0
- package/dist/rules/queue-names.d.ts +108 -0
- package/dist/rules/queue-names.d.ts.map +1 -0
- package/dist/rules/queue-names.js +395 -0
- package/dist/rules/queue-names.js.map +1 -0
- package/dist/rules/singleton-identity.d.ts +205 -0
- package/dist/rules/singleton-identity.d.ts.map +1 -0
- package/dist/rules/singleton-identity.js +830 -0
- package/dist/rules/singleton-identity.js.map +1 -0
- package/dist/rules/subscribe-seam.d.ts +121 -0
- package/dist/rules/subscribe-seam.d.ts.map +1 -0
- package/dist/rules/subscribe-seam.js +594 -0
- package/dist/rules/subscribe-seam.js.map +1 -0
- package/dist/rules/transaction-context.d.ts +40 -0
- package/dist/rules/transaction-context.d.ts.map +1 -0
- package/dist/rules/transaction-context.js +294 -0
- package/dist/rules/transaction-context.js.map +1 -0
- package/package.json +59 -0
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CI check — **English is the default**, so a prose literal in another language,
|
|
3
|
+
* outside a per-language structure, in a module's own sources is a defect
|
|
4
|
+
* (`specs/094-translation-boundary/`,
|
|
5
|
+
* `contracts/default-language-prose-check.md`).
|
|
6
|
+
*
|
|
7
|
+
* The **second signal** for `contracts/translation-boundary.md` § 3 clause 1,
|
|
8
|
+
* and a check of its own rather than a widening of either instrument that looks
|
|
9
|
+
* like its home. `../research.md` § 8 D-8 records why:
|
|
10
|
+
* `check:untranslated-delivery`'s predicate is *a delivery call that passes
|
|
11
|
+
* prose and no key*, and a `return` is not a call, an array push is not a call
|
|
12
|
+
* and a field assignment is not a call; `check:language`'s scope is **comments**
|
|
13
|
+
* by constitutional design (Principle VIII v3.0.0 permits a string literal in
|
|
14
|
+
* any language), and widening it would make every `{ en, pl }` map a violation.
|
|
15
|
+
*
|
|
16
|
+
* ## The predicate keys on the language of the string, not on how it is carried
|
|
17
|
+
*
|
|
18
|
+
* `specs/093-backend-delivered-prose/` named its population after three delivery
|
|
19
|
+
* **mechanisms** — a notification port, a mailer, a pdfmake renderer — and
|
|
20
|
+
* mechanisms are unbounded: six more shapes were in the tree the whole time (a
|
|
21
|
+
* `return` from a route handler, a push into a persisted operation log, a
|
|
22
|
+
* persisted document line item, a Polish default behind an operator-editable
|
|
23
|
+
* prop, a CMS seed, a bilingual ternary), and **30 of the 36 sites were outside
|
|
24
|
+
* the survey**. A string does not become user-facing by being carried; it
|
|
25
|
+
* becomes user-facing by being read.
|
|
26
|
+
*
|
|
27
|
+
* So this check asks one question of the literal itself, and it answers for the
|
|
28
|
+
* seventh shape nobody has written yet.
|
|
29
|
+
*
|
|
30
|
+
* ## The asymmetry is the rule rather than a limitation
|
|
31
|
+
*
|
|
32
|
+
* It finds non-English prose and says nothing about English prose. Under a
|
|
33
|
+
* ruling that makes English the default, an English literal is not a finding —
|
|
34
|
+
* so the check enforces the ruling **as written**. It is not an instrument for
|
|
35
|
+
* "is this string translated"; that is `check:untranslated-delivery`'s question
|
|
36
|
+
* for case 2 and `tsc`'s for the storefront.
|
|
37
|
+
*
|
|
38
|
+
* ## What "outside a per-language structure" means, derived from the syntax
|
|
39
|
+
*
|
|
40
|
+
* A filename rule is refused (`check:diacritic-folds`' exemption discipline):
|
|
41
|
+
* the exemption is a property of the **structure**, so a per-language map in a
|
|
42
|
+
* file with an ordinary name is exempt and a bare literal in a file called
|
|
43
|
+
* `translations.pl-PL.ts` is not. Three shapes, all decided from the syntax:
|
|
44
|
+
*
|
|
45
|
+
* * **`language-keyed-property`** — an enclosing property assignment, at any
|
|
46
|
+
* depth, whose key is a shipped language or a locale whose primary subtag is
|
|
47
|
+
* one: `'pl-PL': { … }`, `pl: 'Przesyłka wysłana'`. It is one shape and not
|
|
48
|
+
* two, because § 1.2's "an English sibling at the same depth under a
|
|
49
|
+
* language key" is the same fact read from the other member — the key that
|
|
50
|
+
* reaches *this* literal is already a language.
|
|
51
|
+
* * **`language-declaring-object`** — the enclosing object literal declares
|
|
52
|
+
* which language it is written in, in a property whose **name** is one of
|
|
53
|
+
* {@link LANGUAGE_DECLARING_FIELDS} and whose value is a shipped language.
|
|
54
|
+
* A `dictionaries` seed row is the shape —
|
|
55
|
+
* `{ entryType: 'currency', entryCode: 'PLN', languageCode: 'pl-PL', label: '…' }`
|
|
56
|
+
* — and the field **name** is load-bearing: the same row's
|
|
57
|
+
* `entryCode: 'en-US'` is the row's *subject*, not its language, so a rule
|
|
58
|
+
* reading "any sibling holding a language code" would exempt a countries
|
|
59
|
+
* seed keyed `code: 'pl'` as well.
|
|
60
|
+
* * **`single-language-file`** — the file's own declared subject is one
|
|
61
|
+
* language, evidenced by a language **code** in a declared export name
|
|
62
|
+
* (`export const PL_MESSAGES`, `messagesPlPL`). The other evidence § 1.2
|
|
63
|
+
* names, a manifest-declared bundle path, is vacuous here rather than
|
|
64
|
+
* unimplemented: a bundle directory holds `en.json` and `pl.json`, and this
|
|
65
|
+
* walk opens no JSON.
|
|
66
|
+
*
|
|
67
|
+
* ## Detection, and its declared bound
|
|
68
|
+
*
|
|
69
|
+
* Non-English detection is **Polish only**, by diacritic plus a stopword list,
|
|
70
|
+
* reusing `scripts/check-language.sh`'s machinery and its `proper_nouns` list
|
|
71
|
+
* ({@link POLISH_PROPER_NOUNS}, held to the shell list by the companion test).
|
|
72
|
+
* The bound is declared rather than discovered: a check that claimed to detect
|
|
73
|
+
* "any non-English prose" and detected Polish would be the thing this repository
|
|
74
|
+
* refuses. Two consequences:
|
|
75
|
+
*
|
|
76
|
+
* * **Polish prose carrying no diacritic and no stopword is invisible.**
|
|
77
|
+
* `'Nowa Organizacja'` is the measured example, and it is why the stopword
|
|
78
|
+
* list sits *beside* the diacritic class rather than instead of it:
|
|
79
|
+
* `'Zamówienie nie zostało opłacone'` is caught by `nie` when its diacritics
|
|
80
|
+
* are typed as `Zamowienie`.
|
|
81
|
+
* * **A third shipped language needs its own detector.** The set is derived
|
|
82
|
+
* from `SUPPORTED_LANGUAGES`, so the gap is visible the moment it grows: the
|
|
83
|
+
* `read:` line reconciles the two, and a shipped language with no detector
|
|
84
|
+
* is exit 2 rather than a silent implied coverage claim.
|
|
85
|
+
*
|
|
86
|
+
* ## What it does not see, stated rather than discovered later
|
|
87
|
+
*
|
|
88
|
+
* * **A string assembled from fragments**, as a whole. Each fragment that is
|
|
89
|
+
* itself Polish prose is still a site — the two consent seeders are two
|
|
90
|
+
* concatenated literals each — but a sentence split so that no fragment is
|
|
91
|
+
* prose on its own is invisible.
|
|
92
|
+
* * **A single-word literal**, outside the prop-default position. A currency
|
|
93
|
+
* numeral table (`'pięć'`, `'sześćdziesiąt'`) and a unit (`'usł.'`) are not
|
|
94
|
+
* prose, and a token-count floor is the only thing separating them from a
|
|
95
|
+
* label; in the prop-default position the *position* supplies what the token
|
|
96
|
+
* count otherwise has to infer, so `str(props, 'labelPaid', 'Zapłacono')` is
|
|
97
|
+
* a finding and the numeral table is not.
|
|
98
|
+
* * **A module's `migrations/`.** Two reasons, and the first is sufficient: an
|
|
99
|
+
* applied migration cannot be edited, so the repair for a Polish seed there
|
|
100
|
+
* is a *new* migration and a ledger entry over the site would never drain.
|
|
101
|
+
* The second is that this tree's migration prose is per-language JSON inside
|
|
102
|
+
* a SQL string (`'{"en-US":"Card","pl-PL":"Karta płatnicza"}'::jsonb`), which
|
|
103
|
+
* a TypeScript structure analysis cannot see into and would report as bare
|
|
104
|
+
* Polish. The CMS consent seeders are services, not migrations, and stay in.
|
|
105
|
+
* * **A string read from a database or a fixture**, prose in a **comment**
|
|
106
|
+
* (`check:language`'s, deliberately), and `admin/` and `storefront/`
|
|
107
|
+
* sources, which are `i18n:hardcoded`'s and `tsc`'s respectively. `.tsx` is
|
|
108
|
+
* out of the walk for that last reason.
|
|
109
|
+
*
|
|
110
|
+
* ## The ledger
|
|
111
|
+
*
|
|
112
|
+
* `NON_ENGLISH_DEFAULTS`, sharded per module under {@link LEDGER_ROOT}
|
|
113
|
+
* (`scripts/ledgers/non-english-defaults/`), one file per module exporting an
|
|
114
|
+
* `entries` record, two-way, keyed `(file, digest of the literal)` — a **digest, never a line**, so an insertion above the site does not
|
|
115
|
+
* red it and an edited sentence does.
|
|
116
|
+
*
|
|
117
|
+
* It opens holding the sites standing when it lands and is expected to
|
|
118
|
+
* **empty**. An entry says why a non-English default is right; the only entries
|
|
119
|
+
* that should survive are domain-mandated ones — the KSeF offline marking on a
|
|
120
|
+
* Polish invoice is the standing example.
|
|
121
|
+
*
|
|
122
|
+
* Usage: `tsx scripts/check-default-language-prose.ts [--list]`
|
|
123
|
+
* Exit 0 = every prose literal outside a per-language structure is English;
|
|
124
|
+
* exit 1 = at least one is not, or an entry no longer describes one; exit 2 =
|
|
125
|
+
* the run could not see the population it judges — no shipped language, no
|
|
126
|
+
* registered module, a module walk that came back short, no literal read at
|
|
127
|
+
* all, or no detector for a shipped language.
|
|
128
|
+
*/
|
|
129
|
+
import { createHash } from 'node:crypto';
|
|
130
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
131
|
+
import { join } from 'node:path';
|
|
132
|
+
import { pathToFileURL } from 'node:url';
|
|
133
|
+
import ts from 'typescript';
|
|
134
|
+
/**
|
|
135
|
+
* Polish diacritics — `scripts/check-language.sh`'s own `pattern`, character for
|
|
136
|
+
* character, so the two instruments cannot come to disagree about what a Polish
|
|
137
|
+
* letter is. `ó` is in it although it is shared with Icelandic and Spanish:
|
|
138
|
+
* dropping it would make `'Potwierdzenie zamówienia'` invisible, which is a
|
|
139
|
+
* larger hole than the two English currency labels it costs.
|
|
140
|
+
*/
|
|
141
|
+
export const POLISH_DIACRITICS = 'ąĄćĆęĘłŁńŃóÓśŚźŹżŻ';
|
|
142
|
+
const POLISH_DIACRITIC_RE = new RegExp(`[${POLISH_DIACRITICS}]`, 'u');
|
|
143
|
+
/**
|
|
144
|
+
* Polish function words, for the prose that carries no diacritic.
|
|
145
|
+
*
|
|
146
|
+
* Function words only, and deliberately: a domain vocabulary would be a
|
|
147
|
+
* population defined by the presence of the words somebody thought to enumerate
|
|
148
|
+
* (issue #244). Every entry is a form that is not also an English word — `pod`,
|
|
149
|
+
* `ten`, `to`, `do`, `on`, `z` and `we` are all excluded for that reason, and
|
|
150
|
+
* their absence is why `'Data wystawienia'` is one of the blind spots § 1.3
|
|
151
|
+
* declares. The exclusions are measured rather than assumed, twice:
|
|
152
|
+
*
|
|
153
|
+
* * **`we`** (Polish "in") was in the list for one run and reported five
|
|
154
|
+
* English sentences, `'…and "we do not want files" is not a business
|
|
155
|
+
* decision'` among them.
|
|
156
|
+
* * **`w`** (Polish "in") is the single most useful Polish function word and is
|
|
157
|
+
* out for a different reason: it is **one letter**, and a one-letter token is
|
|
158
|
+
* not a word in a tree full of SQL aliases and XML namespaces. Measured: it
|
|
159
|
+
* found two real sites (`'Faktura wystawiona w trybie offline.'` and
|
|
160
|
+
* `'Numer w KSeF'`, both diacritic-free Polish) and eight wrong ones — a
|
|
161
|
+
* `select w.id, w.code …` join alias and seven `http://www.w3.org/…`
|
|
162
|
+
* signature algorithm URIs. Its absence is the second half of § 1.3's
|
|
163
|
+
* declared bound, and the two sites it costs are Polish legal artefacts a
|
|
164
|
+
* ledger entry would keep either way.
|
|
165
|
+
*
|
|
166
|
+
* So the stopword pass keeps {@link proseTokens}' floor: a token is a word when
|
|
167
|
+
* it has two letters.
|
|
168
|
+
*/
|
|
169
|
+
export const POLISH_STOPWORDS = [
|
|
170
|
+
'aby',
|
|
171
|
+
'albo',
|
|
172
|
+
'bardzo',
|
|
173
|
+
'bez',
|
|
174
|
+
'czy',
|
|
175
|
+
'dla',
|
|
176
|
+
'gdy',
|
|
177
|
+
'gdzie',
|
|
178
|
+
'jak',
|
|
179
|
+
'jako',
|
|
180
|
+
'jest',
|
|
181
|
+
'jeszcze',
|
|
182
|
+
'jeśli',
|
|
183
|
+
'już',
|
|
184
|
+
'która',
|
|
185
|
+
'które',
|
|
186
|
+
'który',
|
|
187
|
+
'lub',
|
|
188
|
+
'może',
|
|
189
|
+
'można',
|
|
190
|
+
'nad',
|
|
191
|
+
'nasz',
|
|
192
|
+
'nasza',
|
|
193
|
+
'nasze',
|
|
194
|
+
'nie',
|
|
195
|
+
'niż',
|
|
196
|
+
'oraz',
|
|
197
|
+
'przez',
|
|
198
|
+
'przy',
|
|
199
|
+
'się',
|
|
200
|
+
'są',
|
|
201
|
+
'także',
|
|
202
|
+
'tego',
|
|
203
|
+
'temu',
|
|
204
|
+
'tylko',
|
|
205
|
+
'tym',
|
|
206
|
+
'twoja',
|
|
207
|
+
'twoje',
|
|
208
|
+
'twój',
|
|
209
|
+
'wszystkie',
|
|
210
|
+
'wtedy',
|
|
211
|
+
'więc',
|
|
212
|
+
'zostało',
|
|
213
|
+
'zostały',
|
|
214
|
+
'została',
|
|
215
|
+
'został',
|
|
216
|
+
'ze',
|
|
217
|
+
'że',
|
|
218
|
+
'żeby',
|
|
219
|
+
];
|
|
220
|
+
const STOPWORDS = new Set(POLISH_STOPWORDS);
|
|
221
|
+
/**
|
|
222
|
+
* Polish proper nouns that stay Polish inside English prose — the list
|
|
223
|
+
* `scripts/check-language.sh` carries, blanked before the detector runs for the
|
|
224
|
+
* same reason it blanks them: an English sentence routinely names a Polish
|
|
225
|
+
* institution, and translating the name would make it wrong rather than more
|
|
226
|
+
* English.
|
|
227
|
+
*
|
|
228
|
+
* Held to the shell list, in both directions, by the companion test. Matched
|
|
229
|
+
* **case-sensitively and as a whole phrase**, exactly as the shell does, so
|
|
230
|
+
* `'Metoda płatności'` — an invoice label with a lower-case second word — is
|
|
231
|
+
* still judged while the feature name `Metoda Płatności` is not.
|
|
232
|
+
*/
|
|
233
|
+
export const POLISH_PROPER_NOUNS = [
|
|
234
|
+
'Ministerstwo Finansów',
|
|
235
|
+
'Biała lista',
|
|
236
|
+
'Metoda Płatności',
|
|
237
|
+
'Szybkie Zamówienia',
|
|
238
|
+
];
|
|
239
|
+
/**
|
|
240
|
+
* Property names whose value declares which language the enclosing object is
|
|
241
|
+
* written in — the `language-declaring-object` exemption.
|
|
242
|
+
*
|
|
243
|
+
* A vocabulary and not a derivation, because there is nothing in the tree that
|
|
244
|
+
* declares it; it is kept to the four spellings that mean exactly this, and the
|
|
245
|
+
* reason it cannot be widened to "a sibling holding a language code" is in the
|
|
246
|
+
* header: a seed row's `entryCode: 'en-US'` is the row's subject.
|
|
247
|
+
*/
|
|
248
|
+
export const LANGUAGE_DECLARING_FIELDS = [
|
|
249
|
+
'lang',
|
|
250
|
+
'language',
|
|
251
|
+
'languageCode',
|
|
252
|
+
'locale',
|
|
253
|
+
];
|
|
254
|
+
const DECLARING_FIELDS = new Set(LANGUAGE_DECLARING_FIELDS);
|
|
255
|
+
/** The languages a detector exists for. Polish, and § 1.3 declares the bound. */
|
|
256
|
+
export const DETECTED_LANGUAGES = ['pl'];
|
|
257
|
+
// --- the detector ----------------------------------------------------------
|
|
258
|
+
/** The word tokens a literal holds — letters only, so a placeholder splits. */
|
|
259
|
+
export function proseTokens(text) {
|
|
260
|
+
return [...text.matchAll(/\p{L}[\p{L}'’-]*/gu)]
|
|
261
|
+
.map((match) => match[0])
|
|
262
|
+
.filter((token) => token.length >= 2);
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Whether the literal is prose at all.
|
|
266
|
+
*
|
|
267
|
+
* Two word tokens, or one in the prop-default position — see the header: there
|
|
268
|
+
* the position establishes that the string is a label a human reads, which is
|
|
269
|
+
* what the token count otherwise has to infer.
|
|
270
|
+
*/
|
|
271
|
+
export function isProse(text, inPropDefault = false) {
|
|
272
|
+
const tokens = proseTokens(text);
|
|
273
|
+
return inPropDefault ? tokens.length >= 1 : tokens.length >= 2;
|
|
274
|
+
}
|
|
275
|
+
/** The proper nouns removed, so a citation is not read as prose. */
|
|
276
|
+
export function blankProperNouns(text) {
|
|
277
|
+
let out = text;
|
|
278
|
+
for (const noun of POLISH_PROPER_NOUNS)
|
|
279
|
+
out = out.split(noun).join(' ');
|
|
280
|
+
return out;
|
|
281
|
+
}
|
|
282
|
+
/** Whether the literal holds a Polish function word. */
|
|
283
|
+
export function hasPolishStopword(text) {
|
|
284
|
+
return proseTokens(text.toLowerCase()).some((token) => STOPWORDS.has(token));
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* The non-English language this literal is in, or `null`.
|
|
288
|
+
*
|
|
289
|
+
* `languages` gates the answer: a detector for a language the platform does not
|
|
290
|
+
* ship would report a finding nobody can act on.
|
|
291
|
+
*/
|
|
292
|
+
export function detectLanguage(text, languages) {
|
|
293
|
+
const shipped = new Set(languages.map(primarySubtag));
|
|
294
|
+
const cleaned = blankProperNouns(text);
|
|
295
|
+
if (shipped.has('pl') && (POLISH_DIACRITIC_RE.test(cleaned) || hasPolishStopword(cleaned))) {
|
|
296
|
+
return 'pl';
|
|
297
|
+
}
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
/** `pl-PL` → `pl`, and `PL` → `pl`. Case and region folded, nothing else. */
|
|
301
|
+
export function primarySubtag(code) {
|
|
302
|
+
const cut = code.indexOf('-');
|
|
303
|
+
return (cut === -1 ? code : code.slice(0, cut)).toLowerCase();
|
|
304
|
+
}
|
|
305
|
+
/** Whether a property key or export-name token names a shipped language. */
|
|
306
|
+
function namesLanguage(token, languages) {
|
|
307
|
+
const shipped = new Set(languages.map(primarySubtag));
|
|
308
|
+
return shipped.has(primarySubtag(token));
|
|
309
|
+
}
|
|
310
|
+
// --- the structure analysis ------------------------------------------------
|
|
311
|
+
/**
|
|
312
|
+
* What an interpolation collapses to.
|
|
313
|
+
*
|
|
314
|
+
* Written as an escape and not as the character, because a raw control byte in
|
|
315
|
+
* a source file is invisible in every diff that would reveal it — the shape
|
|
316
|
+
* `check:nul-bytes` exists for. An ellipsis rather than a space: it breaks a
|
|
317
|
+
* token, so `` `Kwota${x}netto` `` reads as two words rather than one, and it
|
|
318
|
+
* prints legibly in a finding and in a ledger entry's own label.
|
|
319
|
+
*/
|
|
320
|
+
const INTERPOLATION_PLACEHOLDER = '\u2026';
|
|
321
|
+
/** The text of a literal node, with an interpolation collapsed to a token break. */
|
|
322
|
+
function literalTextOf(node) {
|
|
323
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))
|
|
324
|
+
return node.text;
|
|
325
|
+
if (ts.isTemplateExpression(node)) {
|
|
326
|
+
return (node.head.text +
|
|
327
|
+
node.templateSpans
|
|
328
|
+
.map((span) => `${INTERPOLATION_PLACEHOLDER}${span.literal.text}`)
|
|
329
|
+
.join(''));
|
|
330
|
+
}
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Whether the literal is in a position where it is not prose at all: a module
|
|
335
|
+
* specifier, a property **key**, a type, an enum member name.
|
|
336
|
+
*
|
|
337
|
+
* A key is excluded rather than judged because `{ 'Faktura korygująca': … }`
|
|
338
|
+
* would be a data shape, not a sentence — and because the language-keyed
|
|
339
|
+
* exemption reads keys itself.
|
|
340
|
+
*/
|
|
341
|
+
function isNonProsePosition(node) {
|
|
342
|
+
const parent = node.parent;
|
|
343
|
+
if (parent === undefined)
|
|
344
|
+
return true;
|
|
345
|
+
if (ts.isImportDeclaration(parent) || ts.isExportDeclaration(parent))
|
|
346
|
+
return true;
|
|
347
|
+
if (ts.isImportTypeNode(parent) || ts.isModuleDeclaration(parent))
|
|
348
|
+
return true;
|
|
349
|
+
if (ts.isLiteralTypeNode(parent))
|
|
350
|
+
return true;
|
|
351
|
+
if (ts.isPropertyAssignment(parent) && parent.name === node)
|
|
352
|
+
return true;
|
|
353
|
+
if (ts.isPropertySignature(parent) && parent.name === node)
|
|
354
|
+
return true;
|
|
355
|
+
if (ts.isEnumMember(parent) && parent.name === node)
|
|
356
|
+
return true;
|
|
357
|
+
if (ts.isComputedPropertyName(parent))
|
|
358
|
+
return true;
|
|
359
|
+
if (ts.isExternalModuleReference(parent))
|
|
360
|
+
return true;
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Whether the literal sits in the **default** position of an operator-editable
|
|
365
|
+
* prop read.
|
|
366
|
+
*
|
|
367
|
+
* Two shapes, both read off the call rather than off a callee's name — a name
|
|
368
|
+
* rule would answer for `str(` and stop answering for whatever the next module
|
|
369
|
+
* calls its accessor:
|
|
370
|
+
*
|
|
371
|
+
* * the **last** argument of a call taking three or more, at least one of the
|
|
372
|
+
* earlier ones being a string literal (the prop key) —
|
|
373
|
+
* `str(props, 'labelSaleDate', 'Data sprzedaży')`;
|
|
374
|
+
* * the right operand of a `??` or `||` whose left operand is a property or
|
|
375
|
+
* element access — `props['labelSaleDate'] ?? 'Data sprzedaży'`.
|
|
376
|
+
*
|
|
377
|
+
* A ternary default (`typeof props['text'] === 'string' ? … : 'Dziękujemy…'`) is
|
|
378
|
+
* outside it and lands in `non-english-default` instead: recognising it needs
|
|
379
|
+
* the condition and the branch to be tied to one property, and the finding it
|
|
380
|
+
* would move is reported either way.
|
|
381
|
+
*/
|
|
382
|
+
function isPropDefaultPosition(node) {
|
|
383
|
+
const parent = node.parent;
|
|
384
|
+
if (parent === undefined)
|
|
385
|
+
return false;
|
|
386
|
+
if (ts.isCallExpression(parent)) {
|
|
387
|
+
const args = parent.arguments;
|
|
388
|
+
if (args.length < 3)
|
|
389
|
+
return false;
|
|
390
|
+
if (args[args.length - 1] !== node)
|
|
391
|
+
return false;
|
|
392
|
+
return args
|
|
393
|
+
.slice(0, -1)
|
|
394
|
+
.some((argument) => ts.isStringLiteral(argument) || ts.isNoSubstitutionTemplateLiteral(argument));
|
|
395
|
+
}
|
|
396
|
+
if (ts.isBinaryExpression(parent) && parent.right === node) {
|
|
397
|
+
const operator = parent.operatorToken.kind;
|
|
398
|
+
if (operator !== ts.SyntaxKind.QuestionQuestionToken &&
|
|
399
|
+
operator !== ts.SyntaxKind.BarBarToken) {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
return (ts.isPropertyAccessExpression(parent.left) || ts.isElementAccessExpression(parent.left));
|
|
403
|
+
}
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
/** Every name a file declares as an export, for the `single-language-file` shape. */
|
|
407
|
+
export function declaredExportNames(sf) {
|
|
408
|
+
const names = [];
|
|
409
|
+
const exported = (node) => ts.canHaveModifiers(node) &&
|
|
410
|
+
(ts.getModifiers(node) ?? []).some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
411
|
+
for (const statement of sf.statements) {
|
|
412
|
+
if (ts.isVariableStatement(statement) && exported(statement)) {
|
|
413
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
414
|
+
if (ts.isIdentifier(declaration.name))
|
|
415
|
+
names.push(declaration.name.text);
|
|
416
|
+
}
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
if ((ts.isFunctionDeclaration(statement) ||
|
|
420
|
+
ts.isClassDeclaration(statement) ||
|
|
421
|
+
ts.isInterfaceDeclaration(statement) ||
|
|
422
|
+
ts.isTypeAliasDeclaration(statement) ||
|
|
423
|
+
ts.isEnumDeclaration(statement)) &&
|
|
424
|
+
exported(statement) &&
|
|
425
|
+
statement.name !== undefined) {
|
|
426
|
+
names.push(statement.name.text);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return names;
|
|
430
|
+
}
|
|
431
|
+
/** `POLISH_TRANSLATION_SEED` → `['POLISH', 'TRANSLATION', 'SEED']`. */
|
|
432
|
+
function nameTokens(name) {
|
|
433
|
+
return name
|
|
434
|
+
.split(/[_\-.]/)
|
|
435
|
+
.flatMap((part) => part.split(/(?<=[a-z0-9])(?=[A-Z])/))
|
|
436
|
+
.filter((part) => part.length > 0);
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Whether the file's own declared subject is one language — § 1.2's third shape.
|
|
440
|
+
*
|
|
441
|
+
* The evidence is a language **code** in a declared export name. It deliberately
|
|
442
|
+
* does not fire on `POLISH_TRANSLATION_SEED`, which is a language *name*: that
|
|
443
|
+
* file is exempt through `language-declaring-object` instead, from the
|
|
444
|
+
* `languageCode: 'pl-PL'` its rows carry, which is the structural answer and
|
|
445
|
+
* needs no mapping from a code to an English word.
|
|
446
|
+
*/
|
|
447
|
+
export function isSingleLanguageFile(sf, languages) {
|
|
448
|
+
return declaredExportNames(sf).some((name) => nameTokens(name).some((token) => namesLanguage(token, languages)));
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Where the literal sits relative to the per-language structures enclosing it.
|
|
452
|
+
*
|
|
453
|
+
* A computed key anywhere on the path is `unclassifiable` and **not** a skip
|
|
454
|
+
* (issue #113): `{ [language]: 'Przesyłka wysłana' }` may or may not be a
|
|
455
|
+
* per-language map, and treating it as exempt is the direction that agrees with
|
|
456
|
+
* the defect.
|
|
457
|
+
*/
|
|
458
|
+
export function placementOf(node, languages, singleLanguageFile) {
|
|
459
|
+
let unclassifiable = false;
|
|
460
|
+
for (let current = node.parent; current; current = current.parent) {
|
|
461
|
+
if (ts.isPropertyAssignment(current)) {
|
|
462
|
+
if (ts.isComputedPropertyName(current.name)) {
|
|
463
|
+
unclassifiable = true;
|
|
464
|
+
}
|
|
465
|
+
else if ((ts.isIdentifier(current.name) ||
|
|
466
|
+
ts.isStringLiteral(current.name) ||
|
|
467
|
+
ts.isNoSubstitutionTemplateLiteral(current.name)) &&
|
|
468
|
+
namesLanguage(current.name.text, languages)) {
|
|
469
|
+
return 'language-keyed-property';
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (ts.isObjectLiteralExpression(current) && declaresItsLanguage(current, languages)) {
|
|
473
|
+
return 'language-declaring-object';
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
if (singleLanguageFile)
|
|
477
|
+
return 'single-language-file';
|
|
478
|
+
return unclassifiable ? 'unclassifiable' : 'outside';
|
|
479
|
+
}
|
|
480
|
+
/** Whether the object literal names the language it is written in. */
|
|
481
|
+
function declaresItsLanguage(object, languages) {
|
|
482
|
+
return object.properties.some((property) => {
|
|
483
|
+
if (!ts.isPropertyAssignment(property))
|
|
484
|
+
return false;
|
|
485
|
+
const name = ts.isIdentifier(property.name)
|
|
486
|
+
? property.name.text
|
|
487
|
+
: ts.isStringLiteral(property.name)
|
|
488
|
+
? property.name.text
|
|
489
|
+
: null;
|
|
490
|
+
if (name === null || !DECLARING_FIELDS.has(name))
|
|
491
|
+
return false;
|
|
492
|
+
const value = property.initializer;
|
|
493
|
+
return ((ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value)) &&
|
|
494
|
+
namesLanguage(value.text, languages));
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
/** Every non-English prose literal in one file, and how many it classified. */
|
|
498
|
+
export function analyzeSource(file, source, languages) {
|
|
499
|
+
const sf = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
500
|
+
const singleLanguageFile = isSingleLanguageFile(sf, languages);
|
|
501
|
+
const sites = [];
|
|
502
|
+
let classified = 0;
|
|
503
|
+
const visit = (node) => {
|
|
504
|
+
const text = literalTextOf(node);
|
|
505
|
+
if (text !== null && !isNonProsePosition(node)) {
|
|
506
|
+
classified += 1;
|
|
507
|
+
const propDefault = isPropDefaultPosition(node);
|
|
508
|
+
if (isProse(text, propDefault)) {
|
|
509
|
+
const language = detectLanguage(text, languages);
|
|
510
|
+
if (language !== null) {
|
|
511
|
+
const placement = placementOf(node, languages, singleLanguageFile);
|
|
512
|
+
if (placement === 'outside' || placement === 'unclassifiable') {
|
|
513
|
+
sites.push({
|
|
514
|
+
file,
|
|
515
|
+
line: sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1,
|
|
516
|
+
text,
|
|
517
|
+
language,
|
|
518
|
+
placement,
|
|
519
|
+
kind: placement === 'unclassifiable'
|
|
520
|
+
? 'unclassifiable-literal'
|
|
521
|
+
: propDefault
|
|
522
|
+
? 'polish-default-behind-a-prop'
|
|
523
|
+
: 'non-english-default',
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
ts.forEachChild(node, visit);
|
|
530
|
+
};
|
|
531
|
+
visit(sf);
|
|
532
|
+
return { sites, classified };
|
|
533
|
+
}
|
|
534
|
+
/** The digest half of a ledger key — never a line (§ 2.1). */
|
|
535
|
+
export function literalDigest(text) {
|
|
536
|
+
return createHash('sha256').update(text, 'utf8').digest('hex').slice(0, 12);
|
|
537
|
+
}
|
|
538
|
+
/** `<file>:<digest>`, so an insertion above the site does not red it. */
|
|
539
|
+
export function ledgerKey(site) {
|
|
540
|
+
return `${site.file}:${literalDigest(site.text)}`;
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* The findings, over the sources and shipped languages handed in.
|
|
544
|
+
*
|
|
545
|
+
* Pure: a red proof enters here, over **source text**, which is where a real run
|
|
546
|
+
* enters (issue #130). A fixture handing in a classified literal would prove the
|
|
547
|
+
* reporter and leave the parser, the position filter, the detector and the whole
|
|
548
|
+
* structure analysis unproven.
|
|
549
|
+
*/
|
|
550
|
+
export function checkDefaultLanguageProse(input, shards = [], moduleOf = () => null) {
|
|
551
|
+
const findings = [];
|
|
552
|
+
let classified = 0;
|
|
553
|
+
for (const [file, source] of input.sources) {
|
|
554
|
+
const analysis = analyzeSource(file, source, input.languages);
|
|
555
|
+
findings.push(...analysis.sites);
|
|
556
|
+
classified += analysis.classified;
|
|
557
|
+
}
|
|
558
|
+
const owners = new Map();
|
|
559
|
+
const reasons = new Map();
|
|
560
|
+
for (const shard of shards) {
|
|
561
|
+
for (const key of Object.keys(shard.entries)) {
|
|
562
|
+
owners.set(key, shard.moduleId);
|
|
563
|
+
reasons.set(key, shard.entries[key] ?? '');
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
const seen = new Set();
|
|
567
|
+
const violations = [];
|
|
568
|
+
const ledgered = [];
|
|
569
|
+
const misfiled = [];
|
|
570
|
+
for (const site of findings) {
|
|
571
|
+
const key = ledgerKey(site);
|
|
572
|
+
seen.add(key);
|
|
573
|
+
if (!reasons.has(key)) {
|
|
574
|
+
violations.push(site);
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
ledgered.push(site);
|
|
578
|
+
const owner = moduleOf(site.file);
|
|
579
|
+
if (owner !== null && owners.get(key) !== owner)
|
|
580
|
+
misfiled.push(key);
|
|
581
|
+
}
|
|
582
|
+
const stale = [...reasons.keys()].filter((key) => !seen.has(key)).sort();
|
|
583
|
+
return { findings, violations, ledgered, stale, misfiled, classified };
|
|
584
|
+
}
|
|
585
|
+
// --- the CLI ---------------------------------------------------------------
|
|
586
|
+
/** Directories a module walk never descends into. */
|
|
587
|
+
/**
|
|
588
|
+
* Directory names this rule's walk prunes wherever they occur under its root.
|
|
589
|
+
*
|
|
590
|
+
* `migrations` is the one that is a *rule* rather than a convenience, and its
|
|
591
|
+
* reason is in this file's header: an applied migration cannot be edited, so a
|
|
592
|
+
* ledger entry over a literal in one would never drain.
|
|
593
|
+
*/
|
|
594
|
+
export const PRUNED_DIRECTORIES = [
|
|
595
|
+
'node_modules',
|
|
596
|
+
'dist',
|
|
597
|
+
'build',
|
|
598
|
+
'migrations',
|
|
599
|
+
];
|
|
600
|
+
const SKIPPED = new Set(PRUNED_DIRECTORIES);
|
|
601
|
+
/**
|
|
602
|
+
* Whether a file is in this rule's population — the **one** membership
|
|
603
|
+
* decision, asked by the walk and by anything that needs to know what the walk
|
|
604
|
+
* would open.
|
|
605
|
+
*
|
|
606
|
+
* It is one function called from two places on purpose, in the idiom
|
|
607
|
+
* `command-coverage.ts` states: while the prune the walk does for speed and the
|
|
608
|
+
* rule the population states were two expressions of one predicate, one could go
|
|
609
|
+
* missing without the other noticing. The second caller is `endora check`'s
|
|
610
|
+
* package-scope floor, which has to know that a declared `./migrations` layer is
|
|
611
|
+
* **not** expected of this rule — otherwise a package that publishes migrations
|
|
612
|
+
* reports a short walk for a layer the rule excludes by design.
|
|
613
|
+
*/
|
|
614
|
+
export function isScannedPath(absolutePath, root) {
|
|
615
|
+
const posix = absolutePath.split('\\').join('/');
|
|
616
|
+
const within = posix.slice(root.split('\\').join('/').length).split('/');
|
|
617
|
+
if (within.slice(0, -1).some((segment) => SKIPPED.has(segment)))
|
|
618
|
+
return false;
|
|
619
|
+
return posix.endsWith('.ts') && !posix.endsWith('.d.ts') && !posix.endsWith('.test.ts');
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Every TypeScript source under `dir`, with the rule's own prunes applied.
|
|
623
|
+
*
|
|
624
|
+
* Exported because the population is part of the rule: two hosts computing
|
|
625
|
+
* "which files this check reads" two ways is the shape that lets one of them go
|
|
626
|
+
* half-blind.
|
|
627
|
+
*/
|
|
628
|
+
export function collectProseSources(dir, out = []) {
|
|
629
|
+
if (!existsSync(dir))
|
|
630
|
+
return out;
|
|
631
|
+
for (const name of readdirSync(dir)) {
|
|
632
|
+
const full = join(dir, name);
|
|
633
|
+
let directory;
|
|
634
|
+
try {
|
|
635
|
+
directory = statSync(full).isDirectory();
|
|
636
|
+
}
|
|
637
|
+
catch {
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
if (directory) {
|
|
641
|
+
if (SKIPPED.has(name))
|
|
642
|
+
continue;
|
|
643
|
+
collectProseSources(full, out);
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
if (name.endsWith('.ts') && !name.endsWith('.test.ts') && !name.endsWith('.d.ts')) {
|
|
647
|
+
out.push(full);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return out;
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Every shard in `directory`, loaded.
|
|
654
|
+
*
|
|
655
|
+
* Rejects rather than returning an empty list when the directory is missing or a
|
|
656
|
+
* shard fails to import: both are "read nothing", and the CLI turns the
|
|
657
|
+
* rejection into exit 2.
|
|
658
|
+
*/
|
|
659
|
+
export async function loadLedgerShards(directory) {
|
|
660
|
+
if (!existsSync(directory)) {
|
|
661
|
+
throw new Error('ledger directory missing — refusing to report a vacuous pass');
|
|
662
|
+
}
|
|
663
|
+
const shards = [];
|
|
664
|
+
for (const name of readdirSync(directory).sort()) {
|
|
665
|
+
if (!name.endsWith('.ts'))
|
|
666
|
+
continue;
|
|
667
|
+
const moduleId = name.replace(/\.ts$/, '');
|
|
668
|
+
let loaded;
|
|
669
|
+
try {
|
|
670
|
+
loaded = (await import(pathToFileURL(join(directory, name)).href));
|
|
671
|
+
}
|
|
672
|
+
catch (error) {
|
|
673
|
+
throw new Error(`ledger shard '${moduleId}' failed to load: ${String(error)}`);
|
|
674
|
+
}
|
|
675
|
+
const entries = loaded.entries;
|
|
676
|
+
if (typeof entries !== 'object' || entries === null) {
|
|
677
|
+
throw new Error(`ledger shard '${moduleId}' failed to load: it exports no 'entries' record`);
|
|
678
|
+
}
|
|
679
|
+
if (Object.keys(entries).length === 0) {
|
|
680
|
+
throw new Error(`ledger shard '${moduleId}' declares no entry — delete the file instead`);
|
|
681
|
+
}
|
|
682
|
+
shards.push({ moduleId, entries: entries });
|
|
683
|
+
}
|
|
684
|
+
return shards;
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* The remedy paragraph, one per finding kind.
|
|
688
|
+
*
|
|
689
|
+
* Exported because the remedy is part of the rule and not part of a host: an
|
|
690
|
+
* author outside this checkout reads the same sentence this repository's run
|
|
691
|
+
* prints, and a second wording would be a second answer to "what do I do".
|
|
692
|
+
*/
|
|
693
|
+
export const REMEDIES = {
|
|
694
|
+
'non-english-default': 'English is the default (AGENTS.md § i18n, owner ruling of 2026-09-01). Author the ' +
|
|
695
|
+
'sentence in English and put the Polish in the module bundle. Which seam carries it is ' +
|
|
696
|
+
"`specs/094-translation-boundary/contracts/translation-boundary.md` § 2: if the backend " +
|
|
697
|
+
"can resolve the reader's language here, translate and ship the code (case 1); if it " +
|
|
698
|
+
'cannot, ship a key, its params and an English fallback sentence (case 2); operator ' +
|
|
699
|
+
'content is seeded per language and belongs in no bundle at all.',
|
|
700
|
+
'polish-default-behind-a-prop': 'The prop is operator-editable and the **default** behind it is Polish, so an operator ' +
|
|
701
|
+
'who never edits the template gets a Polish document on an English deployment. Having a ' +
|
|
702
|
+
'mechanism is not having an English default — that conflation excluded 18 of these once ' +
|
|
703
|
+
'already (`../research.md` § 7.3). Make the default English and translate through the ' +
|
|
704
|
+
"renderer's own `locale`.",
|
|
705
|
+
'unclassifiable-literal': 'The property path reaching this literal carries a computed key, so the analysis cannot ' +
|
|
706
|
+
'say whether it is inside a per-language structure. Reported rather than skipped ' +
|
|
707
|
+
'(issue #113): treating it as exempt is the direction that agrees with the defect. ' +
|
|
708
|
+
'Write the language key as a literal, or move the literal out of the computed path.',
|
|
709
|
+
};
|
|
710
|
+
//# sourceMappingURL=default-language-prose.js.map
|