@salesforce/afv-skills 1.33.0 → 1.35.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.
Files changed (146) hide show
  1. package/package.json +1 -1
  2. package/skills/agentforce-architecture-analyze/SKILL.md +8 -0
  3. package/skills/agentforce-bot-upgrade/SKILL.md +0 -2
  4. package/skills/agentforce-d360-analyze/SKILL.md +8 -0
  5. package/skills/agentforce-observe/SKILL.md +3 -0
  6. package/skills/agentforce-test/SKILL.md +3 -0
  7. package/skills/automation-flow-generate/SKILL.md +1 -0
  8. package/skills/automation-sandbox-post-copy-config-generate/SKILL.md +239 -0
  9. package/skills/automation-sandbox-post-copy-config-generate/assets/config_template.json +21 -0
  10. package/skills/automation-sandbox-post-copy-config-generate/assets/json_schema.json +90 -0
  11. package/skills/automation-sandbox-post-copy-config-generate/examples/sample_sop_excerpt.md +31 -0
  12. package/skills/automation-sandbox-post-copy-config-generate/examples/sample_sop_to_config.json +50 -0
  13. package/skills/automation-sandbox-post-copy-config-generate/references/configuration_catalog.md +76 -0
  14. package/skills/automation-sandbox-post-copy-config-generate/references/sop_parsing_patterns.md +157 -0
  15. package/skills/automation-sandbox-post-copy-config-generate/references/source_format_handling.md +230 -0
  16. package/skills/commerce-b2b-open-code-components-integrate/SKILL.md +8 -0
  17. package/skills/commerce-b2b-open-code-components-replace/SKILL.md +2 -0
  18. package/skills/commerce-b2b-store-create/SKILL.md +4 -1
  19. package/skills/data360-activate/SKILL.md +12 -0
  20. package/skills/data360-code-extension-generate/SKILL.md +6 -6
  21. package/skills/data360-connect/SKILL.md +11 -0
  22. package/skills/data360-harmonize/SKILL.md +11 -0
  23. package/skills/data360-orchestrate/SKILL.md +30 -0
  24. package/skills/data360-prepare/SKILL.md +14 -0
  25. package/skills/data360-query/SKILL.md +10 -0
  26. package/skills/data360-schema-get/SKILL.md +7 -0
  27. package/skills/data360-segment/SKILL.md +11 -0
  28. package/skills/design-systems-slds-validate/SKILL.md +8 -0
  29. package/skills/design-systems-slds2-migrate/SKILL.md +3 -0
  30. package/skills/dx-apexguru-scan/SKILL.md +403 -0
  31. package/skills/dx-apexguru-scan/examples/README.md +54 -0
  32. package/skills/dx-apexguru-scan/examples/sample-decoded-summary.json +176 -0
  33. package/skills/dx-apexguru-scan/examples/sample-full-no-runtime-response.json +26 -0
  34. package/skills/dx-apexguru-scan/examples/sample-succeeded-response.json +15 -0
  35. package/skills/dx-apexguru-scan/references/api-reference.md +81 -0
  36. package/skills/dx-apexguru-scan/references/authentication.md +134 -0
  37. package/skills/dx-apexguru-scan/references/error-handling.md +56 -0
  38. package/skills/dx-apexguru-scan/references/violation-catalog.md +28 -0
  39. package/skills/dx-apexguru-scan/scripts/build-zip.sh +87 -0
  40. package/skills/dx-apexguru-scan/scripts/decode-report.js +389 -0
  41. package/skills/dx-apexguru-scan/scripts/resolve-token.sh +151 -0
  42. package/skills/dx-apexguru-scan/scripts/run-scan.sh +153 -0
  43. package/skills/dx-apexguru-scan/scripts/scan.sh +96 -0
  44. package/skills/dx-apexguru-scan/scripts/validate-token.js +121 -0
  45. package/skills/dx-app-analytics-query/SKILL.md +5 -0
  46. package/skills/dx-code-analyzer-configure/SKILL.md +5 -5
  47. package/skills/dx-code-analyzer-custom-rule-create/SKILL.md +3 -1
  48. package/skills/dx-code-analyzer-run/SKILL.md +10 -2
  49. package/skills/dx-devops-pipeline-manage/SKILL.md +263 -0
  50. package/skills/dx-devops-pipeline-manage/examples/common-workflows.md +177 -0
  51. package/skills/dx-devops-pipeline-manage/references/cli-commands.md +298 -0
  52. package/skills/dx-devops-pipeline-manage/references/parsing-patterns.md +134 -0
  53. package/skills/dx-devops-pipeline-manage/scripts/check-activation-ready.sh +34 -0
  54. package/skills/dx-devops-pipeline-manage/scripts/validate-org-type.sh +17 -0
  55. package/skills/dx-devops-pipeline-manage/scripts/verify-operation.sh +82 -0
  56. package/skills/dx-devops-promote/SKILL.md +214 -0
  57. package/skills/dx-devops-promote/examples/promotion-workflows.md +212 -0
  58. package/skills/dx-devops-promote/references/cli-commands.md +303 -0
  59. package/skills/dx-devops-test-failures-analyze/SKILL.md +8 -0
  60. package/skills/dx-devops-test-pipeline-configure/SKILL.md +7 -0
  61. package/skills/dx-devops-test-suite-assignments-configure/SKILL.md +9 -0
  62. package/skills/dx-devops-test-suite-run/SKILL.md +8 -0
  63. package/skills/dx-org-switch/SKILL.md +3 -0
  64. package/skills/experience-lwc-base-components-integrate/SKILL.md +176 -0
  65. package/skills/experience-lwc-base-components-integrate/references/lbc-expert-guidance.md +127 -0
  66. package/skills/experience-lwc-base-components-integrate/references/lightning-component-index.md +179 -0
  67. package/skills/experience-lwc-base-components-integrate/references/lightning-components.md +5429 -0
  68. package/skills/experience-lwc-base-components-integrate/scripts/extract-component-docs.sh +61 -0
  69. package/skills/experience-lwc-generate/SKILL.md +18 -0
  70. package/skills/experience-lwc-rtl-validate/SKILL.md +149 -0
  71. package/skills/experience-lwc-rtl-validate/references/rtl-expert.md +892 -0
  72. package/skills/experience-lwc-rtl-validate/scripts/scan-rtl-css.sh +206 -0
  73. package/skills/experience-lwc-typescript-migrate/SKILL.md +207 -0
  74. package/skills/experience-lwc-typescript-migrate/assets/dts-template.ts +15 -0
  75. package/skills/experience-lwc-typescript-migrate/assets/type-patterns.ts +44 -0
  76. package/skills/experience-lwc-typescript-migrate/scripts/find-consumers.sh +128 -0
  77. package/skills/experience-lwr-site-generate/SKILL.md +1 -1
  78. package/skills/experience-ui-bundle-agentforce-client-generate/SKILL.md +5 -2
  79. package/skills/experience-ui-bundle-custom-app-generate/SKILL.md +1 -1
  80. package/skills/experience-ui-bundle-file-upload-generate/SKILL.md +5 -0
  81. package/skills/experience-ui-bundle-localize/SKILL.md +323 -0
  82. package/skills/experience-ui-bundle-localize/references/gotchas.md +249 -0
  83. package/skills/experience-ui-bundle-localize/references/i18n-setup.md +169 -0
  84. package/skills/experience-ui-bundle-localize/references/interpolation.md +311 -0
  85. package/skills/experience-ui-bundle-localize/references/label-xml.md +282 -0
  86. package/skills/experience-ui-bundle-localize/references/verifying.md +219 -0
  87. package/skills/experience-ui-bundle-localize/scripts/check-i18n-wired.sh +195 -0
  88. package/skills/experience-ui-bundle-localize/scripts/check-manifest-registered.sh +100 -0
  89. package/skills/experience-ui-bundle-localize/scripts/check-org-api-version.sh +40 -0
  90. package/skills/experience-ui-bundle-localize/scripts/detect-bundle-type.sh +57 -0
  91. package/skills/experience-ui-bundle-metadata-generate/SKILL.md +1 -1
  92. package/skills/experience-ui-bundle-project-generate/SKILL.md +4 -4
  93. package/skills/experience-ui-bundle-salesforce-data-access/SKILL.md +11 -0
  94. package/skills/external-diagram-mermaid-generate/SKILL.md +21 -0
  95. package/skills/integration-connectivity-generate/SKILL.md +17 -0
  96. package/skills/integration-eventing-cdc-configure/SKILL.md +9 -0
  97. package/skills/integration-eventing-subscription-configure/SKILL.md +7 -0
  98. package/skills/mobile-apps-create/SKILL.md +5 -0
  99. package/skills/mobile-platform-native-capabilities-integrate/SKILL.md +3 -0
  100. package/skills/mobile-platform-offline-validate/SKILL.md +9 -0
  101. package/skills/omnistudio-callable-apex-generate/SKILL.md +14 -0
  102. package/skills/omnistudio-datamapper-generate/SKILL.md +13 -0
  103. package/skills/omnistudio-datapacks-deploy/SKILL.md +18 -0
  104. package/skills/omnistudio-dependencies-analyze/SKILL.md +12 -0
  105. package/skills/omnistudio-epc-catalog-generate/SKILL.md +15 -0
  106. package/skills/omnistudio-flexcard-generate/SKILL.md +11 -0
  107. package/skills/omnistudio-integration-procedure-generate/SKILL.md +11 -0
  108. package/skills/omnistudio-omniscript-generate/SKILL.md +9 -0
  109. package/skills/platform-agentexchange-partner-offers-configure/SKILL.md +8 -0
  110. package/skills/platform-agentsetup-categories-fetch/SKILL.md +3 -0
  111. package/skills/platform-apex-generate/SKILL.md +5 -0
  112. package/skills/platform-apex-logs-debug/SKILL.md +12 -0
  113. package/skills/platform-apex-test-generate/SKILL.md +5 -0
  114. package/skills/platform-apex-test-run/SKILL.md +14 -0
  115. package/skills/platform-custom-application-generate/SKILL.md +1 -0
  116. package/skills/platform-custom-field-generate/SKILL.md +1 -1
  117. package/skills/platform-custom-lightning-type-generate/SKILL.md +3 -0
  118. package/skills/platform-custom-lightning-type-generate/assets/primitive-types-and-constraints.md +1 -1
  119. package/skills/platform-custom-tab-generate/SKILL.md +1 -0
  120. package/skills/platform-data-manage/SKILL.md +15 -0
  121. package/skills/platform-dataspace-access-configure/SKILL.md +2 -2
  122. package/skills/platform-flexipage-generate/SKILL.md +3 -0
  123. package/skills/platform-lightning-app-coordinate/SKILL.md +18 -1
  124. package/skills/platform-mcp-tool-widget-coordinate/SKILL.md +250 -0
  125. package/skills/platform-mcp-tool-widget-coordinate/examples/action-name-source-prompt.md +74 -0
  126. package/skills/platform-mcp-tool-widget-coordinate/examples/apex-invocable-source-prompt.md +90 -0
  127. package/skills/platform-mcp-tool-widget-coordinate/examples/nested-object-source-prompt.md +191 -0
  128. package/skills/platform-mcp-tool-widget-coordinate/examples/pasted-tool-output-prompt.md +85 -0
  129. package/skills/platform-mcp-tool-widget-coordinate/references/build-plan-format.md +74 -0
  130. package/skills/platform-mcp-tool-widget-coordinate/references/mcp-tool-output-discovery.md +184 -0
  131. package/skills/platform-mcp-tool-widget-coordinate/references/two-clt-modeling.md +128 -0
  132. package/skills/platform-mcp-tool-widget-coordinate/references/validation-gates.md +181 -0
  133. package/skills/platform-metadata-api-context-get/SKILL.md +6 -2
  134. package/skills/platform-metadata-retrieve/SKILL.md +5 -0
  135. package/skills/platform-models-api-configure/SKILL.md +10 -0
  136. package/skills/platform-permission-set-generate/SKILL.md +1 -2
  137. package/skills/platform-policy-rule-generate/SKILL.md +2 -0
  138. package/skills/platform-report-generate/SKILL.md +1 -1
  139. package/skills/platform-sharing-rules-generate/SKILL.md +1 -1
  140. package/skills/platform-soql-query/SKILL.md +13 -0
  141. package/skills/platform-tracing-agentforce-configure/SKILL.md +5 -1
  142. package/skills/platform-tracing-configure/SKILL.md +4 -1
  143. package/skills/platform-trust-archive-manage/SKILL.md +3 -0
  144. package/skills/platform-validation-rule-generate/SKILL.md +1 -0
  145. package/skills/platform-widget-generate/SKILL.md +2 -1
  146. package/skills/sales-agentforce-pipeline-management-configure/SKILL.md +3 -1
