@salesforce/afv-skills 1.50.0 → 1.52.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 (41) hide show
  1. package/package.json +1 -1
  2. package/skills/experience-ui-bundle-deploy/SKILL.md +43 -2
  3. package/skills/experience-ui-bundle-deploy/references/config-scaffold.md +16 -3
  4. package/skills/experience-ui-bundle-deploy/references/logout-url.md +97 -0
  5. package/skills/experience-ui-bundle-deploy/scripts/set-logout-url.mjs +304 -0
  6. package/skills/experience-ui-bundle-localize/SKILL.md +107 -90
  7. package/skills/experience-ui-bundle-localize/references/angular/check-i18n-wired.sh +159 -0
  8. package/skills/experience-ui-bundle-localize/references/angular/i18n-setup.md +250 -0
  9. package/skills/experience-ui-bundle-localize/references/angular/interpolation.md +156 -0
  10. package/skills/experience-ui-bundle-localize/references/angular/localize.md +111 -0
  11. package/skills/experience-ui-bundle-localize/references/{gotchas.md → common/gotchas.md} +27 -43
  12. package/skills/experience-ui-bundle-localize/references/{label-xml.md → common/label-xml.md} +27 -17
  13. package/skills/experience-ui-bundle-localize/references/common/platform-sdk-i18n.md +169 -0
  14. package/skills/experience-ui-bundle-localize/references/{verifying.md → common/verifying.md} +26 -16
  15. package/skills/experience-ui-bundle-localize/{scripts → references/react}/check-i18n-wired.sh +8 -3
  16. package/skills/experience-ui-bundle-localize/references/{i18n-setup.md → react/i18n-setup.md} +48 -10
  17. package/skills/experience-ui-bundle-localize/references/{interpolation.md → react/interpolation.md} +4 -4
  18. package/skills/experience-ui-bundle-localize/references/react/localize.md +76 -0
  19. package/skills/experience-ui-bundle-localize/scripts/check-manifest-registered.sh +92 -24
  20. package/skills/experience-ui-bundle-localize/scripts/detect-framework.sh +73 -0
  21. package/skills/experience-ui-bundle-site-generate/SKILL.md +1 -1
  22. package/skills/field-service-data-capture-form-deployer-configure/SKILL.md +1 -1
  23. package/skills/field-service-data-capture-form-deployer-configure/references/flow-metadata-json.md +1 -1
  24. package/skills/field-service-data-capture-form-editor-configure/SKILL.md +1 -1
  25. package/skills/field-service-data-capture-reference-configure/SKILL.md +28 -15
  26. package/skills/field-service-foundation-setup-designer-get/SKILL.md +1 -1
  27. package/skills/field-service-mobile-branding-configure/SKILL.md +1 -1
  28. package/skills/field-service-objective-designer-configure/SKILL.md +381 -64
  29. package/skills/field-service-prework-brief-deployer-configure/SKILL.md +566 -3
  30. package/skills/field-service-scheduling-policy-designer-query/SKILL.md +1097 -57
  31. package/skills/field-service-sobject-create-configure/SKILL.md +222 -7
  32. package/skills/field-service-voice-to-form-configure/SKILL.md +11 -11
  33. package/skills/field-service-work-rule-designer-configure/SKILL.md +92 -107
  34. package/skills/service-digital-engagement-channel-configure/SKILL.md +20 -37
  35. package/skills/service-digital-engagement-channel-configure/assets/messaging_channel_template.xml +3 -2
  36. package/skills/service-digital-engagement-channel-configure/examples/asa_agent_channel.xml +4 -4
  37. package/skills/service-helpagent-coordinate/SKILL.md +37 -29
  38. package/skills/service-helpagent-coordinate/assets/help-agent-spec.md +33 -28
  39. package/skills/service-helpagent-coordinate/references/agent-script.md +4 -1
  40. package/skills/service-helpagent-coordinate/references/channel-voice.md +1 -1
  41. package/skills/service-helpagent-coordinate/references/channel-web-chat.md +10 -12
