@mccaskie-labs/workbench-config 0.1.17 → 0.1.18

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @mccaskie-labs/workbench-config
2
2
 
3
+ ## 0.1.18
4
+
5
+ ### Patch Changes
6
+
7
+ - The `strings` component: `capabilities/strings.json` (rules and probes for `@mccaskie-labs/workbench-strings`) joins the manifests, and `strings` joins the component list.
8
+
3
9
  ## 0.1.17
4
10
 
5
11
  ### Patch Changes
@@ -31,7 +31,8 @@
31
31
  "access",
32
32
  "r2",
33
33
  "kv",
34
- "github"
34
+ "github",
35
+ "strings"
35
36
  ]
36
37
  },
37
38
  "title": {
@@ -0,0 +1,131 @@
1
+ {
2
+ "$schema": "./schema.json",
3
+ "format": 1,
4
+ "component": "strings",
5
+ "title": "Words on screens and in emails",
6
+ "summary": "workbench-strings holds the words an app's own screens and emails say: one file per language in the app's repository, looked up on the server with named placeholders and plurals, one fallback chain to the base language, language detection with country before the browser's language, back-translation notes stripped at build, and a review gate so no unreviewed translation is inferred for anyone. The standard's tests ship as a kit.",
7
+ "status": "built",
8
+ "surfaces": {
9
+ "package": [
10
+ "defineStrings",
11
+ "resolveLocale",
12
+ "stripGlosses",
13
+ "WORKBENCH_LANGUAGES",
14
+ "WorkbenchStringsError"
15
+ ]
16
+ },
17
+ "rules": [
18
+ {
19
+ "id": "strings.server-only",
20
+ "kind": "must_not",
21
+ "text": "Import workbench-strings into browser code: the catalogue holds every language. Look words up on the server and pass the finished words to the page; the package's browser export throws.",
22
+ "enforced_by": "types",
23
+ "probe": "strings.lookup"
24
+ },
25
+ {
26
+ "id": "strings.no-literals",
27
+ "kind": "must_not",
28
+ "text": "Write user-visible words as literals in code, or keep a key nothing renders. Every word is a key in strings/strings.<base>.json, and the test kit's dead-key sweep fails on an unused one.",
29
+ "enforced_by": "review",
30
+ "probe": "strings.kit"
31
+ },
32
+ {
33
+ "id": "strings.one-chain",
34
+ "kind": "must",
35
+ "text": "Use strings.t and strings.email for every lookup, so screens and emails share one fallback chain: the locale, its parent, then the base. A word found further down the chain is logged.",
36
+ "enforced_by": "types",
37
+ "probe": "strings.lookup"
38
+ },
39
+ {
40
+ "id": "strings.country-before-header",
41
+ "kind": "must",
42
+ "text": "Detect the language in the order URL prefix, cookie, account, country, Accept-Language, default, with strings.detect. Country comes before the browser's language because phones report English whatever their owner reads.",
43
+ "enforced_by": "types",
44
+ "probe": "strings.detect"
45
+ },
46
+ {
47
+ "id": "strings.review-gate",
48
+ "kind": "must_not",
49
+ "text": "Serve a translated language to someone who did not choose it until its catalogue has _review.signed_off naming the reviewer and the date; country, header and default resolve to the base until then.",
50
+ "enforced_by": "types",
51
+ "probe": "strings.gate"
52
+ },
53
+ {
54
+ "id": "strings.named-reviewer",
55
+ "kind": "must_not",
56
+ "text": "Put a translated language live in production without _review.reviewer naming a native-speaker reviewer and their cadence; defineStrings refuses to start.",
57
+ "enforced_by": "types",
58
+ "probe": "strings.no-reviewer"
59
+ },
60
+ {
61
+ "id": "strings.email-missing-throws",
62
+ "kind": "must",
63
+ "text": "Treat an email word missing in the recipient's language and in the base as an error: strings.email throws missing_string rather than sending a key.",
64
+ "enforced_by": "types",
65
+ "probe": "strings.email-missing"
66
+ },
67
+ {
68
+ "id": "strings.glosses-stripped",
69
+ "kind": "must",
70
+ "text": "Keep a _gloss back-translation beside every translated string in the authored file, and ship only the generated file from npx workbench-strings build, which strips them; defineStrings refuses a catalogue with glosses.",
71
+ "enforced_by": "types",
72
+ "probe": "strings.kit"
73
+ }
74
+ ],
75
+ "examples": [
76
+ {
77
+ "id": "strings.example.lookup",
78
+ "title": "Detect the language and look words up",
79
+ "lang": "ts",
80
+ "probe": "strings.lookup",
81
+ "code": "// SERVER ONLY: the catalogue holds every language.\nimport { defineStrings } from '@mccaskie-labs/workbench-strings'\nimport enAU from '../strings/strings.en-AU.json'\nimport enPH from '../strings/runtime/strings.en-PH.json'\n\nconst strings = defineStrings({ env: workbenchEnv(env), base: { locale: 'en-AU', strings: enAU }, translations: { 'en-PH': enPH }, locales: ['en-PH', 'en-AU'], defaultLocale: 'en-PH' })\nconst { locale } = strings.detect(request, { account: user?.locale })\nconst title = strings.t(locale, 'ui.cart.count', { count: 3 })"
82
+ }
83
+ ],
84
+ "probes": [
85
+ {
86
+ "id": "strings.lookup",
87
+ "half": "runtime",
88
+ "expect": "pass",
89
+ "smoke": true,
90
+ "cost_hint_usd": 0,
91
+ "note": "Placeholders and a plural fill in each live locale, and a word missing in a translation comes from the base and is logged"
92
+ },
93
+ {
94
+ "id": "strings.detect",
95
+ "half": "runtime",
96
+ "expect": "pass",
97
+ "cost_hint_usd": 0,
98
+ "note": "Each step of the order wins over the next; a PH visitor whose browser says en-US gets the PH locale; XX falls through to the header"
99
+ },
100
+ {
101
+ "id": "strings.gate",
102
+ "half": "runtime",
103
+ "expect": "pass",
104
+ "cost_hint_usd": 0,
105
+ "note": "An unsigned translation is not inferred (gated: true, base served) but is served from the cookie or prefix"
106
+ },
107
+ {
108
+ "id": "strings.no-reviewer",
109
+ "half": "runtime",
110
+ "expect": "refused_as_expected",
111
+ "refusal": "no_reviewer",
112
+ "cost_hint_usd": 0,
113
+ "note": "defineStrings with env production and a translation with no _review.reviewer"
114
+ },
115
+ {
116
+ "id": "strings.email-missing",
117
+ "half": "runtime",
118
+ "expect": "refused_as_expected",
119
+ "refusal": "missing_string",
120
+ "cost_hint_usd": 0,
121
+ "note": "strings.email for a part no catalogue has"
122
+ },
123
+ {
124
+ "id": "strings.kit",
125
+ "half": "runtime",
126
+ "expect": "pass",
127
+ "cost_hint_usd": 0,
128
+ "note": "The app's own checkStrings run passes in its tests, and npx workbench-strings check passes in CI"
129
+ }
130
+ ]
131
+ }
@@ -1,4 +1,4 @@
1
- export declare const MANIFEST_VERSION: "849b265d6d63";
1
+ export declare const MANIFEST_VERSION: "d0231144218c";
2
2
  export declare const capabilities: {
3
3
  readonly ai: {
4
4
  readonly $schema: "./schema.json";
@@ -1518,6 +1518,113 @@ export declare const capabilities: {
1518
1518
  readonly note: "Prints the app's own HTML holding an image from a host not in allowOrigins; the PDF is made and the browser reports that image's request as blocked";
1519
1519
  }];
1520
1520
  };
1521
+ readonly strings: {
1522
+ readonly $schema: "./schema.json";
1523
+ readonly format: 1;
1524
+ readonly component: "strings";
1525
+ readonly title: "Words on screens and in emails";
1526
+ readonly summary: "workbench-strings holds the words an app's own screens and emails say: one file per language in the app's repository, looked up on the server with named placeholders and plurals, one fallback chain to the base language, language detection with country before the browser's language, back-translation notes stripped at build, and a review gate so no unreviewed translation is inferred for anyone. The standard's tests ship as a kit.";
1527
+ readonly status: "built";
1528
+ readonly surfaces: {
1529
+ readonly package: readonly ["defineStrings", "resolveLocale", "stripGlosses", "WORKBENCH_LANGUAGES", "WorkbenchStringsError"];
1530
+ };
1531
+ readonly rules: readonly [{
1532
+ readonly id: "strings.server-only";
1533
+ readonly kind: "must_not";
1534
+ readonly text: "Import workbench-strings into browser code: the catalogue holds every language. Look words up on the server and pass the finished words to the page; the package's browser export throws.";
1535
+ readonly enforced_by: "types";
1536
+ readonly probe: "strings.lookup";
1537
+ }, {
1538
+ readonly id: "strings.no-literals";
1539
+ readonly kind: "must_not";
1540
+ readonly text: "Write user-visible words as literals in code, or keep a key nothing renders. Every word is a key in strings/strings.<base>.json, and the test kit's dead-key sweep fails on an unused one.";
1541
+ readonly enforced_by: "review";
1542
+ readonly probe: "strings.kit";
1543
+ }, {
1544
+ readonly id: "strings.one-chain";
1545
+ readonly kind: "must";
1546
+ readonly text: "Use strings.t and strings.email for every lookup, so screens and emails share one fallback chain: the locale, its parent, then the base. A word found further down the chain is logged.";
1547
+ readonly enforced_by: "types";
1548
+ readonly probe: "strings.lookup";
1549
+ }, {
1550
+ readonly id: "strings.country-before-header";
1551
+ readonly kind: "must";
1552
+ readonly text: "Detect the language in the order URL prefix, cookie, account, country, Accept-Language, default, with strings.detect. Country comes before the browser's language because phones report English whatever their owner reads.";
1553
+ readonly enforced_by: "types";
1554
+ readonly probe: "strings.detect";
1555
+ }, {
1556
+ readonly id: "strings.review-gate";
1557
+ readonly kind: "must_not";
1558
+ readonly text: "Serve a translated language to someone who did not choose it until its catalogue has _review.signed_off naming the reviewer and the date; country, header and default resolve to the base until then.";
1559
+ readonly enforced_by: "types";
1560
+ readonly probe: "strings.gate";
1561
+ }, {
1562
+ readonly id: "strings.named-reviewer";
1563
+ readonly kind: "must_not";
1564
+ readonly text: "Put a translated language live in production without _review.reviewer naming a native-speaker reviewer and their cadence; defineStrings refuses to start.";
1565
+ readonly enforced_by: "types";
1566
+ readonly probe: "strings.no-reviewer";
1567
+ }, {
1568
+ readonly id: "strings.email-missing-throws";
1569
+ readonly kind: "must";
1570
+ readonly text: "Treat an email word missing in the recipient's language and in the base as an error: strings.email throws missing_string rather than sending a key.";
1571
+ readonly enforced_by: "types";
1572
+ readonly probe: "strings.email-missing";
1573
+ }, {
1574
+ readonly id: "strings.glosses-stripped";
1575
+ readonly kind: "must";
1576
+ readonly text: "Keep a _gloss back-translation beside every translated string in the authored file, and ship only the generated file from npx workbench-strings build, which strips them; defineStrings refuses a catalogue with glosses.";
1577
+ readonly enforced_by: "types";
1578
+ readonly probe: "strings.kit";
1579
+ }];
1580
+ readonly examples: readonly [{
1581
+ readonly id: "strings.example.lookup";
1582
+ readonly title: "Detect the language and look words up";
1583
+ readonly lang: "ts";
1584
+ readonly code: "// SERVER ONLY: the catalogue holds every language.\nimport { defineStrings } from '@mccaskie-labs/workbench-strings'\nimport enAU from '../strings/strings.en-AU.json'\nimport enPH from '../strings/runtime/strings.en-PH.json'\n\nconst strings = defineStrings({ env: workbenchEnv(env), base: { locale: 'en-AU', strings: enAU }, translations: { 'en-PH': enPH }, locales: ['en-PH', 'en-AU'], defaultLocale: 'en-PH' })\nconst { locale } = strings.detect(request, { account: user?.locale })\nconst title = strings.t(locale, 'ui.cart.count', { count: 3 })";
1585
+ readonly probe: "strings.lookup";
1586
+ }];
1587
+ readonly probes: readonly [{
1588
+ readonly id: "strings.lookup";
1589
+ readonly half: "runtime";
1590
+ readonly expect: "pass";
1591
+ readonly smoke: true;
1592
+ readonly cost_hint_usd: 0;
1593
+ readonly note: "Placeholders and a plural fill in each live locale, and a word missing in a translation comes from the base and is logged";
1594
+ }, {
1595
+ readonly id: "strings.detect";
1596
+ readonly half: "runtime";
1597
+ readonly expect: "pass";
1598
+ readonly cost_hint_usd: 0;
1599
+ readonly note: "Each step of the order wins over the next; a PH visitor whose browser says en-US gets the PH locale; XX falls through to the header";
1600
+ }, {
1601
+ readonly id: "strings.gate";
1602
+ readonly half: "runtime";
1603
+ readonly expect: "pass";
1604
+ readonly cost_hint_usd: 0;
1605
+ readonly note: "An unsigned translation is not inferred (gated: true, base served) but is served from the cookie or prefix";
1606
+ }, {
1607
+ readonly id: "strings.no-reviewer";
1608
+ readonly half: "runtime";
1609
+ readonly expect: "refused_as_expected";
1610
+ readonly refusal: "no_reviewer";
1611
+ readonly cost_hint_usd: 0;
1612
+ readonly note: "defineStrings with env production and a translation with no _review.reviewer";
1613
+ }, {
1614
+ readonly id: "strings.email-missing";
1615
+ readonly half: "runtime";
1616
+ readonly expect: "refused_as_expected";
1617
+ readonly refusal: "missing_string";
1618
+ readonly cost_hint_usd: 0;
1619
+ readonly note: "strings.email for a part no catalogue has";
1620
+ }, {
1621
+ readonly id: "strings.kit";
1622
+ readonly half: "runtime";
1623
+ readonly expect: "pass";
1624
+ readonly cost_hint_usd: 0;
1625
+ readonly note: "The app's own checkStrings run passes in its tests, and npx workbench-strings check passes in CI";
1626
+ }];
1627
+ };
1521
1628
  };