@@ -0,0 +1,323 @@
1
+ ---
2
+ name: experience-ui-bundle-localize
3
+ description: "MUST activate to localize / internationalize a uiBundles/*/src/ React project: extract hardcoded user-facing strings into Custom Labels, wire i18next over the Platform SDK GraphQL backend, add a language to a localized bundle, or troubleshoot label rendering across locales. Triggers: user-facing string literals in .tsx/.jsx, a CustomLabels.labels-meta.xml, a src/i18n/ directory or label-manifest.ts, t(…) calls, or requests to 'translate / localize / internationalize / support another language.' Scope: authenticated UI Bundles only (B2E CustomApplication-bound, or in-core accessCheck-bound apps). DO NOT TRIGGER when: the bundle is a site (B2C/B2B) app bound to a DigitalExperienceConfig (site localization not yet supported), building app shell/UI or styling, reading/writing/refreshing records (use experience-ui-bundle-salesforce-data-access), generating a new bundle (use experience-ui-bundle-frontend-generate), deploying (use experience-ui-bundle-deploy), or authoring translations in Translation Workbench."
4
+ metadata:
5
+ version: "1.0"
6
+ minApiVersion: "68.0"
7
+ relatedSkills:
8
+ - "experience-ui-bundle-deploy"
9
+ - "experience-ui-bundle-frontend-generate"
10
+ - "experience-ui-bundle-salesforce-data-access"
11
+ cliTools:
12
+ - tool: ["jq"]
13
+ semver: ">=1.6"
14
+ - tool: ["npm"]
15
+ semver: ">=7.0.0"
16
+ - tool: ["sf"]
17
+ semver: ">=2.0.0"
18
+ ---
19
+
20
+ # Localize a React UI Bundle
21
+
22
+ Walk a developer through localizing a React UI Bundle: detect hardcoded user-facing strings, extract them into Salesforce Custom Labels, wire up i18next over the Platform SDK GraphQL backend, and verify labels render across locales.
23
+
24
+ This file is the **workflow + guardrail spine**. Depth lives in linked docs:
25
+
26
+ - **[references/i18n-setup.md](references/i18n-setup.md)**: the two files you write: the i18next init and the label manifest
27
+ - **[references/label-xml.md](references/label-xml.md)**: Custom Labels and translation metadata XML shapes; the `namespace:Key` rules
28
+ - **[references/interpolation.md](references/interpolation.md)**: positional `{0}/{1}` placeholder interpolation in labels
29
+ - **[references/verifying.md](references/verifying.md)**: serve URL, locale flip, and verifying labels render
30
+ - **[references/gotchas.md](references/gotchas.md)**: the three silent-fail traps: unregistered manifest keys, API-version bake-in, stale label cache
31
+
32
+ ## The one-paragraph mental model
33
+
34
+ A React UI Bundle can't use `@salesforce/label/*` the way LWC does, those imports resolve at compile time inside the platform's compiler, which your standalone React bundle doesn't go through. Instead, your app **fetches labels at runtime** through the Salesforce GraphQL UI API and hands them to **i18next** (a standard React i18n library) to render. The Platform SDK provides the runtime plumbing for this, a detector that reads the user's language, a backend that fetches labels over GraphQL, and a context fetch. You write two thin files: a short init that wires the SDK pieces into i18next, and a manifest listing which labels your app uses. The rest is authoring the labels themselves as Salesforce Custom Labels metadata.
35
+
36
+ ```typescript
37
+ import { useTranslation } from "react-i18next";
38
+
39
+ function WelcomeBanner() {
40
+ const { t } = useTranslation("c"); // "c" = custom label namespace
41
+ return <h1>{t("Welcome_Text")}</h1>; // renders "Welcome" or "Bienvenido" per user's language
42
+ }
43
+ ```
44
+
45
+ ---
46
+
47
+ ## Step 0: Route the task
48
+
49
+ | The task is… | Go to |
50
+ |---|---|
51
+ | Bundle doesn't exist yet | **experience-ui-bundle-frontend-generate** skill |
52
+ | Deploying the app with its labels | **experience-ui-bundle-deploy** skill |
53
+ | Localizing an existing bundle | **Workflow below** |
54
+
55
+ ---
56
+
57
+ ## Preconditions: verify before editing
58
+
59
+ | # | Requirement | Verify | If missing |
60
+ |---|---|---|---|
61
+ | 1 | It's a `uiBundles/*/src/` React project | Project structure matches | Not a UI Bundle → route to the correct skill |
62
+ | 2 | `@salesforce/platform-sdk` installed (≥11.42.1) | `package.json` in the UI bundle dir | Tell user to install it; cannot proceed |
63
+ | 3 | You can identify where the app mounts | Read the entry file (usually `src/index.tsx`) | No clear mount point → ask user to point it out |
64
+ | 4 | Target org actually supports API v68.0+ (runtime label GraphQL for UI Bundles ships in Release 264) | Run the runtime org-release check below | Org's max API version is below v68.0 (Release 262 or older) → cannot proceed; retarget a Release 264+ org or upgrade the org |
65
+ | 5 | The bundle is an authenticated app (B2E, or an in-core internal app), not a public site | Run the authenticated-app detection below | Bundle is a site (B2C/B2B) app → localization is **not yet supported for site bundles**; stop and tell the user B2C support is planned for when B2C localization is ready |
66
+
67
+ **Runtime org-release check (precondition 4).** The `platform.labels` GraphQL path that resolves labels at runtime for UI Bundles ships in Salesforce Release 264 (API v68.0 or higher). A `sourceApiVersion` in `sfdx-project.json` records what you declared, not what the org supports, so a newer CLI pointed at an older org can pass a static file check and then fail at runtime. Query the org's actual maximum API version before wiring anything:
68
+
69
+ ```bash
70
+ bash <skill-dir>/scripts/check-org-api-version.sh <org-alias-or-username>
71
+ ```
72
+
73
+ Exit `0` → the org supports v68.0+, proceed. Exit `1` → the org is too old or unreachable; do not write i18n wiring or labels, report the version mismatch to the user and stop. (`sf api request rest` inside the script keeps authentication at the CLI transport layer, so no access token enters context.)
74
+
75
+ **Authenticated-app detection (precondition 5).** The bundle's type decides whether localization is supported, and it's decided by deterministic file and string checks. Pass the full path to the bundle dir; the script derives the metadata root from it, so the current directory does not matter:
76
+
77
+ ```bash
78
+ bash <skill-dir>/scripts/detect-bundle-type.sh <path-to-uiBundles/<name>/ dir>
79
+ ```
80
+
81
+ Act on the exit code: `0` → authenticated app (in-core internal or B2E), proceed; `1` → site (B2C/B2B), localization is **not yet supported for site bundles**, stop and tell the user (B2C support is planned for when B2C localization is ready); `2` → unbound or cannot auto-detect, **ask the user to confirm** the bundle is an authenticated app (B2E or in-core internal) and stop if they cannot.
82
+
83
+ If a precondition isn't met, stop: report the specific block to the user and record a plan item to return once it's resolved. Do not edit the bundle, in particular, never add i18n wiring or TODO markers to a site (B2C/B2B) bundle that precondition 5 gated off as unsupported.
84
+
85
+ ---
86
+
87
+ ## Workflow: the five steps
88
+
89
+ Each step has a **checkable completion criterion** and a **confirm-before-continue** pause.
90
+
91
+ ### Step 1: Detect
92
+
93
+ **Goal:** Scan `.tsx`/`.jsx` files for user-facing hardcoded strings.
94
+
95
+ **What to scan:**
96
+ - String literals inside JSX tags: `<h1>Welcome</h1>` → candidate
97
+ - String props shown to users: `placeholder="Enter name"` → candidate
98
+ - User-facing accessible text: `aria-label`, `aria-describedby`, `alt` → candidate (a screen-reader user hears these, so they must localize too)
99
+
100
+ **What to skip:**
101
+ - Import statements
102
+ - Object keys / property names
103
+ - `data-*` attributes (machine-readable)
104
+ - Test IDs (`data-testid`, `id` attributes)
105
+ - Text already wrapped in `t()` calls
106
+ - Console logs, error messages thrown to developers (not user-facing)
107
+ - Class names, file paths, technical constants
108
+
109
+ **Action:**
110
+ 1. Scan the `src/` directory for `.tsx` and `.jsx` files
111
+ 2. Extract candidates, showing file path + line number for each
112
+ 3. Show the list to the developer
113
+
114
+ **Completion criterion:**
115
+ Developer confirms the list (or edits it to remove false positives).
116
+
117
+ **Pause:** "I found N user-facing strings across M components. Here's the list: [show file:line + string]. Look right? [confirm / edit the list / skip some]"
118
+
119
+ ---
120
+
121
+ ### Step 2: Extract
122
+
123
+ **Goal:** For each confirmed string, add a Custom Label and replace the JSX literal with a `t()` call.
124
+
125
+ **Action for each string:**
126
+ 1. **Propose a key name**, format: `<Context>_<Role>` (e.g., `"Welcome"` → `Welcome_Text`, `"Save"` → `Save_Button`, `"Failed to save"` → `Save_Failed_Message`). Follow naming: PascalCase words, underscores between parts, descriptive enough to be unique.
127
+ 2. **Add the label** to `force-app/main/default/labels/CustomLabels.labels-meta.xml`:
128
+ ```xml
129
+ <labels>
130
+ <fullName>Welcome_Text</fullName>
131
+ <language>en_US</language>
132
+ <protected>false</protected>
133
+ <shortDescription>Welcome banner heading</shortDescription>
134
+ <value>Welcome</value>
135
+ </labels>
136
+ ```
137
+ (Full XML structure: [references/label-xml.md](references/label-xml.md))
138
+ 3. **Replace the string** in the component with `{t("Key")}`:
139
+ ```tsx
140
+ // Before: <h1>Welcome</h1>
141
+ // After: <h1>{t("Welcome_Text")}</h1>
142
+ ```
143
+ 4. **Add the import** if not present: `import { useTranslation } from "react-i18next";` and `const { t } = useTranslation("c");` at the top of the component function.
144
+
145
+ **Completion criterion:**
146
+ Every confirmed string has both a `CustomLabels` entry and a `t()` call in its original location.
147
+
148
+ **Pause:** "For each string I'll add a Custom Label and replace the JSX with t(). Here are the proposed keys: [show string → namespace:Key mapping]. Apply these edits? [y / review each]"
149
+
150
+ ---
151
+
152
+ ### Step 3: Register
153
+
154
+ **Goal:** Add each key to the label manifest so i18next knows to fetch it.
155
+
156
+ **Action:**
157
+ 1. Add each key to the manifest array in `src/i18n/label-manifest.ts`:
158
+ ```typescript
159
+ export const labelManifest = [
160
+ "c:Welcome_Text",
161
+ "c:Save_Button",
162
+ "c:Save_Failed_Message",
163
+ ];
164
+ ```
165
+ If the file doesn't exist yet, Step 4 scaffolds it; the completion check below reports its absence, so don't test for the file by hand.
166
+
167
+ **Completion criterion:**
168
+ Run `check-manifest-registered.sh` from the UI bundle dir (it scans `src/` relative to the current directory) and report any errors it returns. It owns the deterministic inspection: it cross-checks every `t("Key")` call site against the manifest and treats a missing `label-manifest.ts` (when `t()` calls exist) as a failure. A key that's called but not registered renders as its own literal name at runtime with no error, the silent-fail trap this guards.
169
+
170
+ ```bash
171
+ cd <path-to-uiBundles/<name>/ dir> # scripts scan src/ relative to here
172
+ bash <skill-dir>/scripts/check-manifest-registered.sh
173
+ ```
174
+
175
+ Branch on the exit code: `0`, every key is registered (or there are no `t()` calls to gate), proceed. `1`, the manifest is missing or the listed keys aren't in it; scaffold or add them (Step 4 scaffolds the file) and re-run. `64`, usage error, the source dir doesn't exist (wrong cwd or bad argument); this is **not** a "keys missing" result, do not scaffold or register, fix the path and re-run.
176
+
177
+ **Pause:** "Added N entries to label-manifest.ts. check-manifest-registered.sh passed: [confirm]."
178
+
179
+ ---
180
+
181
+ ### Step 4: Wire
182
+
183
+ **Goal:** Ensure the i18next init exists; scaffold it if the app has no i18n yet.
184
+
185
+ **Check:**
186
+ Run `check-i18n-wired.sh` from the UI bundle dir (it scans `src/` relative to the current directory) and report what it returns. The script owns the whole deterministic inspection: it looks for an init file defining `initI18n()` and a boot-time call to it, and when those exist it also reports whether the label manifest is imported and actually passed into the backend config. Do not re-derive any of this by reading files yourself.
187
+
188
+ ```bash
189
+ cd <path-to-uiBundles/<name>/ dir> # scripts scan src/ relative to here
190
+ bash <skill-dir>/scripts/check-i18n-wired.sh
191
+ ```
192
+
193
+ Branch on the **exit code** (the printed message names the specific file/symbol for your report, but the decision is the code):
194
+ - Exit `0` → fully wired, the manifest is passed into the backend; go to "If i18n already exists" below and just add new keys.
195
+ - Exit `1` → no `initI18n()` exists; scaffold the whole setup via "If no i18n setup exists yet".
196
+ - Exit `2` → the init already exists but isn't called at boot; do **not** re-scaffold or overwrite it. Add only the boot-time `initI18n()` call in the entry file (step 4 of "If no i18n setup exists yet"), then re-run.
197
+ - Exit `3` → wired at boot but the script **could not confirm** the manifest is passed into the backend. It scans the whole `src` tree, but this last check is a textual heuristic: the manifest may be wired through a variable, spread, or helper the script can't see, so treat exit 3 as "verify before editing," not "definitely broken." Open the file the message names and confirm the manifest really isn't in `backendOptions`. Only if it genuinely dangles, do what the message names: if the manifest is imported but unused, pass it into the existing `backendOptions` without clobbering it; if there's no `backendOptions`/`SalesforceBackend` config at all, add that backend block to the existing init (see [references/i18n-setup.md](references/i18n-setup.md)). Never re-scaffold the init file or duplicate wiring that already works.
198
+ - Exit `64` → usage error: the source dir doesn't exist (wrong cwd or bad argument). This is **not** a "no init" result; do not scaffold. Fix the path (run from the UI bundle dir, or pass its `src` path) and re-run.
199
+
200
+ **If no i18n setup exists yet:**
201
+ 1. Install dependencies (tell the user to run):
202
+ ```bash
203
+ npm install i18next react-i18next i18next-chained-backend i18next-localstorage-backend
204
+ ```
205
+ 2. Create `src/i18n/index.ts` with the init wiring (full code: [references/i18n-setup.md](references/i18n-setup.md))
206
+ 3. Create `src/i18n/label-manifest.ts` with an empty array (Step 3 will populate it)
207
+ 4. Call `initI18n()` once at boot in the entry file (before mounting the app):
208
+ ```typescript
209
+ import { initI18n } from "./i18n";
210
+
211
+ initI18n().then(() => {
212
+ // mount app
213
+ });
214
+ ```
215
+
216
+ **If i18n already exists:**
217
+ Act on the message `check-i18n-wired.sh` already printed (above): if it reports the manifest wired, just add new keys to it; if it reports a reconcile is needed, do exactly what its message names (import the manifest and/or pass it into the backend config) without clobbering existing wiring.
218
+
219
+ **Completion criterion:**
220
+ `initI18n()` exists and is called once at boot; the manifest is wired into the backend.
221
+
222
+ **Pause:** "i18next setup [exists / created]. initI18n() is called at boot: [confirm]."
223
+
224
+ ---
225
+
226
+ ### Step 5: Verify
227
+
228
+ **Goal:** Guide the developer to verify labels render in a second language.
229
+
230
+ **Action:**
231
+ 1. **Activate a second language** (if not already active), tell the user: "In your org, go to Setup → Translation Workbench → Translation Settings → add a language (e.g., Spanish)."
232
+ 2. **Author a translation**, scaffold an empty translation file for the language:
233
+ ```xml
234
+ <!-- force-app/main/default/translations/es.translation-meta.xml -->
235
+ <Translations xmlns="http://soap.sforce.com/2006/04/metadata">
236
+ <customLabels>
237
+ <label>Bienvenido</label>
238
+ <name>Welcome_Text</name>
239
+ </customLabels>
240
+ </Translations>
241
+ ```
242
+ (Full structure: [references/label-xml.md](references/label-xml.md))
243
+
244
+ Tell the user to either:
245
+ - Edit the XML file by hand (for a small number of labels), or
246
+ - Use Translation Workbench (Setup → Translate → Custom Label → pick language → enter translations), then retrieve with `sf project retrieve start --metadata Translations:es`.
247
+
248
+ 3. **Build and deploy**, tell the user:
249
+ ```bash
250
+ sf config set target-org=<alias> # API version bakes in; point at the deploy target first
251
+ npm run build
252
+ sf project deploy start --source-dir force-app --target-org <alias>
253
+ ```
254
+
255
+ 4. **Open the app** at the `/lwr/application/ai/<namespace>-<bundleName>` URL on the `lightning.force.com` domain (redirects to the app host).
256
+
257
+ 5. **Change the user's Language** (not Locale), Setup → My Settings → Language & Time Zone → Language → pick the translated language → Save.
258
+
259
+ 6. **Reload the app**, labels should flip to the translated language.
260
+
261
+ **If it doesn't render:**
262
+ Check the three gotchas in [references/gotchas.md](references/gotchas.md):
263
+ - Unregistered manifest key (Step 3 missed a label)
264
+ - API-version mismatch (built against a different org)
265
+ - Stale localStorage cache (clear `i18next_res_*` keys in DevTools)
266
+
267
+ **Completion criterion:**
268
+ Labels render in ≥2 locales, or the blocking gotcha is identified.
269
+
270
+ **Pause:** "To verify: activate a second language in Translation Workbench, author a translation (I can scaffold the XML), build/deploy, and reload. Want me to scaffold the translation file for [language]? [y / I'll do it manually]"
271
+
272
+ ---
273
+
274
+ ## Edge cases: handle gracefully
275
+
276
+ - **Already-localized code**: detect existing `t()` usage / a populated manifest; offer to *add to* the setup rather than re-scaffold everything.
277
+ - **No strings found**: report cleanly and stop; do not invent work.
278
+ - **App has no i18n setup yet**: Step 4 scaffolds the two files first before Step 3 can register anything.
279
+ - **Partial setup** (manifest exists but init missing, or vice-versa), reconcile what's present; never clobber existing wiring.
280
+
281
+ ---
282
+
283
+ ## Guardrails: never regress these
284
+
285
+ 1. **Never machine-translate into deployable metadata.** Scaffold empty translation files and guide the developer to author translations (by hand or via Translation Workbench). Do not call any MT API and paste the result into `translation-meta.xml`; unreviewed machine translations are a quality liability.
286
+ 2. **Never register a key that has no label.** Manifest entry count must equal label count (Step 3 criterion). An unregistered key renders as its own literal name with no console warning. It's the most common localization bug.
287
+ 3. **Never clobber existing i18n wiring.** If Step 4 finds an existing `initI18n()`, reconcile (add the manifest import if missing) rather than replace the whole file.
288
+ 4. **Every file must be customer-safe.** No `webapps`, core-only paths, or internal infrastructure references anywhere. Write as if for an external customer in an SFDX project.
289
+
290
+ ---
291
+
292
+ ## Commands & layout
293
+
294
+ ```text
295
+ <project-root>/ ← SFDX project root
296
+ └── force-app/main/default/
297
+ ├── labels/CustomLabels.labels-meta.xml ← English base labels
298
+ ├── translations/<locale>.translation-meta.xml ← one per translated language
299
+ └── uiBundles/<your-bundle>/
300
+ ├── package.json
301
+ └── src/
302
+ ├── i18n/
303
+ │ ├── index.ts ← init wiring (you write this once)
304
+ │ └── label-manifest.ts ← list of labels to fetch (you maintain this)
305
+ └── components/ ← components call t()
306
+ ```
307
+
308
+ | Command | Run from | Purpose |
309
+ |---|---|---|
310
+ | `npm install i18next react-i18next i18next-chained-backend i18next-localstorage-backend` | UI bundle dir | Install i18n dependencies (Step 4) |
311
+ | `npm run build` | UI bundle dir | Build the app (API version bakes in, set target-org first) |
312
+ | `sf project deploy start --source-dir force-app` | Project root | Deploy the app + labels + translations |
313
+ | `sf project retrieve start --metadata Translations:<locale>` | Project root | Pull translations authored in Translation Workbench |
314
+
315
+ ---
316
+
317
+ ## Pre-flight checklist: completion criteria for the whole run
318
+
319
+ - [ ] Every confirmed string has both a `CustomLabels` entry and a `t()` call
320
+ - [ ] `label-manifest.ts` entry count == label count (no unregistered keys)
321
+ - [ ] `initI18n()` present and called once at boot
322
+ - [ ] Labels render in ≥2 locales (or the blocking gotcha is named)
323
+ - [ ] No hand-written machine translations landed in `*-meta.xml` (only scaffold-and-guide)
@@ -0,0 +1,249 @@
1
+ # Gotchas: the three silent-fail traps
2
+
3
+ These are localization-specific problems that fail **silently** (no console warning, no error, just wrong text). If your app loads but labels are wrong, start here.
4
+
5
+ ---
6
+
7
+ ## 1. Unregistered manifest key → renders as literal key name
8
+
9
+ **Symptom:** You see the literal string `Welcome_Text` on screen instead of "Welcome."
10
+
11
+ **Cause:** The key isn't in your `label-manifest.ts`. The app only fetches labels that are listed in the manifest, so an unregistered key is **never requested**, and i18next, finding nothing in its cache, renders the **literal key string** back to you as a fallback.
12
+
13
+ **No console warning. No error.** It fails silently. This is the most common localization bug.
14
+
15
+ **Example:**
16
+
17
+ ```typescript
18
+ // src/i18n/label-manifest.ts
19
+ export const labelManifest = [
20
+ "c:Save_Button",
21
+ // forgot "c:Welcome_Text"
22
+ ];
23
+ ```
24
+
25
+ ```typescript
26
+ // component
27
+ const { t } = useTranslation("c");
28
+ return <h1>{t("Welcome_Text")}</h1>; // renders "Welcome_Text" (literal string)
29
+ ```
30
+
31
+ **Fix:** Add the missing key to `label-manifest.ts`:
32
+
33
+ ```typescript
34
+ export const labelManifest = [
35
+ "c:Save_Button",
36
+ "c:Welcome_Text", // ← add this
37
+ ];
38
+ ```
39
+
40
+ **Why it's silent:** i18next is a general-purpose library. It doesn't know your labels come from Salesforce. When a key isn't in its cache, the fallback behavior (per i18next's design) is to render the key string, because in some apps, that's a valid debugging signal. In our case, it's just a trap.
41
+
42
+ **Prevention:** Always keep manifest entry count equal to label count (Step 3 completion criterion in the workflow). PR review catches mismatches today.
43
+
44
+ **Deeper detail:** The manifest is read by `SalesforceBackend` at boot. It groups entries by namespace (all `c:*` together, all `LightningDatatable:*` together) and issues a GraphQL query per namespace:
45
+
46
+ ```graphql
47
+ query LoadLabels {
48
+ uiapi {
49
+ platform {
50
+ labels(namespace: "c", names: ["Save_Button", ...]) {
51
+ name
52
+ value
53
+ resolvedLocale
54
+ }
55
+ }
56
+ }
57
+ }
58
+ ```
59
+
60
+ If `Welcome_Text` isn't in the manifest, it isn't in the `names` array, so the server never returns it. i18next's cache is empty for that key, and the `t()` call falls back to rendering the key name.
61
+
62
+ Within a namespace, `SalesforceBackend` also splits the names into batches of at most **100** and fires the queries in parallel, because `uiapi.platform.labels` rejects a call with more than 100 names. This is automatic: a manifest with hundreds of `c:*` keys works with no extra config on your part. See the "Large manifest" note under Related problems.
63
+
64
+ ---
65
+
66
+ ## 2. API-version bake-in → blank page after deploy
67
+
68
+ **Symptom:** The app worked locally (or on a different org), but after deploying to a new org it boots to a **blank page** (white screen, no obvious error in the console).
69
+
70
+ **Cause:** You built the app while pointed at a **different org** than the one you deployed to, and the API versions don't match. The build plugin reads your default org's API version and stamps it into the bundle's JavaScript:
71
+
72
+ ```javascript
73
+ // baked into the built JS
74
+ const endpoint = `https://<org>/services/data/v65.0/graphql`;
75
+ ```
76
+
77
+ If you built while pointed at a v65.0 org and deploy to a v63.0 org, the bundle tries to call `/services/data/v65.0/graphql`, an endpoint that org doesn't have. The GraphQL call **404s**, the i18n context fetch (`fetchI18nContext()`) throws, and the app crashes during boot before React even mounts. You see a blank page.
78
+
79
+ **Fix:** Before building, set the deploy target org as default so the versions match:
80
+
81
+ ```bash
82
+ sf config set target-org=<your-deploy-target-alias>
83
+ npm run build
84
+ sf project deploy start --source-dir force-app --target-org <same-alias>
85
+ ```
86
+
87
+ **Check an org's API version:**
88
+
89
+ ```bash
90
+ sf org display --target-org <alias>
91
+ ```
92
+
93
+ Look for `"API Version"` in the output (e.g., `63.0`).
94
+
95
+ **Prevention:** Always run `sf config set target-org=<deploy-target>` **before** `npm run build`. The workflow (Step 1 of verifying) calls this out, but it's easy to forget when iterating.
96
+
97
+ **Why it's silent:** The 404 happens in the SDK's GraphQL fetch, which throws a generic network error. The stack trace points at `fetchI18nContext`, but doesn't say "wrong API version"; you have to infer it from the 404. A mismatch of one minor version (e.g., v63 → v64) usually works (backward-compatible); a mismatch of two or more (v63 → v65) reliably breaks.
98
+
99
+ **Not the same as the version floor:** The bake-in above is a *mismatch* problem (two orgs, different versions). Separate from it is a hard floor: runtime label resolution over GraphQL for UI Bundles works from **API v68.0 (the 264 release)** onward. The `platform.labels` field appears in the schema at v67.0, but the end-to-end feature is functional for UI Bundles at 264.4 / v68.0; on an org below that, a localized bundle renders blank or shows raw key names. This is why precondition 4 queries the org's actual maximum API version (via `check-org-api-version.sh`) before starting, rather than trusting the `sourceApiVersion` declared in `sfdx-project.json`: that field records what you declared, not what the org supports, so an org that old can't run this pattern even when the file check passes. Confirm an org's ceiling with `sf org display --target-org <alias>` (the `API Version` row) or by opening `/services/data/` and checking the highest `vNN.0` listed.
100
+
101
+ **Version floor is not the same as release timing:** The precondition blocks orgs older than v68.0, but it cannot distinguish patch releases within v68 (264.3 vs 264.4 both report v68.0). If this skill is deployed to orgs mid-rollout (on 264.3, before 264.4 is live), it can still produce non-functional localized bundles. That is a release-coordination concern handled by when the skill ships, not by this check.
102
+
103
+ ---
104
+
105
+ ## 3. Stale localStorage cache → old translations persist
106
+
107
+ **Symptom:** You changed a translation (edited the `translation-meta.xml` or updated it in Translation Workbench), confirmed the new value is in the org, redeployed, but the app **still shows the old text** on reload.
108
+
109
+ **Cause:** Your `src/i18n/index.ts` chains two backends:
110
+
111
+ ```typescript
112
+ backend: {
113
+ backends: [LocalStorageBackend, SalesforceBackend],
114
+ backendOptions: [
115
+ { expirationTime: 86400000 }, // 24 hours
116
+ { dataSDK, labelManifest },
117
+ ],
118
+ },
119
+ ```
120
+
121
+ On load, i18next reads labels from **localStorage first** (the `LocalStorageBackend`), and only falls through to the GraphQL fetch (the `SalesforceBackend`) on a **cache miss**. Labels are cached per language+namespace under keys like:
122
+
123
+ - `i18next_res_en-c` (English custom labels)
124
+ - `i18next_res_es-c` (Spanish custom labels)
125
+ - `i18next_res_fr-c` (French custom labels)
126
+
127
+ Each has an `expirationTime` of **24 hours** (86400000 milliseconds). Until that entry expires, your app serves the cached copy and **never refetches**, so a fresh org value doesn't appear.
128
+
129
+ **What you see in DevTools:**
130
+ - **Network tab** shows the i18n **context/detect** query on every reload (this is how the SDK reads the user's language).
131
+ - But it shows **no labels query**, because the labels are served from localStorage, not the network.
132
+ - This makes it look like "the network is fine, so why is the text stale?" But the network is fine *for the context fetch*; the labels never hit the network at all.
133
+
134
+ **Fix:** Clear the cached labels, then reload:
135
+
136
+ 1. **DevTools → Application → Local Storage** → find your org's origin (e.g., `https://<org>.my.salesforce.app`)
137
+ 2. Delete the `i18next_res_*` keys (or **Clear site data** to wipe everything)
138
+ 3. Reload the app
139
+
140
+ The next load misses the cache, refetches the labels over GraphQL, and shows the current value.
141
+
142
+ **This is expected behavior, not a bug.** The cache is what makes labels fast after first load (no GraphQL roundtrip on every boot). In production, a translation change takes up to `expirationTime` to roll out to all users; during development, you manually clear the cache to see edits immediately.
143
+
144
+ **Prevention:** When testing translation changes, habitually clear localStorage before reloading. Or lower `expirationTime` in the init file during development (e.g., 60000 = 1 minute), but remember to raise it back for production.
145
+
146
+ **Why the GraphQL calls you DO see aren't label refetches:** The context query runs on every boot to detect the user's current language. It's a separate, small query:
147
+
148
+ ```graphql
149
+ query I18nContext {
150
+ uiapi {
151
+ platform {
152
+ i18n {
153
+ lang
154
+ locale
155
+ dir
156
+ currency
157
+ }
158
+ }
159
+ }
160
+ }
161
+ ```
162
+
163
+ That's not cached. The **labels** query (the big one with all your label names) is what's cached, and it only fires on a cache miss.
164
+
165
+ ---
166
+
167
+ ## Summary table
168
+
169
+ | Symptom | Cause | Fix |
170
+ |---|---|---|
171
+ | Label renders as its own key name (`"Welcome_Text"`) | Unregistered manifest key | Add the key to `label-manifest.ts` |
172
+ | Blank page after deploy | API-version mismatch (built against different org) | `sf config set target-org=<deploy-target>` before building |
173
+ | Old translation persists after update | Stale localStorage cache | Clear `i18next_res_*` keys in DevTools, reload |
174
+
175
+ ---
176
+
177
+ ## Related problems (not silent-fail, but localization-adjacent)
178
+
179
+ ### Deploy rejected: "Not available for deploy for this organization"
180
+
181
+ **Cause:** You're deploying a `<locale>.translation-meta.xml` for a language that isn't activated yet.
182
+
183
+ **Fix:** Setup → Translation Workbench → Translation Settings → Add the language, then re-deploy.
184
+
185
+ English never needs activation.
186
+
187
+ ---
188
+
189
+ ### Label shows in English when it should be translated (but not as a literal key)
190
+
191
+ **Symptom:** The text is correct, just not translated (e.g., you see "Welcome" when you expected "Bienvenido").
192
+
193
+ **Cause (most likely):** The translation file is missing that label, or its `<name>` doesn't match the `<fullName>` in `CustomLabels`. i18next falls back to the English base value (via `fallbackLng: "en"`), which is correct behavior; it just means that one key wasn't translated.
194
+
195
+ **Other causes:**
196
+ - The user's **Language** (not Locale) isn't set to the language you translated.
197
+ - The translation file wasn't deployed (missing from `force-app/main/default/translations/`).
198
+
199
+ **Check:** Setup → Translation Workbench → Translate → pick the language and the label. Is the translation there? If not, author it and re-deploy.
200
+
201
+ ---
202
+
203
+ ### Logged-in user sees their own language instead of the org default
204
+
205
+ **Symptom:** In a B2X site (or any authenticated context), a logged-in user whose **Language** is French sees French text for a label you only authored in the org-default language (English), **even when the app requests English**. A guest user switching languages works fine; only the logged-in user with a different Language setting hits it.
206
+
207
+ **Cause:** the server's label **fallback strategy**. When the app asks for a locale that has no explicit translation for a label, the server decides what to return. Its default strategy, `USER_DEFAULT`, resolves in this order: requested locale → that locale's language-fallback chain → **the logged-in user's own language** → the base/master value. Because the user's language comes *before* the base value, a French user gets French, not the English default.
208
+
209
+ **Fix on the documented floor (11.42.1):** `SalesforceBackend` does not yet override the server's default, so the query resolves with `USER_DEFAULT` and a logged-in user with a different Language can still see their own language instead of the base value. To get the org default on this version, author an explicit org-default translation for the label, or debug the raw query with `fallback: BASE_VALUE` (shown below) to confirm the value you expect.
210
+
211
+ > **Coming in a later SDK release:** a change that makes `SalesforceBackend` request `fallback: BASE_VALUE` is merged (2026-07-29) but not yet published, so it is not in 11.42.1. Once the release that carries it ships, the backend skips straight to the org-default value when a translation is missing, and you no longer need an explicit English translation just to work around this. Until then, treat the behavior above as current.
212
+
213
+ **If you're debugging the raw labels query** (e.g., re-running it in DevTools): `fallback` must be declared in the operation signature (`$fallback: LabelFallback`) **and** passed to the `labels(...)` field. A `fallback` key in the variables block alone is silently dropped and the server falls back to `USER_DEFAULT`. It takes the `LabelFallback` enum (`USER_DEFAULT` / `BASE_VALUE` / `NONE`), not a locale string like `"en"`:
214
+
215
+ ```graphql
216
+ query Labels($ns: String!, $names: [String!]!, $locale: String, $fallback: LabelFallback) {
217
+ uiapi {
218
+ platform {
219
+ labels(namespace: $ns, names: $names, locale: $locale, fallback: $fallback) {
220
+ name
221
+ value
222
+ resolvedLocale
223
+ wasFallback
224
+ }
225
+ }
226
+ }
227
+ }
228
+ ```
229
+
230
+ with `"fallback": "BASE_VALUE"` in the variables.
231
+
232
+ ---
233
+
234
+ ### Large manifest: no per-query limit to manage yourself
235
+
236
+ **Symptom / worry:** You have hundreds of labels in one namespace and wonder whether you have to split the manifest or cap it.
237
+
238
+ **You don't.** `uiapi.platform.labels` rejects any single call with more than 100 names, but `SalesforceBackend` handles this for you: it dedupes the names, splits each namespace into batches of at most 100, and issues those queries in parallel, then merges the results. A manifest with 500 `c:*` keys becomes 5 batched queries under the hood; your code and manifest stay flat.
239
+
240
+ The load is **all-or-nothing per namespace**: if any batch fails, the whole namespace's read rejects (you don't get a half-populated namespace). If you're watching the Network tab, a large manifest is why you may see several `Labels` queries fire at once rather than one.
241
+
242
+ ---
243
+
244
+ ## Related
245
+
246
+ - [i18n-setup.md](i18n-setup.md): the init file (where `expirationTime` is configured)
247
+ - [label-xml.md](label-xml.md): Custom Labels + translations metadata
248
+ - [interpolation.md](interpolation.md): `{0}/{1}` placeholders
249
+ - [verifying.md](verifying.md): the serve/verify flow