@@ -8,7 +8,7 @@ These are localization-specific problems that fail **silently** (no console warn
8
8
 
9
9
  **Symptom:** You see the literal string `Welcome_Text` on screen instead of "Welcome."
10
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.
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 the i18n library, finding nothing in its cache, renders the **literal key string** back to you as a fallback.
12
12
 
13
13
  **No console warning. No error.** It fails silently. This is the most common localization bug.
14
14
 
@@ -22,10 +22,9 @@ export const labelManifest = [
22
22
  ];
23
23
  ```
24
24
 
25
- ```typescript
26
- // component
27
- const { t } = useTranslation("c");
28
- return <h1>{t("Welcome_Text")}</h1>; // renders "Welcome_Text" (literal string)
25
+ ```text
26
+ // component calls the translation function with an unregistered key:
27
+ translate("Welcome_Text") // renders "Welcome_Text" (literal string)
29
28
  ```
30
29
 
31
30
  **Fix:** Add the missing key to `label-manifest.ts`:
@@ -37,11 +36,11 @@ export const labelManifest = [
37
36
  ];
38
37
  ```
39
38
 
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.
39
+ **Why it's silent:** the i18n library (i18next, ngx-translate) is general-purpose. It doesn't know your labels come from Salesforce. When a key isn't in its cache, the fallback behavior (by 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
40
 
42
41
  **Prevention:** Always keep manifest entry count equal to label count (Step 3 completion criterion in the workflow). PR review catches mismatches today.
43
42
 
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:
43
+ **Deeper detail:** The manifest is read by the label backend at boot (React's shipped `SalesforceBackend`, or Angular's custom `TranslateLoader`). It groups entries by namespace (all `c:*` together, all `LightningDatatable:*` together) and issues a GraphQL query per namespace:
45
44
 
46
45
  ```graphql
47
46
  query LoadLabels {
@@ -57,9 +56,9 @@ query LoadLabels {
57
56
  }
58
57
  ```
59
58
 
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.
59
+ If `Welcome_Text` isn't in the manifest, it isn't in the `names` array, so the server never returns it. The library's cache is empty for that key, and the translation call falls back to rendering the key name.
61
60
 
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.
61
+ Within a namespace, the label backend 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
62
 
64
63
  ---
65
64
 
@@ -74,7 +73,7 @@ Within a namespace, `SalesforceBackend` also splits the names into batches of at
74
73
  const endpoint = `https://<org>/services/data/v65.0/graphql`;
75
74
  ```
76
75
 
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.
76
+ 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 it mounts. You see a blank page.
78
77
 
79
78
  **Fix:** Before building, set the deploy target org as default so the versions match:
80
79
 
@@ -106,46 +105,30 @@ Look for `"API Version"` in the output (e.g., `63.0`).
106
105
 
107
106
  ---
108
107
 
109
- ## 3. Stale localStorage cache → old translations persist
108
+ ## 3. Stale label cache → old translations persist
110
109
 
111
110
  **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.
112
111
 
113
- **Cause:** Your `src/i18n/index.ts` chains two backends:
114
-
115
- ```typescript
116
- backend: {
117
- backends: [LocalStorageBackend, SalesforceBackend],
118
- backendOptions: [
119
- { expirationTime: 86400000 }, // 24 hours
120
- { dataSDK, labelManifest },
121
- ],
122
- },
123
- ```
124
-
125
- 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:
126
-
127
- - `i18next_res_en-c` (English custom labels)
128
- - `i18next_res_es-c` (Spanish custom labels)
129
- - `i18next_res_fr-c` (French custom labels)
112
+ **Cause:** The i18n setup caches fetched labels so it doesn't hit GraphQL on every boot. Until that cache is invalidated, the app serves the cached copy and **never refetches**, so a fresh org value doesn't appear. The cache layer is **framework-specific**:
130
113
 
131
- 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.
114
+ - **React / i18next** persists labels to **localStorage** via `i18next-localstorage-backend`, chained in front of the network backend: `backends: [LocalStorageBackend, SalesforceBackend]`. Labels are keyed per language+namespace as `i18next_res_<lang>-<ns>` (e.g. `i18next_res_en-c`, `i18next_res_es-c`) with an `expirationTime` (default 24h / 86400000 ms). This survives reloads until it expires.
115
+ - **Angular / ngx-translate** caches the resolved translations **in memory** on the `TranslateService` for the page session; the custom `TranslateLoader` re-issues the GraphQL query on the next `TranslateService.use(lang)` or a full page reload. A reload refetches (no persistent layer unless you add one).
132
116
 
133
- **What you see in DevTools:**
117
+ **What you see in DevTools (React case):**
134
118
  - **Network tab** shows the i18n **context/detect** query on every reload (this is how the SDK reads the user's language).
135
119
  - But it shows **no labels query**, because the labels are served from localStorage, not the network.
136
120
  - 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.
137
121
 
138
- **Fix:** Clear the cached labels, then reload:
122
+ **Fix:** Invalidate the cache, then reload:
139
123
 
140
- 1. **DevTools → Application → Local Storage** → find your org's origin (e.g., `https://<org>.my.salesforce.app`)
141
- 2. Delete the `i18next_res_*` keys (or **Clear site data** to wipe everything)
142
- 3. Reload the app
124
+ - **React / i18next:** DevTools → Application → Local Storage → your org's origin (e.g. `https://<org>.my.salesforce.app`) → delete the `i18next_res_*` keys (or **Clear site data**), then reload.
125
+ - **Angular / ngx-translate:** a full page reload refetches; if you added a persistent cache layer, clear it too (**Clear site data**).
143
126
 
144
127
  The next load misses the cache, refetches the labels over GraphQL, and shows the current value.
145
128
 
146
- **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.
129
+ **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 React translation change takes up to `expirationTime` to roll out to all users; during development, you manually clear the cache to see edits immediately.
147
130
 
148
- **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.
131
+ **Prevention:** When testing translation changes, habitually clear the cache before reloading. In React you can also lower `expirationTime` in the init file during development (e.g., 60000 = 1 minute), then raise it back for production.
149
132
 
150
133
  **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:
151
134
 
@@ -174,7 +157,7 @@ That's not cached. The **labels** query (the big one with all your label names)
174
157
  |---|---|---|
175
158
  | Label renders as its own key name (`"Welcome_Text"`) | Unregistered manifest key | Add the key to `label-manifest.ts` |
176
159
  | Blank page after deploy | API-version mismatch (built against different org) | `sf config set target-org=<deploy-target>` before building |
177
- | Old translation persists after update | Stale localStorage cache | Clear `i18next_res_*` keys in DevTools, reload |
160
+ | Old translation persists after update | Stale label cache | Clear the i18n cache (React: `i18next_res_*` in localStorage) and reload |
178
161
 
179
162
  ---
180
163
 
@@ -216,13 +199,13 @@ English never needs activation.
216
199
 
217
200
  **Symptom:** The URL or switcher displays a new site language, but labels remain in the previous language.
218
201
 
219
- **Cause:** B2C language context is established at boot from the language-specific route through `SFDC_ENV.language`. An in-place language change does not rebuild the SDK context, and `i18next_res_*` localStorage entries may continue serving cached labels.
202
+ **Cause:** B2C language context is established at boot from the language-specific route through `SFDC_ENV.language`. An in-place language change does not rebuild the SDK context, and cached labels (React's `i18next_res_*` localStorage entries) may continue serving the old language.
220
203
 
221
- **Fix:** Make the switcher navigate to the configured language URL and perform a full page reload. Confirm the route and `SFDC_ENV.language` agree. During development, clear `i18next_res_*` before retesting.
204
+ **Fix:** Make the switcher navigate to the configured language URL and perform a full page reload. Confirm the route and `SFDC_ENV.language` agree. During development, clear the i18n cache (React: `i18next_res_*`) before retesting.
222
205
 
223
206
  ### Choose fallback by bundle type
224
207
 
225
- The shipped `SalesforceBackend` supports `labelFallback`. Preserve its `BASE_VALUE` default for B2E by omitting the option. For B2C only, configure `labelFallback: "USER_DEFAULT"` so fallback follows the guest/site language context. Do not apply the B2C override to B2E.
208
+ The label fetch supports `labelFallback`. Preserve its `BASE_VALUE` default for B2E (React: omit the option; Angular: pass `BASE_VALUE`). For B2C only, configure `labelFallback: "USER_DEFAULT"` so fallback follows the guest/site language context. Do not apply the B2C override to B2E.
226
209
 
227
210
  **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"`:
228
211
 
@@ -251,7 +234,7 @@ B2B remains unsupported. Do not infer B2B support from the B2C guidance or apply
251
234
 
252
235
  **Symptom / worry:** You have hundreds of labels in one namespace and wonder whether you have to split the manifest or cap it.
253
236
 
254
- **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.
237
+ **You don't.** `uiapi.platform.labels` rejects any single call with more than 100 names, but the label backend handles this for you (React's `SalesforceBackend`, or Angular's custom loader implementing the same logic): 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.
255
238
 
256
239
  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.
257
240
 
@@ -259,7 +242,8 @@ The load is **all-or-nothing per namespace**: if any batch fails, the whole name
259
242
 
260
243
  ## Related
261
244
 
262
- - [i18n-setup.md](i18n-setup.md): the init file (where `expirationTime` is configured)
263
245
  - [label-xml.md](label-xml.md): Custom Labels + translations metadata
264
- - [interpolation.md](interpolation.md): `{0}/{1}` placeholders
265
246
  - [verifying.md](verifying.md): the serve/verify flow
247
+ - `platform-sdk-i18n.md` (this folder): the shared runtime engine (Labels query, batching, fallback)
248
+ - your framework reference's `i18n-setup.md`: the init file (where the cache is configured)
249
+ - your framework reference's `interpolation.md`: `{0}/{1}` placeholders
@@ -34,7 +34,7 @@ This holds the **English base labels**: the source of truth for label content.
34
34
 
35
35
  | Field | Purpose | Notes |
36
36
  |---|---|---|
37
- | `<fullName>` | The label's API name | Used in `t("Key")` calls and the manifest (`"c:Welcome_Text"`). PascalCase, descriptive, unique. |
37
+ | `<fullName>` | The label's API name | Used in translation calls (`"Key"`) and the manifest (`"c:Welcome_Text"`). PascalCase, descriptive, unique. |
38
38
  | `<language>` | Language code | Always `en_US` for the base label file. |
39
39
  | `<protected>` | Managed package protection | Always `false` for custom labels in your org (you can edit them). |
40
40
  | `<shortDescription>` | Internal description | For translators/developers, not shown to users. Describe what the label is for. |
@@ -61,6 +61,16 @@ Format: `<Context>_<Role>` in PascalCase with underscores between parts.
61
61
 
62
62
  One file per translated language (e.g., `es.translation-meta.xml` for Spanish, `fr.translation-meta.xml` for French, `ja.translation-meta.xml` for Japanese).
63
63
 
64
+ 🚫 **Custom Label translations use the `Translations` metadata type — never `CustomObjectTranslation`.**
65
+ Write them to `force-app/main/default/translations/<locale>.translation-meta.xml` with a
66
+ `<Translations>` root. Do **not** author them as
67
+ `objectTranslations/CustomLabels-<locale>/CustomLabels-<locale>.objectTranslation-meta.xml` with a
68
+ `<CustomObjectTranslation>` root — that type is for translating **custom-object components** (field
69
+ labels, record types, picklist values), not org-level Custom Labels, and deploying labels that way
70
+ fails. If you catch yourself reaching for `objectTranslations/` or `CustomObjectTranslation` for a
71
+ Custom Label, stop: the correct target is the `translations/<locale>.translation-meta.xml` file
72
+ described here.
73
+
64
74
  ### Structure
65
75
 
66
76
  ```xml
@@ -77,6 +87,8 @@ One file per translated language (e.g., `es.translation-meta.xml` for Spanish, `
77
87
  </Translations>
78
88
  ```
79
89
 
90
+ ⚠️ Inside `<Translations>`, the translated text goes in **`<label>`**, never `<value>`. `<value>` is the *CustomLabels* element (it belongs in `CustomLabels.labels-meta.xml`); using it inside a `<customLabels>` block here produces malformed metadata that fails validation. Each `<customLabels>` block contains exactly `<label>` and `<name>` — nothing else.
91
+
80
92
  Always write the whole document shown above, not a fragment: exactly one XML declaration, one `<Translations>` root with the Metadata API namespace, and one complete `<customLabels>` block per label. For an untranslated scaffold, copy the XML-escaped English source text into `<label>` and leave actual translation to the localization team. Preserve positional placeholders such as `{0}` and `{1}`.
81
93
 
82
94
  XML-escape text values: `&` becomes `&amp;`, `<` becomes `&lt;`, and `>` becomes `&gt;`. Before considering the file complete, parse both `CustomLabels.labels-meta.xml` and every generated `*.translation-meta.xml` with an XML parser. A fragment with balanced-looking inner tags is still invalid without its root document.
@@ -151,7 +163,7 @@ export const labelManifest = [
151
163
 
152
164
  Other namespaces exist (e.g., `LightningDatatable` for framework-shipped labels), but most bundles only use `c`.
153
165
 
154
- In component code, the namespace is usually implicit (set via `defaultNS: "c"` in the init), so you call `t("Welcome_Text")` not `t("c:Welcome_Text")`.
166
+ In component code the namespace is usually implicit (the init sets `c` as the default), so you call your framework's translation function with the bare key (`Welcome_Text`), not `c:Welcome_Text`. The exact call convention is in your framework reference's `localize.md`.
155
167
 
156
168
  ---
157
169
 
@@ -169,13 +181,15 @@ Labels can include **positional placeholders** for runtime substitution:
169
181
  </labels>
170
182
  ```
171
183
 
172
- At call time:
184
+ At call time, your framework's translation function substitutes the positional values:
173
185
 
174
- ```typescript
175
- t("Save_Failed_Message", { 0: "Account", 1: "Permission denied" });
186
+ ```text
187
+ translate("Save_Failed_Message", { 0: "Account", 1: "Permission denied" })
176
188
  // → "Failed to save Account: Permission denied"
177
189
  ```
178
190
 
191
+ (The exact call syntax is framework-specific — see your framework reference's `interpolation.md`.)
192
+
179
193
  **Translations must preserve the placeholders:**
180
194
 
181
195
  ```xml
@@ -186,9 +200,9 @@ t("Save_Failed_Message", { 0: "Account", 1: "Permission denied" });
186
200
  </customLabels>
187
201
  ```
188
202
 
189
- The placeholders can move (Spanish grammar might flip the order), but they must stay as `{0}`, `{1}`; i18next does the substitution at render time.
203
+ The placeholders can move (Spanish grammar might flip the order), but they must stay as `{0}`, `{1}`; the i18n library does the substitution at render time.
190
204
 
191
- See [interpolation.md](interpolation.md) for how this works under the hood.
205
+ See your framework reference's `interpolation.md` for how this works under the hood.
192
206
 
193
207
  ---
194
208
 
@@ -259,14 +273,9 @@ export const labelManifest = ["c:Welcome_Text"];
259
273
 
260
274
  ### 4. Use in a component
261
275
 
262
- ```typescript
263
- import { useTranslation } from "react-i18next";
264
-
265
- function WelcomeBanner() {
266
- const { t } = useTranslation("c");
267
- return <h1>{t("Welcome_Text")}</h1>;
268
- }
269
- ```
276
+ Replace the hardcoded string with your framework's translation call (referencing the key by
277
+ its bare `<fullName>`). The exact call convention — the import/injection and the function
278
+ name — is in your framework reference's `localize.md`.
270
279
 
271
280
  ### 5. Deploy
272
281
 
@@ -290,7 +299,8 @@ Review the exact paths and target org with the user before deploying. Repeat the
290
299
 
291
300
  ## Related
292
301
 
293
- - [i18n-setup.md](i18n-setup.md): the init file + manifest wiring
294
- - [interpolation.md](interpolation.md): how `{0}/{1}` substitution works
302
+ - `platform-sdk-i18n.md` (this folder): the shared runtime engine that fetches these labels
295
303
  - [verifying.md](verifying.md): the serve/verify flow
296
304
  - [gotchas.md](gotchas.md): silent-fail traps
305
+ - your framework reference's `i18n-setup.md`: the init file + manifest wiring
306
+ - your framework reference's `interpolation.md`: how `{0}/{1}` substitution works
@@ -0,0 +1,169 @@
1
+ # The Platform SDK i18n engine (framework-neutral)
2
+
3
+ The runtime plumbing that resolves Salesforce Custom Labels for a UI Bundle is **the same
4
+ regardless of framework**. It ships in `@salesforce/platform-sdk` (the `/i18n` subpath) and is
5
+ what both the React (i18next) and Angular (ngx-translate) paths build on. This file documents
6
+ that shared engine once; each framework reference only covers how it wires the engine into its
7
+ own i18n library.
8
+
9
+ A UI Bundle can't use compile-time `@salesforce/label/*` imports the way LWC does (those resolve
10
+ inside the platform compiler your standalone bundle skips). Instead the app **fetches labels at
11
+ runtime** over the Salesforce GraphQL UI API and hands them to a standard i18n library to render.
12
+
13
+ ---
14
+
15
+ ## What the SDK gives you (you don't write these)
16
+
17
+ From `@salesforce/platform-sdk` (package root):
18
+
19
+ - `createDataSDK()` → returns a `DataSDK` whose `.graphql.query(...)` surface talks to the org.
20
+ Its `query` **never rejects**; transport/GraphQL errors come back on `result.errors`.
21
+
22
+ From `@salesforce/platform-sdk/i18n`:
23
+
24
+ - `fetchI18nContext(dataSDK)` — reads the user's locale context (below). **Memoized** for the page.
25
+ - `reloadI18nContext(dataSDK)` — clears that memo and re-fetches (for in-session locale changes).
26
+ - `createSalesforceDetector(dataSDK)` — an i18next-shaped language detector whose `detect()`
27
+ returns the cached context's `lang`. (React consumes this directly; Angular reads `ctx.lang`
28
+ from `fetchI18nContext` instead and calls `TranslateService.use(lang)`.)
29
+ - `SalesforceBackend` — an **i18next backend plugin** that fetches labels over GraphQL. React
30
+ plugs it straight into i18next. Angular does **not** use it; its custom `TranslateLoader`
31
+ re-implements the same GraphQL fetch (same query, same batching) against ngx-translate's
32
+ `Observable` contract.
33
+ - `createI18nFormatters(ctx)` — Intl wrappers (`formatDate`, `formatNumber`, `formatCurrency`).
34
+
35
+ You write only two thin files per app: the **init/loader wiring** (framework-specific) and the
36
+ **label manifest** (framework-neutral format, below).
37
+
38
+ ---
39
+
40
+ ## The label fetch: `uiapi.platform.labels`
41
+
42
+ Both frameworks issue the same GraphQL query to resolve labels for a language + namespace:
43
+
44
+ ```graphql
45
+ query Labels($ns: String!, $names: [String!]!, $locale: String, $fallback: LabelFallback) {
46
+ uiapi {
47
+ platform {
48
+ labels(namespace: $ns, names: $names, locale: $locale, fallback: $fallback) {
49
+ name
50
+ value
51
+ resolvedLocale
52
+ wasFallback
53
+ }
54
+ }
55
+ }
56
+ }
57
+ ```
58
+
59
+ - `ns` — the namespace (`c` for your org's custom labels; also e.g. `LightningDatatable`).
60
+ - `names` — the label API names in that namespace (no `ns:` prefix here).
61
+ - `locale` — the target language (the user's `lang`, or a B2C site language).
62
+ - `fallback` — a `LabelFallback` enum value (below).
63
+
64
+ The result is assembled into a plain `Record<string, string>` of `{ [label.name]: label.value }`.
65
+ **Labels whose `value` is null are skipped** (not added to the map), so an unresolved key falls
66
+ through to the i18n library's key-name fallback.
67
+
68
+ ### Manifest → namespaces → batches
69
+
70
+ The **label manifest** is a flat array of `"namespace:key"` strings you maintain by hand:
71
+
72
+ ```typescript
73
+ // src/i18n/label-manifest.ts
74
+ export const labelManifest = [
75
+ "c:Welcome_Text",
76
+ "c:Save_Button",
77
+ "LightningDatatable:sort",
78
+ ];
79
+ ```
80
+
81
+ The backend/loader:
82
+ 1. Splits each entry on the first `:` and **groups by namespace** (`{ c: [...], LightningDatatable: [...] }`).
83
+ 2. For each namespace + language, **dedupes** the names and **chunks them into batches of at most
84
+ 100** (`uiapi.platform.labels` rejects a single call with > 100 names).
85
+ 3. Fires the batches **in parallel** and merges the results. The load is **all-or-nothing per
86
+ namespace**: if any batch rejects, the whole namespace read fails (no half-populated namespace).
87
+
88
+ A manifest with 500 `c:*` keys becomes 5 batched queries under the hood; your manifest stays flat.
89
+
90
+ ### `LabelFallback` — pick by bundle type
91
+
92
+ ```typescript
93
+ type LabelFallback = "BASE_VALUE" | "USER_DEFAULT" | "NONE"; // default: BASE_VALUE
94
+ ```
95
+
96
+ - **B2E** — keep the `BASE_VALUE` default (a registered key with no translation for the active
97
+ language resolves to its English base value; `wasFallback: true`).
98
+ - **B2C** — set `labelFallback: "USER_DEFAULT"` so fallback follows the guest/site language context.
99
+ - Never apply the B2C override to B2E.
100
+
101
+ (When re-running the query by hand in DevTools, `$fallback` must be declared in the operation
102
+ signature **and** passed to the `labels(...)` field — a `fallback` key in the variables block alone
103
+ is silently dropped and the server defaults to `USER_DEFAULT`.)
104
+
105
+ ---
106
+
107
+ ## The locale context: `fetchI18nContext`
108
+
109
+ A separate, small GraphQL query reads the user's locale context. It runs on every boot (it is
110
+ **not** cached in localStorage — only memoized in module memory for the page lifetime):
111
+
112
+ ```graphql
113
+ query I18nDetect {
114
+ uiapi { platform { i18n { lang locale dir currency timeZone } } }
115
+ }
116
+ ```
117
+
118
+ ```typescript
119
+ interface I18nContext { lang; locale; dir; currency; timeZone; }
120
+ ```
121
+
122
+ - `lang` — the **translation** axis (`en_US`, `es`, `de`) → picks which label translations load.
123
+ - `locale` — the **formatting** axis (`de_DE`, `fr_CA`) → feeds `Intl` formatters. Region-only;
124
+ it does **not** flip label text.
125
+ - `dir` — `"ltr"` / `"rtl"` (set `document.documentElement.dir` at boot).
126
+
127
+ At boot each framework: `await fetchI18nContext(dataSDK)`, then sets `document.documentElement.dir`
128
+ and `.lang`, and tells its i18n library which language to use. `reloadI18nContext(dataSDK)` is the
129
+ only way to pick up an in-session locale change (a fresh `fetchI18nContext` returns the memo).
130
+
131
+ ### Formatting: `createI18nFormatters(ctx)`
132
+
133
+ Returns `{ formatDate, formatNumber, formatCurrency }` bound to `ctx.locale` (BCP-47),
134
+ `ctx.currency`, and `ctx.timeZone`, wrapping `Intl.*`. Note it formats off `ctx.locale` (region),
135
+ **not** `ctx.lang` (translation). Positional `{0}/{1}` interpolation does **not** format values —
136
+ pass an already-formatted string as the placeholder value if you need locale-aware numbers/dates.
137
+
138
+ ---
139
+
140
+ ## Runtime floor & dependency alignment
141
+
142
+ - The `platform.labels` runtime path for UI Bundles ships in **Salesforce Release 264 / API v68.0**.
143
+ A `sourceApiVersion` in `sfdx-project.json` records what you *declared*, not what the org
144
+ *supports* — query the org's actual max API version before wiring (`check-org-api-version.sh`,
145
+ precondition 4). Below v68.0 a localized bundle renders blank or shows raw key names.
146
+ - Keep the Platform SDK, UI Bundle, and build-plugin siblings aligned and **≥ 11.49.3**.
147
+
148
+ ---
149
+
150
+ ## How each framework consumes this engine
151
+
152
+ | Concern | React (i18next) | Angular (ngx-translate) |
153
+ |---|---|---|
154
+ | Label fetch | `SalesforceBackend` (shipped) in a chained backend | custom `TranslateLoader.getTranslation(lang)` issuing the same query |
155
+ | Language pick | `createSalesforceDetector(dataSDK)` | read `ctx.lang`, call `TranslateService.use(lang)` |
156
+ | Cache | `i18next-localstorage-backend` (`i18next_res_*`, 24h) | in-memory on `TranslateService` (reload refetches) |
157
+ | Interpolation | `interpolation: { prefix:"{", suffix:"}" }` for `{0}/{1}` | custom `TranslateParser`/compiler for `{0}/{1}` |
158
+
159
+ See your framework reference's `i18n-setup.md` for the exact wiring, and [gotchas.md](gotchas.md)
160
+ for the silent-fail traps this engine can produce.
161
+
162
+ ---
163
+
164
+ ## Related
165
+
166
+ - [label-xml.md](label-xml.md): the Custom Labels + translations metadata this engine resolves
167
+ - [verifying.md](verifying.md): build/deploy/verify labels across locales
168
+ - [gotchas.md](gotchas.md): unregistered keys, API-version bake-in, stale cache, B2C 403
169
+ - your framework reference's `i18n-setup.md` / `interpolation.md`: the framework-specific wiring
@@ -15,6 +15,19 @@ Authenticated B2E verification has five steps:
15
15
 
16
16
  B2C verification uses the site-configured languages and language-specific URLs instead of the authenticated user's Language setting. Follow the B2C section after deployment.
17
17
 
18
+ ### What this skill produces vs. what's left for you / other skills
19
+
20
+ When asked "what's left to deploy," answer with these — do **not** invent new metadata types:
21
+
22
+ - **This skill already wrote** the deployable metadata: `CustomLabels.labels-meta.xml` (English base)
23
+ and `translations/<locale>.translation-meta.xml` (the `Translations` type — **not**
24
+ `CustomObjectTranslation`; see [label-xml.md](label-xml.md)), plus the i18n wiring and manifest.
25
+ - **Manual/admin prerequisite (not a deployable file this skill emits):** activate each non-English
26
+ language in Translation Workbench (Step 2) — otherwise the deploy is rejected. There is no
27
+ "language settings" file for this skill to generate.
28
+ - **Delegated to other skills:** Experience Cloud **site languages** / `sfdc_cms__languageSettings`
29
+ → `experience-ui-bundle-site-generate`; the **deploy** itself → `experience-ui-bundle-deploy`.
30
+
18
31
  ---
19
32
 
20
33
  ## Step 1: Build the app
@@ -132,10 +145,10 @@ This step is for **B2E only**. For B2C, use the site language route below.
132
145
  3. If the configured language or deployed metadata requires publishing, treat publication as a separate go-live mutation. Resolve the site's `Network.Name` from the target site (prefer matching its `UrlPathPrefix`), show that community name and target org, and wait for explicit user confirmation immediately before running `sf community publish --name "<network-name>" --target-org <org-alias>`. A metadata deployment is not publication approval. Track the returned background operation to completion before guest verification; if propagation is still in progress, report that instead of treating stale output as a localization failure.
133
146
  4. Open the published site as a signed-out guest through its language-specific URL. Confirm the boot environment exposes the same language in `SFDC_ENV.language`.
134
147
  5. Confirm translated labels render and the labels GraphQL request succeeds for the guest.
135
- 6. Use the site's language switcher. It must navigate to the target language URL and cause a **full page reload**; changing i18next in place leaves the boot-time SDK context and cached resources stale.
148
+ 6. Use the site's language switcher. It must navigate to the target language URL and cause a **full page reload**; changing the i18n library's language in place leaves the boot-time SDK context and cached resources stale.
136
149
  7. Confirm the new route and `SFDC_ENV.language` agree, then verify the translated labels again.
137
150
 
138
- For a localized local preview, supply an explicit configured language in the preview URL/route. Do not use the authenticated org user's Language as evidence for guest behavior. If a recent translation or route change appears stale, delete `i18next_res_*` localStorage entries and reload each language URL.
151
+ For a localized local preview, supply an explicit configured language in the preview URL/route. Do not use the authenticated org user's Language as evidence for guest behavior. If a recent translation or route change appears stale, clear your i18n library's cached labels and reload each language URL (the exact cache keys are in your framework reference's `gotchas.md`).
139
152
 
140
153
  ---
141
154
 
@@ -162,7 +175,7 @@ When you test localization, **change the Language**, not the Locale. Changing Lo
162
175
 
163
176
  **Check:** Go to Setup → Translation Workbench → Translate → pick the language and the label. Is the translation there? If not, author it and re-deploy.
164
177
 
165
- **Fallback behavior:** If a registered key is untranslated for the active language, the Platform SDK's GraphQL request resolves it according to `labelFallback`: B2E uses `BASE_VALUE`; B2C explicitly uses `USER_DEFAULT`. The i18next `fallbackLng` applies only if no resource is loaded for the detected language; it does not define Salesforce label-resolution semantics.
178
+ **Fallback behavior:** If a registered key is untranslated for the active language, the Platform SDK's GraphQL request resolves it according to `labelFallback`: B2E uses `BASE_VALUE`; B2C explicitly uses `USER_DEFAULT`. Your i18n library's own language fallback (i18next's `fallbackLng`, ngx-translate's `fallbackLang` — `defaultLanguage` in ngx-translate majors before v16) applies only if no resource is loaded for the detected language; it does not define Salesforce label-resolution semantics.
166
179
 
167
180
  ---
168
181
 
@@ -170,7 +183,7 @@ When you test localization, **change the Language**, not the Locale. Changing Lo
170
183
 
171
184
  You see the literal string `Welcome_Text` on screen instead of "Welcome."
172
185
 
173
- **Cause:** The key isn't in your `label-manifest.ts`. The app only fetches labels listed in the manifest, so an unregistered key is **never requested**, and i18next, finding nothing, renders the key string. **No console warning, no error**; it fails silently.
186
+ **Cause:** The key isn't in your `label-manifest.ts`. The app only fetches labels listed in the manifest, so an unregistered key is **never requested**, and the i18n library, finding nothing, renders the key string. **No console warning, no error**; it fails silently.
174
187
 
175
188
  **Fix:** Add the `"c:Key"` entry to `label-manifest.ts` (Step 3 of the workflow). This is the most common localization bug; if a label looks wrong, check the manifest first.
176
189
 
@@ -182,17 +195,13 @@ See [gotchas.md](gotchas.md) for the full explanation.
182
195
 
183
196
  You edited a label in the Translation Workbench (or redeployed a `translation-meta.xml`), confirmed the new value is in the org, but the app keeps rendering the **previous** value on reload.
184
197
 
185
- **Cause:** The label cache. Your `src/i18n/index.ts` chains two backends, `[LocalStorageBackend, SalesforceBackend]`, so on load i18next reads labels from **localStorage first** and only falls through to GraphQL on a cache miss. Labels are cached per language+namespace under keys like `i18next_res_de-c` (DevTools → Application → Local Storage), with a 24-hour `expirationTime`. Until that entry expires, your app serves the cached copy and never refetches.
198
+ **Cause:** The label cache. To avoid a GraphQL roundtrip on every boot, the i18n setup caches fetched labels client-side (typically in `localStorage`) per language+namespace, with an expiration window (commonly 24 hours). Until that entry expires, your app serves the cached copy and never refetches — so a fresh org value doesn't appear.
186
199
 
187
- **Fix:** Clear the cached labels, then reload:
188
- - **DevTools → Application → Local Storage** → delete the `i18next_res_*` keys
189
- - Or **Clear site data** to wipe everything
200
+ **Fix:** Clear the cached labels, then reload (**DevTools → Application → Local Storage** → delete the cached label entries, or **Clear site data** to wipe everything). The next load misses the cache, refetches over GraphQL, and shows the current value. The **exact cache keys and where they live are framework-specific** — see your framework reference's `gotchas.md` (e.g. React/i18next uses `i18next_res_*`).
190
201
 
191
- The next load misses the cache, refetches over GraphQL, and shows the current value.
202
+ **This is expected behavior, not a bug.** The cache is what makes labels fast after first load. In production, a translation change takes up to the expiration window to roll out; during development, clear the cache to see edits immediately.
192
203
 
193
- **This is expected behavior, not a bug.** The cache is what makes labels fast after first load. In production, a translation change takes up to `expirationTime` (24 hours) to roll out; during development, clear the cache to see edits immediately.
194
-
195
- See [gotchas.md](gotchas.md) for the full explanation.
204
+ See your framework reference's `gotchas.md` for the full explanation.
196
205
 
197
206
  ---
198
207
 
@@ -200,7 +209,7 @@ See [gotchas.md](gotchas.md) for the full explanation.
200
209
 
201
210
  This is expected, not a bug, **as long as you only authored the base (`en_US`) translation.**
202
211
 
203
- For B2E, the shipped `SalesforceBackend` default is `BASE_VALUE`. The GraphQL server returns the label's base value for a regional Language with no explicit translation: `resolvedLocale` comes back as the base (`en_US`) with `wasFallback: true`. The server does **not** map `en_GB → en_US` region-aware; it honors the base-value fallback.
212
+ For B2E, the label fetch default is `BASE_VALUE`. The GraphQL server returns the label's base value for a regional Language with no explicit translation: `resolvedLocale` comes back as the base (`en_US`) with `wasFallback: true`. The server does **not** map `en_GB → en_US` region-aware; it honors the base-value fallback.
204
213
 
205
214
  **Fix (only if you want region-specific text):** Author a translation for that exact regional Language (`en_GB.translation-meta.xml`). Otherwise the base value is the intended, correct result.
206
215
 
@@ -218,7 +227,7 @@ Use this to confirm everything works:
218
227
  - [ ] Opened at `lightning.force.com/lwr/application/ai/<namespace>-<bundleName>` (redirected to `.my.salesforce.app` is fine)
219
228
  - [ ] Changed user's **Language** (not Locale) to the translated language
220
229
  - [ ] Reloaded, labels render in the new language
221
- - [ ] If labels are stale, cleared `i18next_res_*` from localStorage
230
+ - [ ] If labels are stale, cleared the i18n library's cached labels from localStorage (see your framework `gotchas.md`)
222
231
  - [ ] B2C: site languages are configured/published and the language URL matches `SFDC_ENV.language`
223
232
  - [ ] B2C: any site publication received separate explicit confirmation for the named site and org
224
233
  - [ ] B2C: signed-out guest labels GraphQL succeeds (no HTTP 403)
@@ -241,7 +250,8 @@ Use this to confirm everything works:
241
250
 
242
251
  ## Related
243
252
 
244
- - [i18n-setup.md](i18n-setup.md): the init file + manifest
245
253
  - [label-xml.md](label-xml.md): Custom Labels + translations metadata
246
- - [interpolation.md](interpolation.md): `{0}/{1}` placeholders
247
254
  - [gotchas.md](gotchas.md): silent-fail traps (unregistered keys, stale cache, API-version mismatch)
255
+ - `platform-sdk-i18n.md` (this folder): the shared runtime engine (Labels query, fallback)
256
+ - your framework reference's `i18n-setup.md`: the init file + manifest
257
+ - your framework reference's `interpolation.md`: `{0}/{1}` placeholders
@@ -1,7 +1,12 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail # exit on error (-e), undefined vars (-u), and propagate pipeline failures (-o pipefail)
3
3
  #
4
- # check-i18n-wired.sh — Confirm the i18n init exists and is called at boot.
4
+ # check-i18n-wired.sh — Confirm the React i18next init exists and is called at boot.
5
+ #
6
+ # React-specific: the manifest -> backendOptions / SalesforceBackend detection is
7
+ # i18next-shaped and has no ngx-translate analog, so this script lives under the
8
+ # React reference (references/react/) rather than the shared scripts/ folder. The
9
+ # Angular equivalent is authored separately under references/angular/.
5
10
  #
6
11
  # Whether the app already has i18n wiring is a file-existence plus call-site
7
12
  # check, so run this rather than reading for it by hand. It looks for an init
@@ -9,7 +14,7 @@ set -euo pipefail # exit on error (-e), undefined vars (-u), and propagate pipe
9
14
  # file.
10
15
  #
11
16
  # Usage (run from the UI bundle dir, or pass its src path):
12
- # bash <skill-dir>/scripts/check-i18n-wired.sh [src-dir]
17
+ # bash <skill-dir>/references/react/check-i18n-wired.sh [src-dir]
13
18
  #
14
19
  # src-dir defaults to "src".
15
20
  #
@@ -23,7 +28,7 @@ set -euo pipefail # exit on error (-e), undefined vars (-u), and propagate pipe
23
28
  # 0 fully wired — initI18n() defined, called at boot, AND the manifest is
24
29
  # passed into the SalesforceBackend's backendOptions. Just add new keys.
25
30
  # 1 no i18n init found — no initI18n() definition. Scaffold the whole setup
26
- # (see references/i18n-setup.md).
31
+ # (see references/react/i18n-setup.md).
27
32
  # 2 init defined but not called at boot — add only the boot-time initI18n()
28
33
  # call in the entry file; do NOT re-scaffold or overwrite the init.
29
34
  # 3 wired at boot but the manifest could not be confirmed as passed into the