1522
1629
  export type Alias = (typeof capabilities)["ai"]["allowed"]["aliases"][number]["alias"];
1523
1630
  export type GatewayScope = (typeof capabilities)["gateway"]["allowed"]["scopes"][number];
@@ -1,6 +1,6 @@
1
1
  // Generated by `npm run capabilities:build` from capabilities/*.json. Do not edit.
2
2
  // check:capabilities fails in Checks when this file is stale.
3
- export const MANIFEST_VERSION = "849b265d6d63";
3
+ export const MANIFEST_VERSION = "d0231144218c";
4
4
  export const capabilities = {
5
5
  "ai": {
6
6
  "$schema": "./schema.json",
@@ -2052,5 +2052,136 @@ export const capabilities = {
2052
2052
  "note": "Prints the app's own HTML holding an image from a host not in allowOrigins; the PDF is made and the browser reports that image's request as blocked"
2053
2053
  }
2054
2054
  ]
2055
+ },
2056
+ "strings": {
2057
+ "$schema": "./schema.json",
2058
+ "format": 1,
2059
+ "component": "strings",
2060
+ "title": "Words on screens and in emails",
2061
+ "summary": "workbench-strings holds the words an app's own screens and emails say: one file per language in the app's repository, looked up on the server with named placeholders and plurals, one fallback chain to the base language, language detection with country before the browser's language, back-translation notes stripped at build, and a review gate so no unreviewed translation is inferred for anyone. The standard's tests ship as a kit.",
2062
+ "status": "built",
2063
+ "surfaces": {
2064
+ "package": [
2065
+ "defineStrings",
2066
+ "resolveLocale",
2067
+ "stripGlosses",
2068
+ "WORKBENCH_LANGUAGES",
2069
+ "WorkbenchStringsError"
2070
+ ]
2071
+ },
2072
+ "rules": [
2073
+ {
2074
+ "id": "strings.server-only",
2075
+ "kind": "must_not",
2076
+ "text": "Import workbench-strings into browser code: the catalogue holds every language. Look words up on the server and pass the finished words to the page; the package's browser export throws.",
2077
+ "enforced_by": "types",
2078
+ "probe": "strings.lookup"
2079
+ },
2080
+ {
2081
+ "id": "strings.no-literals",
2082
+ "kind": "must_not",
2083
+ "text": "Write user-visible words as literals in code, or keep a key nothing renders. Every word is a key in strings/strings.<base>.json, and the test kit's dead-key sweep fails on an unused one.",
2084
+ "enforced_by": "review",
2085
+ "probe": "strings.kit"
2086
+ },
2087
+ {
2088
+ "id": "strings.one-chain",
2089
+ "kind": "must",
2090
+ "text": "Use strings.t and strings.email for every lookup, so screens and emails share one fallback chain: the locale, its parent, then the base. A word found further down the chain is logged.",
2091
+ "enforced_by": "types",
2092
+ "probe": "strings.lookup"
2093
+ },
2094
+ {
2095
+ "id": "strings.country-before-header",
2096
+ "kind": "must",
2097
+ "text": "Detect the language in the order URL prefix, cookie, account, country, Accept-Language, default, with strings.detect. Country comes before the browser's language because phones report English whatever their owner reads.",
2098
+ "enforced_by": "types",
2099
+ "probe": "strings.detect"
2100
+ },
2101
+ {
2102
+ "id": "strings.review-gate",
2103
+ "kind": "must_not",
2104
+ "text": "Serve a translated language to someone who did not choose it until its catalogue has _review.signed_off naming the reviewer and the date; country, header and default resolve to the base until then.",
2105
+ "enforced_by": "types",
2106
+ "probe": "strings.gate"
2107
+ },
2108
+ {
2109
+ "id": "strings.named-reviewer",
2110
+ "kind": "must_not",
2111
+ "text": "Put a translated language live in production without _review.reviewer naming a native-speaker reviewer and their cadence; defineStrings refuses to start.",
2112
+ "enforced_by": "types",
2113
+ "probe": "strings.no-reviewer"
2114
+ },
2115
+ {
2116
+ "id": "strings.email-missing-throws",
2117
+ "kind": "must",
2118
+ "text": "Treat an email word missing in the recipient's language and in the base as an error: strings.email throws missing_string rather than sending a key.",
2119
+ "enforced_by": "types",
2120
+ "probe": "strings.email-missing"
2121
+ },
2122
+ {
2123
+ "id": "strings.glosses-stripped",
2124
+ "kind": "must",
2125
+ "text": "Keep a _gloss back-translation beside every translated string in the authored file, and ship only the generated file from npx workbench-strings build, which strips them; defineStrings refuses a catalogue with glosses.",
2126
+ "enforced_by": "types",
2127
+ "probe": "strings.kit"
2128
+ }
2129
+ ],
2130
+ "examples": [
2131
+ {
2132
+ "id": "strings.example.lookup",
2133
+ "title": "Detect the language and look words up",
2134
+ "lang": "ts",
2135
+ "code": "// SERVER ONLY: the catalogue holds every language.\nimport { defineStrings } from '@mccaskie-labs/workbench-strings'\nimport enAU from '../strings/strings.en-AU.json'\nimport enPH from '../strings/runtime/strings.en-PH.json'\n\nconst strings = defineStrings({ env: workbenchEnv(env), base: { locale: 'en-AU', strings: enAU }, translations: { 'en-PH': enPH }, locales: ['en-PH', 'en-AU'], defaultLocale: 'en-PH' })\nconst { locale } = strings.detect(request, { account: user?.locale })\nconst title = strings.t(locale, 'ui.cart.count', { count: 3 })",
2136
+ "probe": "strings.lookup"
2137
+ }
2138
+ ],
2139
+ "probes": [
2140
+ {
2141
+ "id": "strings.lookup",
2142
+ "half": "runtime",
2143
+ "expect": "pass",
2144
+ "smoke": true,
2145
+ "cost_hint_usd": 0,
2146
+ "note": "Placeholders and a plural fill in each live locale, and a word missing in a translation comes from the base and is logged"
2147
+ },
2148
+ {
2149
+ "id": "strings.detect",
2150
+ "half": "runtime",
2151
+ "expect": "pass",
2152
+ "cost_hint_usd": 0,
2153
+ "note": "Each step of the order wins over the next; a PH visitor whose browser says en-US gets the PH locale; XX falls through to the header"
2154
+ },
2155
+ {
2156
+ "id": "strings.gate",
2157
+ "half": "runtime",
2158
+ "expect": "pass",
2159
+ "cost_hint_usd": 0,
2160
+ "note": "An unsigned translation is not inferred (gated: true, base served) but is served from the cookie or prefix"
2161
+ },
2162
+ {
2163
+ "id": "strings.no-reviewer",
2164
+ "half": "runtime",
2165
+ "expect": "refused_as_expected",
2166
+ "refusal": "no_reviewer",
2167
+ "cost_hint_usd": 0,
2168
+ "note": "defineStrings with env production and a translation with no _review.reviewer"
2169
+ },
2170
+ {
2171
+ "id": "strings.email-missing",
2172
+ "half": "runtime",
2173
+ "expect": "refused_as_expected",
2174
+ "refusal": "missing_string",
2175
+ "cost_hint_usd": 0,
2176
+ "note": "strings.email for a part no catalogue has"
2177
+ },
2178
+ {
2179
+ "id": "strings.kit",
2180
+ "half": "runtime",
2181
+ "expect": "pass",
2182
+ "cost_hint_usd": 0,
2183
+ "note": "The app's own checkStrings run passes in its tests, and npx workbench-strings check passes in CI"
2184
+ }
2185
+ ]
2055
2186
  }
