@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.
- package/package.json +1 -1
- package/skills/experience-ui-bundle-deploy/SKILL.md +43 -2
- package/skills/experience-ui-bundle-deploy/references/config-scaffold.md +16 -3
- package/skills/experience-ui-bundle-deploy/references/logout-url.md +97 -0
- package/skills/experience-ui-bundle-deploy/scripts/set-logout-url.mjs +304 -0
- package/skills/experience-ui-bundle-localize/SKILL.md +107 -90
- package/skills/experience-ui-bundle-localize/references/angular/check-i18n-wired.sh +159 -0
- package/skills/experience-ui-bundle-localize/references/angular/i18n-setup.md +250 -0
- package/skills/experience-ui-bundle-localize/references/angular/interpolation.md +156 -0
- package/skills/experience-ui-bundle-localize/references/angular/localize.md +111 -0
- package/skills/experience-ui-bundle-localize/references/{gotchas.md → common/gotchas.md} +27 -43
- package/skills/experience-ui-bundle-localize/references/{label-xml.md → common/label-xml.md} +27 -17
- package/skills/experience-ui-bundle-localize/references/common/platform-sdk-i18n.md +169 -0
- package/skills/experience-ui-bundle-localize/references/{verifying.md → common/verifying.md} +26 -16
- package/skills/experience-ui-bundle-localize/{scripts → references/react}/check-i18n-wired.sh +8 -3
- package/skills/experience-ui-bundle-localize/references/{i18n-setup.md → react/i18n-setup.md} +48 -10
- package/skills/experience-ui-bundle-localize/references/{interpolation.md → react/interpolation.md} +4 -4
- package/skills/experience-ui-bundle-localize/references/react/localize.md +76 -0
- package/skills/experience-ui-bundle-localize/scripts/check-manifest-registered.sh +92 -24
- package/skills/experience-ui-bundle-localize/scripts/detect-framework.sh +73 -0
- package/skills/experience-ui-bundle-site-generate/SKILL.md +1 -1
- package/skills/field-service-data-capture-form-deployer-configure/SKILL.md +1 -1
- package/skills/field-service-data-capture-form-deployer-configure/references/flow-metadata-json.md +1 -1
- package/skills/field-service-data-capture-form-editor-configure/SKILL.md +1 -1
- package/skills/field-service-data-capture-reference-configure/SKILL.md +28 -15
- package/skills/field-service-foundation-setup-designer-get/SKILL.md +1 -1
- package/skills/field-service-mobile-branding-configure/SKILL.md +1 -1
- package/skills/field-service-objective-designer-configure/SKILL.md +381 -64
- package/skills/field-service-prework-brief-deployer-configure/SKILL.md +566 -3
- package/skills/field-service-scheduling-policy-designer-query/SKILL.md +1097 -57
- package/skills/field-service-sobject-create-configure/SKILL.md +222 -7
- package/skills/field-service-voice-to-form-configure/SKILL.md +11 -11
- package/skills/field-service-work-rule-designer-configure/SKILL.md +92 -107
- package/skills/service-digital-engagement-channel-configure/SKILL.md +20 -37
- package/skills/service-digital-engagement-channel-configure/assets/messaging_channel_template.xml +3 -2
- package/skills/service-digital-engagement-channel-configure/examples/asa_agent_channel.xml +4 -4
- package/skills/service-helpagent-coordinate/SKILL.md +37 -29
- package/skills/service-helpagent-coordinate/assets/help-agent-spec.md +33 -28
- package/skills/service-helpagent-coordinate/references/agent-script.md +4 -1
- package/skills/service-helpagent-coordinate/references/channel-voice.md +1 -1
- 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
|
|
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
|
-
```
|
|
26
|
-
// component
|
|
27
|
-
|
|
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
|
|
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
|
|
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.
|
|
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,
|
|
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
|
|
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
|
|
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:**
|
|
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
|
-
|
|
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:**
|
|
122
|
+
**Fix:** Invalidate the cache, then reload:
|
|
139
123
|
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
package/skills/experience-ui-bundle-localize/references/{label-xml.md → common/label-xml.md}
RENAMED
|
@@ -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 `
|
|
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 `&`, `<` becomes `<`, and `>` becomes `>`. 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
|
|
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
|
-
```
|
|
175
|
-
|
|
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}`;
|
|
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
|
|
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
|
-
|
|
263
|
-
|
|
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
|
-
-
|
|
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
|
package/skills/experience-ui-bundle-localize/references/{verifying.md → common/verifying.md}
RENAMED
|
@@ -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
|
|
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,
|
|
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`.
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
package/skills/experience-ui-bundle-localize/{scripts → references/react}/check-i18n-wired.sh
RENAMED
|
@@ -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
|
|
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>/
|
|
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
|