@salesforce/afv-skills 1.34.0 → 1.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/automation-sandbox-post-copy-config-generate/SKILL.md +239 -0
- package/skills/automation-sandbox-post-copy-config-generate/assets/config_template.json +21 -0
- package/skills/automation-sandbox-post-copy-config-generate/assets/json_schema.json +90 -0
- package/skills/automation-sandbox-post-copy-config-generate/examples/sample_sop_excerpt.md +31 -0
- package/skills/automation-sandbox-post-copy-config-generate/examples/sample_sop_to_config.json +50 -0
- package/skills/automation-sandbox-post-copy-config-generate/references/configuration_catalog.md +76 -0
- package/skills/automation-sandbox-post-copy-config-generate/references/sop_parsing_patterns.md +157 -0
- package/skills/automation-sandbox-post-copy-config-generate/references/source_format_handling.md +230 -0
- package/skills/dx-apexguru-scan/SKILL.md +403 -0
- package/skills/dx-apexguru-scan/examples/README.md +54 -0
- package/skills/dx-apexguru-scan/examples/sample-decoded-summary.json +176 -0
- package/skills/dx-apexguru-scan/examples/sample-full-no-runtime-response.json +26 -0
- package/skills/dx-apexguru-scan/examples/sample-succeeded-response.json +15 -0
- package/skills/dx-apexguru-scan/references/api-reference.md +81 -0
- package/skills/dx-apexguru-scan/references/authentication.md +134 -0
- package/skills/dx-apexguru-scan/references/error-handling.md +56 -0
- package/skills/dx-apexguru-scan/references/violation-catalog.md +28 -0
- package/skills/dx-apexguru-scan/scripts/build-zip.sh +87 -0
- package/skills/dx-apexguru-scan/scripts/decode-report.js +389 -0
- package/skills/dx-apexguru-scan/scripts/resolve-token.sh +151 -0
- package/skills/dx-apexguru-scan/scripts/run-scan.sh +153 -0
- package/skills/dx-apexguru-scan/scripts/scan.sh +96 -0
- package/skills/dx-apexguru-scan/scripts/validate-token.js +121 -0
- package/skills/dx-devops-pipeline-manage/SKILL.md +263 -0
- package/skills/dx-devops-pipeline-manage/examples/common-workflows.md +177 -0
- package/skills/dx-devops-pipeline-manage/references/cli-commands.md +298 -0
- package/skills/dx-devops-pipeline-manage/references/parsing-patterns.md +134 -0
- package/skills/dx-devops-pipeline-manage/scripts/check-activation-ready.sh +34 -0
- package/skills/dx-devops-pipeline-manage/scripts/validate-org-type.sh +17 -0
- package/skills/dx-devops-pipeline-manage/scripts/verify-operation.sh +82 -0
- package/skills/dx-devops-promote/SKILL.md +214 -0
- package/skills/dx-devops-promote/examples/promotion-workflows.md +212 -0
- package/skills/dx-devops-promote/references/cli-commands.md +303 -0
- package/skills/experience-lwc-base-components-integrate/SKILL.md +176 -0
- package/skills/experience-lwc-base-components-integrate/references/lbc-expert-guidance.md +127 -0
- package/skills/experience-lwc-base-components-integrate/references/lightning-component-index.md +179 -0
- package/skills/experience-lwc-base-components-integrate/references/lightning-components.md +5429 -0
- package/skills/experience-lwc-base-components-integrate/scripts/extract-component-docs.sh +61 -0
- package/skills/experience-lwc-rtl-validate/SKILL.md +149 -0
- package/skills/experience-lwc-rtl-validate/references/rtl-expert.md +892 -0
- package/skills/experience-lwc-rtl-validate/scripts/scan-rtl-css.sh +206 -0
- package/skills/experience-lwc-typescript-migrate/SKILL.md +207 -0
- package/skills/experience-lwc-typescript-migrate/assets/dts-template.ts +15 -0
- package/skills/experience-lwc-typescript-migrate/assets/type-patterns.ts +44 -0
- package/skills/experience-lwc-typescript-migrate/scripts/find-consumers.sh +128 -0
- package/skills/experience-ui-bundle-localize/SKILL.md +323 -0
- package/skills/experience-ui-bundle-localize/references/gotchas.md +249 -0
- package/skills/experience-ui-bundle-localize/references/i18n-setup.md +169 -0
- package/skills/experience-ui-bundle-localize/references/interpolation.md +311 -0
- package/skills/experience-ui-bundle-localize/references/label-xml.md +282 -0
- package/skills/experience-ui-bundle-localize/references/verifying.md +219 -0
- package/skills/experience-ui-bundle-localize/scripts/check-i18n-wired.sh +195 -0
- package/skills/experience-ui-bundle-localize/scripts/check-manifest-registered.sh +100 -0
- package/skills/experience-ui-bundle-localize/scripts/check-org-api-version.sh +40 -0
- package/skills/experience-ui-bundle-localize/scripts/detect-bundle-type.sh +57 -0
- package/skills/platform-custom-lightning-type-generate/SKILL.md +3 -0
- package/skills/platform-custom-lightning-type-generate/assets/primitive-types-and-constraints.md +1 -1
- package/skills/platform-mcp-tool-widget-coordinate/SKILL.md +250 -0
- package/skills/platform-mcp-tool-widget-coordinate/examples/action-name-source-prompt.md +74 -0
- package/skills/platform-mcp-tool-widget-coordinate/examples/apex-invocable-source-prompt.md +90 -0
- package/skills/platform-mcp-tool-widget-coordinate/examples/nested-object-source-prompt.md +191 -0
- package/skills/platform-mcp-tool-widget-coordinate/examples/pasted-tool-output-prompt.md +85 -0
- package/skills/platform-mcp-tool-widget-coordinate/references/build-plan-format.md +74 -0
- package/skills/platform-mcp-tool-widget-coordinate/references/mcp-tool-output-discovery.md +184 -0
- package/skills/platform-mcp-tool-widget-coordinate/references/two-clt-modeling.md +128 -0
- package/skills/platform-mcp-tool-widget-coordinate/references/validation-gates.md +181 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# i18n Setup: the two files you write
|
|
2
|
+
|
|
3
|
+
You write two files to set up i18n in a React UI Bundle. The Platform SDK provides the runtime plumbing (detector, backend, context fetch); you just wire it into i18next.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## File 1: `src/i18n/index.ts` (the init wiring)
|
|
8
|
+
|
|
9
|
+
This is the only "glue" you write. It connects the SDK's i18n pieces to i18next.
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { createDataSDK } from "@salesforce/platform-sdk";
|
|
13
|
+
import {
|
|
14
|
+
createSalesforceDetector,
|
|
15
|
+
fetchI18nContext,
|
|
16
|
+
SalesforceBackend,
|
|
17
|
+
} from "@salesforce/platform-sdk/i18n";
|
|
18
|
+
import i18next from "i18next";
|
|
19
|
+
import ChainedBackend from "i18next-chained-backend";
|
|
20
|
+
import LocalStorageBackend from "i18next-localstorage-backend";
|
|
21
|
+
import { initReactI18next } from "react-i18next";
|
|
22
|
+
import { labelManifest } from "./label-manifest";
|
|
23
|
+
|
|
24
|
+
export async function initI18n() {
|
|
25
|
+
const dataSDK = await createDataSDK();
|
|
26
|
+
const ctx = await fetchI18nContext(dataSDK);
|
|
27
|
+
|
|
28
|
+
// Tell the browser the user's language + text direction (RTL support).
|
|
29
|
+
document.documentElement.dir = ctx.dir;
|
|
30
|
+
document.documentElement.lang = ctx.lang;
|
|
31
|
+
|
|
32
|
+
await i18next
|
|
33
|
+
.use(ChainedBackend)
|
|
34
|
+
.use(createSalesforceDetector(dataSDK))
|
|
35
|
+
.use(initReactI18next)
|
|
36
|
+
.init({
|
|
37
|
+
fallbackLng: "en", // untranslated keys fall back to the English base value
|
|
38
|
+
defaultNS: "c", // "c" = your org's custom-label namespace
|
|
39
|
+
backend: {
|
|
40
|
+
backends: [LocalStorageBackend, SalesforceBackend],
|
|
41
|
+
backendOptions: [
|
|
42
|
+
{ expirationTime: 86400000 }, // cache labels in localStorage for a day
|
|
43
|
+
{ dataSDK, labelManifest }, // fetch the rest over GraphQL
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
interpolation: {
|
|
47
|
+
// escapeValue: false is correct for React: React already escapes JSX
|
|
48
|
+
// output. Do NOT feed interpolated label output into
|
|
49
|
+
// dangerouslySetInnerHTML; that bypasses React's escaping and, with this
|
|
50
|
+
// setting, is an XSS vector when a label interpolates user-controlled
|
|
51
|
+
// text. Render labels as normal JSX (`{t(...)}`).
|
|
52
|
+
escapeValue: false,
|
|
53
|
+
prefix: "{", // Salesforce labels interpolate with {0}, {1}, …
|
|
54
|
+
suffix: "}",
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Call it once at boot**, before mounting your app:
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
// src/index.tsx
|
|
64
|
+
import { initI18n } from "./i18n";
|
|
65
|
+
|
|
66
|
+
initI18n().then(() => {
|
|
67
|
+
// mount app here
|
|
68
|
+
root.render(<App />);
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## File 2: `src/i18n/label-manifest.ts` (the list of labels your app uses)
|
|
75
|
+
|
|
76
|
+
This tells i18next which labels to fetch at boot.
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
export const labelManifest = [
|
|
80
|
+
"c:Welcome_Text",
|
|
81
|
+
"c:Save_Button",
|
|
82
|
+
"c:Save_Failed_Message",
|
|
83
|
+
// one entry per label, "namespace:Key"
|
|
84
|
+
];
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Format:** `"<namespace>:<Key>"`
|
|
88
|
+
- `c` = custom labels in your org
|
|
89
|
+
- `Key` = the `<fullName>` from your `CustomLabels.labels-meta.xml`
|
|
90
|
+
|
|
91
|
+
The manifest is how i18next knows what to fetch. An **unregistered key fails silently**: it renders as its own literal name (e.g., `"Welcome_Text"` instead of "Welcome") with no console warning. Always keep the manifest in sync with your `t()` calls.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Dependencies
|
|
96
|
+
|
|
97
|
+
Install these first (Step 4 of the main workflow):
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
npm install i18next react-i18next i18next-chained-backend i18next-localstorage-backend
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Use `@salesforce/platform-sdk` **≥11.42.1**. The `@salesforce/platform-sdk/i18n` subpath (`SalesforceBackend`, `createSalesforceDetector`, `fetchI18nContext`) has existed since 11.4.1, and 11.7.0 added `reloadI18nContext` for refreshing the cached label context. 11.42.1 is the validated floor because it carries the fix that batches a namespace into 100-name-or-fewer queries. Without it, a manifest of more than 100 labels in one namespace hits the `uiapi.platform.labels` limit and the whole namespace read fails. Keep the SDK's siblings (`@salesforce/vite-plugin-ui-bundle`, `@salesforce/ui-bundle`) on the same version.
|
|
104
|
+
|
|
105
|
+
Known-good companion versions:
|
|
106
|
+
- `i18next` **^24.2.2**
|
|
107
|
+
- `react-i18next` **^15.5.1**
|
|
108
|
+
- `i18next-chained-backend` **^4.6.2**
|
|
109
|
+
- `i18next-localstorage-backend` **^4.2.0**
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## What you DON'T write
|
|
114
|
+
|
|
115
|
+
The Platform SDK ships with:
|
|
116
|
+
- `createSalesforceDetector`: reads the user's language from the org
|
|
117
|
+
- `SalesforceBackend`: fetches labels over GraphQL
|
|
118
|
+
- `fetchI18nContext`: gets language, locale, text direction, currency
|
|
119
|
+
|
|
120
|
+
If you see an older example that vendors `salesforce-detector.ts` or `salesforce-backend.ts` into `src/`, it predates the SDK's i18n export. You no longer copy those files in; just import from `@salesforce/platform-sdk/i18n`.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## How it works at boot
|
|
125
|
+
|
|
126
|
+
1. Bundle loads, `initI18n()` runs
|
|
127
|
+
2. `createDataSDK()` initializes the SDK
|
|
128
|
+
3. `fetchI18nContext()` queries the org for the user's language/locale/direction
|
|
129
|
+
4. `SalesforceBackend` reads the manifest and issues a GraphQL query per namespace:
|
|
130
|
+
```graphql
|
|
131
|
+
query LoadLabels {
|
|
132
|
+
uiapi {
|
|
133
|
+
platform {
|
|
134
|
+
labels(namespace: "c", names: ["Welcome_Text", "Save_Button", ...]) {
|
|
135
|
+
name
|
|
136
|
+
value
|
|
137
|
+
resolvedLocale
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
5. Platform returns labels at the user's resolved locale
|
|
144
|
+
6. i18next caches them (in memory + localStorage)
|
|
145
|
+
7. React mounts; components call `t()`; lookups hit the cache
|
|
146
|
+
|
|
147
|
+
The two backends chain: `LocalStorageBackend` serves cached labels (24-hour expiry), and `SalesforceBackend` fetches misses over GraphQL. This makes subsequent loads fast.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Namespace note
|
|
152
|
+
|
|
153
|
+
`defaultNS: "c"` means components can call `t("Welcome_Text")` instead of `t("c:Welcome_Text")`; the namespace is implicit. If you're loading labels from multiple namespaces (e.g., framework-shipped labels like `LightningDatatable`), you'd specify the namespace in the `useTranslation` hook:
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
const { t } = useTranslation("c"); // custom labels
|
|
157
|
+
const { t: tFw } = useTranslation("LightningDatatable"); // framework labels
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
For most bundles, a single `"c"` namespace is all you need.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Related
|
|
165
|
+
|
|
166
|
+
- [label-xml.md](label-xml.md): the Custom Labels metadata XML shape
|
|
167
|
+
- [interpolation.md](interpolation.md): how `{0}/{1}` placeholders work
|
|
168
|
+
- [verifying.md](verifying.md): the serve/verify flow
|
|
169
|
+
- [gotchas.md](gotchas.md): silent-fail traps to avoid
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# Interpolation: how `{0}`, `{1}` placeholders work
|
|
2
|
+
|
|
3
|
+
Salesforce Custom Labels use **positional interpolation**: `{0}`, `{1}`, `{N}` placeholders that get replaced with runtime values at render time. This is the same substitution syntax used by Apex `String.format` and LWC `@salesforce/label`, so one label works across all three frameworks.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## TLDR
|
|
8
|
+
|
|
9
|
+
It's a string-replace. i18next finds every `{0}`, `{1}`, `{N}` in the cached label content and substitutes the value at that key from the second argument of `t()`.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Basic example
|
|
14
|
+
|
|
15
|
+
**Label:**
|
|
16
|
+
```xml
|
|
17
|
+
<labels>
|
|
18
|
+
<fullName>Greeting</fullName>
|
|
19
|
+
<value>Hello, {0}</value>
|
|
20
|
+
</labels>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Component:**
|
|
24
|
+
```typescript
|
|
25
|
+
const { t } = useTranslation("c");
|
|
26
|
+
const userName = "Tosin";
|
|
27
|
+
return <h1>{t("Greeting", { 0: userName })}</h1>;
|
|
28
|
+
// → "Hello, Tosin"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## How it works
|
|
34
|
+
|
|
35
|
+
1. At boot, the Platform SDK's `SalesforceBackend` fetches the label content from the org via GraphQL
|
|
36
|
+
2. i18next caches it: `{ "c:Greeting": "Hello, {0}" }`
|
|
37
|
+
3. At render time, the component calls `t("Greeting", { 0: "Tosin" })`
|
|
38
|
+
4. i18next's interpolator does:
|
|
39
|
+
```javascript
|
|
40
|
+
template = "Hello, {0}";
|
|
41
|
+
template.replace("{0}", "Tosin");
|
|
42
|
+
// → "Hello, Tosin"
|
|
43
|
+
```
|
|
44
|
+
5. The resulting string renders to the DOM
|
|
45
|
+
|
|
46
|
+
The configuration that makes this work is in `src/i18n/index.ts`:
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
interpolation: {
|
|
50
|
+
escapeValue: false,
|
|
51
|
+
prefix: "{", // scan for {X} placeholders
|
|
52
|
+
suffix: "}",
|
|
53
|
+
},
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
i18next ships with `{{name}}`-style named interpolation by default. We override it to use Salesforce's `{0}` positional style so one label works across Apex, LWC, and React.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Multiple placeholders
|
|
61
|
+
|
|
62
|
+
**Label:**
|
|
63
|
+
```xml
|
|
64
|
+
<value>Failed to save {0}: {1}</value>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Component:**
|
|
68
|
+
```typescript
|
|
69
|
+
t("Save_Failed", { 0: "Account", 1: "Permission denied" });
|
|
70
|
+
// → "Failed to save Account: Permission denied"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Order in the object doesn't matter; the placeholder **number** is what binds:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
t("Save_Failed", { 1: "Permission denied", 0: "Account" }); // same result
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Same placeholder used twice
|
|
82
|
+
|
|
83
|
+
Salesforce labels sometimes reuse the same positional placeholder:
|
|
84
|
+
|
|
85
|
+
**Label:**
|
|
86
|
+
```xml
|
|
87
|
+
<value>User {0} cannot edit {0}'s own profile</value>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Component:**
|
|
91
|
+
```typescript
|
|
92
|
+
t("Profile_Edit_Error", { 0: "tosin" });
|
|
93
|
+
// → "User tosin cannot edit tosin's own profile"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
This is common when sentence structure differs across languages and the same value appears in different grammatical roles.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Numeric and date values
|
|
101
|
+
|
|
102
|
+
Placeholders accept any JavaScript value. The value's `toString()` is what gets interpolated:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
// Numbers
|
|
106
|
+
t("Record_Count", { 0: 42, 1: 100 });
|
|
107
|
+
// label: "Showing {0} of {1} records" → "Showing 42 of 100 records"
|
|
108
|
+
|
|
109
|
+
// Dates (default JS toString, not localized)
|
|
110
|
+
t("Last_Modified", { 0: new Date() });
|
|
111
|
+
// label: "Last modified {0}" → "Last modified Mon Jul 07 2026 14:23:00 GMT-0700"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**Note:** This does **not** apply locale-aware formatting (e.g., `1.000,00` for German number formatting). For that, you'd use `Intl.NumberFormat` / `Intl.DateTimeFormat` separately and pass the **formatted string** as the placeholder value:
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
const formattedCount = new Intl.NumberFormat(locale).format(count);
|
|
118
|
+
t("Record_Count", { 0: formattedCount, 1: total });
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The Platform SDK's `fetchI18nContext()` gives you `ctx.locale` and `ctx.currency` for feeding `Intl` formatters.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Translations must preserve placeholders
|
|
126
|
+
|
|
127
|
+
**English:**
|
|
128
|
+
```xml
|
|
129
|
+
<value>Failed to save {0}: {1}</value>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Spanish translation:**
|
|
133
|
+
```xml
|
|
134
|
+
<label>Error al guardar {0}: {1}</label>
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The placeholders stay as `{0}`, `{1}`; they can move (grammar might flip the order), but the **numbers must match**. If the Spanish translation says `{A}` or drops `{1}`, the substitution breaks.
|
|
138
|
+
|
|
139
|
+
This is why Translation Workbench is valuable: it shows translators the placeholders and warns if they're missing.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Failure modes
|
|
144
|
+
|
|
145
|
+
### Missing placeholder value
|
|
146
|
+
|
|
147
|
+
**Label:** `"Hello, {0} from {1}"`
|
|
148
|
+
|
|
149
|
+
**Call:**
|
|
150
|
+
```typescript
|
|
151
|
+
t("Greeting", { 0: "Tosin" }); // forgot {1}
|
|
152
|
+
// → "Hello, Tosin from {1}" (literal placeholder leaks)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
There's no runtime error or console warning; the unsubstituted placeholder just renders as-is. This is a visual bug, not a crash.
|
|
156
|
+
|
|
157
|
+
**Fix:** Always pass every placeholder the label expects. PR review catches this today (no build-time validation yet).
|
|
158
|
+
|
|
159
|
+
### Missing label entirely
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
t("Nonexistent_Key");
|
|
163
|
+
// → "Nonexistent_Key" (literal key string)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
This is the **unregistered manifest key** trap (see [gotchas.md](gotchas.md)): the label wasn't fetched, so i18next has nothing to interpolate.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Worked examples by complexity
|
|
171
|
+
|
|
172
|
+
### 1. No interpolation (most labels)
|
|
173
|
+
|
|
174
|
+
**Label:** `"Welcome"`
|
|
175
|
+
|
|
176
|
+
**Call:** `t("Welcome_Text")`
|
|
177
|
+
|
|
178
|
+
**Result:** `"Welcome"`
|
|
179
|
+
|
|
180
|
+
Most labels are just lookups, no placeholders.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
### 2. Single value
|
|
185
|
+
|
|
186
|
+
**Label:** `"You have {0} items"`
|
|
187
|
+
|
|
188
|
+
**Call:** `t("Item_Count", { 0: 5 })`
|
|
189
|
+
|
|
190
|
+
**Result:** `"You have 5 items"`
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
### 3. Error with context
|
|
195
|
+
|
|
196
|
+
**Label:** `"{0} failed at step {1} for record {2}: {3}"`
|
|
197
|
+
|
|
198
|
+
**Call:**
|
|
199
|
+
```typescript
|
|
200
|
+
t("Pipeline_Error", {
|
|
201
|
+
0: "OnboardingPipeline",
|
|
202
|
+
1: err.step,
|
|
203
|
+
2: record.name,
|
|
204
|
+
3: err.message,
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Result:** `"OnboardingPipeline failed at step ValidateAccount for record Acme Corp: Missing tax ID"`
|
|
209
|
+
|
|
210
|
+
One label, reusable across pipelines. Translated once. Engineers in any framework call it the same way.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
### 4. Pluralization (leveraging i18next on top of positional)
|
|
215
|
+
|
|
216
|
+
i18next has native pluralization support. For Salesforce labels, you author separate labels for each plural form:
|
|
217
|
+
|
|
218
|
+
**Labels:**
|
|
219
|
+
```xml
|
|
220
|
+
<labels>
|
|
221
|
+
<fullName>Item_Count_one</fullName>
|
|
222
|
+
<value>You have {0} item</value>
|
|
223
|
+
</labels>
|
|
224
|
+
<labels>
|
|
225
|
+
<fullName>Item_Count_other</fullName>
|
|
226
|
+
<value>You have {0} items</value>
|
|
227
|
+
</labels>
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**Manifest:**
|
|
231
|
+
```typescript
|
|
232
|
+
export const labelManifest = [
|
|
233
|
+
"c:Item_Count_one",
|
|
234
|
+
"c:Item_Count_other",
|
|
235
|
+
];
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**Component:**
|
|
239
|
+
```typescript
|
|
240
|
+
t("Item_Count", { 0: count, count: count });
|
|
241
|
+
// count = 1 → "You have 1 item"
|
|
242
|
+
// count = 5 → "You have 5 items"
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
i18next reads the `count` arg and picks the `_one` or `_other` suffix per the English plural rules. The `0` arg drives substitution. The suffixes must be **lowercase** (`_one`, `_other`): i18next derives them from the browser's `Intl.PluralRules` categories, which are lowercase, and appends them verbatim to the key. A PascalCase `_One` label would never be found, and the label would render as its literal key name.
|
|
246
|
+
|
|
247
|
+
Languages with more plural forms (Russian, Polish, Arabic) need more label variants, `_zero`, `_few`, `_many`, etc. Author them as separate Custom Labels with the appropriate lowercase suffixes.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Why positional (`{0}`) instead of named (`{name}`)?
|
|
252
|
+
|
|
253
|
+
**Portability.** Salesforce has three UI frameworks:
|
|
254
|
+
- Apex (backend): `String.format(label, [arg0, arg1])`
|
|
255
|
+
- LWC: `@salesforce/label/c.Save_Failed` with `{0}/{1}`
|
|
256
|
+
- React UI Bundles: `t("Save_Failed", { 0: ..., 1: ... })`
|
|
257
|
+
|
|
258
|
+
All three use the same Custom Labels metadata. If React used `{name}` syntax, the same logical string would need to be authored **twice**, once for Apex/LWC with `{0}`, once for React with `{name}`. Translators would maintain two formats. Engineers couldn't reuse strings across frameworks.
|
|
259
|
+
|
|
260
|
+
With the bridge (`prefix: "{", suffix: "}"`), **one Custom Label serves all three**. Portability is the win.
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## What the bridge doesn't do (not in MVP)
|
|
265
|
+
|
|
266
|
+
- **Date/number formatting**: `{ 0: new Date() }` produces JS's default `toString`, not localized formatting. Wiring up `Intl.DateTimeFormat` / `Intl.NumberFormat` via i18next's `interpolation.format` callback is a separate design decision (not in the MVP scope).
|
|
267
|
+
- **Type safety**: nothing checks that `Save_Failed` actually has 2 placeholders at build time. A build-time extractor could close this gap (future work).
|
|
268
|
+
- **Escape for literal `{0}` in content**: i18next has escape options if labels genuinely need to contain literal curly braces. Edge case.
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## Visual flow
|
|
273
|
+
|
|
274
|
+
```text
|
|
275
|
+
┌─────────────────────────────────────────────────┐
|
|
276
|
+
│ 1. Custom Label (authored in Salesforce): │
|
|
277
|
+
│ "Failed to save {0}: {1}" │
|
|
278
|
+
└─────────────────────────────────────────────────┘
|
|
279
|
+
↓
|
|
280
|
+
GraphQL fetch at boot
|
|
281
|
+
↓
|
|
282
|
+
┌─────────────────────────────────────────────────┐
|
|
283
|
+
│ 2. Cached in i18next memory: │
|
|
284
|
+
│ { 'c:Save_Failed': "Failed to save {0}: {1}" }│
|
|
285
|
+
└─────────────────────────────────────────────────┘
|
|
286
|
+
↓
|
|
287
|
+
Component calls t('Save_Failed',
|
|
288
|
+
{ 0: 'Account',
|
|
289
|
+
1: 'Permission denied' })
|
|
290
|
+
↓
|
|
291
|
+
┌─────────────────────────────────────────────────┐
|
|
292
|
+
│ 3. i18next's interpolator does: │
|
|
293
|
+
│ template = "Failed to save {0}: {1}" │
|
|
294
|
+
│ template.replace('{0}', 'Account') │
|
|
295
|
+
│ .replace('{1}', 'Permission denied') │
|
|
296
|
+
└─────────────────────────────────────────────────┘
|
|
297
|
+
↓
|
|
298
|
+
┌─────────────────────────────────────────────────┐
|
|
299
|
+
│ 4. Output: │
|
|
300
|
+
│ "Failed to save Account: Permission denied" │
|
|
301
|
+
└─────────────────────────────────────────────────┘
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## Related
|
|
307
|
+
|
|
308
|
+
- [i18n-setup.md](i18n-setup.md): the init file where the `prefix`/`suffix` are configured
|
|
309
|
+
- [label-xml.md](label-xml.md): how to author labels with placeholders
|
|
310
|
+
- [verifying.md](verifying.md): testing interpolated labels
|
|
311
|
+
- [gotchas.md](gotchas.md): silent-fail traps
|