2056
2187
  };
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- export declare const COMPONENTS: readonly ["gateway", "ai", "images", "image-search", "config", "email", "email-inbound", "email-sending", "database", "workers", "domains", "dns", "analytics", "country", "pdf", "access", "r2", "kv", "github"];
2
+ export declare const COMPONENTS: readonly ["gateway", "ai", "images", "image-search", "config", "email", "email-inbound", "email-sending", "database", "workers", "domains", "dns", "analytics", "country", "pdf", "access", "r2", "kv", "github", "strings"];
3
3
  export type Component = (typeof COMPONENTS)[number];
4
4
  export declare const allowedSchemas: {
5
5
  gateway: z.ZodObject<{
@@ -110,6 +110,7 @@ export declare const capabilitySchema: z.ZodObject<{
110
110
  kv: "kv";
111
111
  pdf: "pdf";
112
112
  r2: "r2";
113
+ strings: "strings";
113
114
  workers: "workers";
114
115
  }>;
115
116
  title: z.ZodString;
@@ -22,6 +22,7 @@ export const COMPONENTS = [
22
22
  "r2",
23
23
  "kv",
24
24
  "github",
25
+ "strings",
25
26
  ];
26
27
  const id = z.string().regex(/^[a-z0-9][a-z0-9.-]*$/, "lower case, digits, dots and hyphens");
27
28
  const code = z.string().regex(/^[a-z][a-z0-9_]*$/, "a snake_case error code");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mccaskie-labs/workbench-config",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "Workbench app configuration: WORKBENCH_ENV, typed variables that fail by name, secrets that are never printed, and the capabilities manifest.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",