@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
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# i18n Setup: the two files you write (Angular / ngx-translate)
|
|
2
|
+
|
|
3
|
+
You write two files to set up i18n in an Angular UI Bundle: a **custom `TranslateLoader`** that
|
|
4
|
+
fetches Custom Labels over GraphQL, and the **label manifest**. The Platform SDK provides the
|
|
5
|
+
runtime plumbing (context fetch, the GraphQL surface); ngx-translate provides the pipe/service.
|
|
6
|
+
The shared engine — the Labels query, manifest format, batching, and fallback — is documented
|
|
7
|
+
once in [`../common/platform-sdk-i18n.md`](../common/platform-sdk-i18n.md); this file only covers
|
|
8
|
+
the Angular wiring.
|
|
9
|
+
|
|
10
|
+
> **Why a custom loader?** React drops the shipped `SalesforceBackend` straight into i18next.
|
|
11
|
+
> ngx-translate loads translations through a `TranslateLoader` instead, so you implement one
|
|
12
|
+
> that issues the **same** `uiapi.platform.labels` query. `SalesforceBackend` is i18next-shaped
|
|
13
|
+
> (it implements i18next's `read()` contract), so it isn't reused directly here.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## File 1: `src/i18n/salesforce-translate-loader.ts` (the label loader)
|
|
18
|
+
|
|
19
|
+
This is the only "glue" you write. `getTranslation(lang)` returns an `Observable` of a flat
|
|
20
|
+
`{ key: value }` map for that language — exactly what ngx-translate expects.
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { Injectable } from "@angular/core";
|
|
24
|
+
import type { TranslateLoader } from "@ngx-translate/core";
|
|
25
|
+
import { createDataSDK } from "@salesforce/platform-sdk";
|
|
26
|
+
import { from, Observable } from "rxjs";
|
|
27
|
+
import { labelManifest } from "./label-manifest";
|
|
28
|
+
|
|
29
|
+
const LABELS_QUERY = `
|
|
30
|
+
query Labels($ns: String!, $names: [String!]!, $locale: String, $fallback: LabelFallback) {
|
|
31
|
+
uiapi {
|
|
32
|
+
platform {
|
|
33
|
+
labels(namespace: $ns, names: $names, locale: $locale, fallback: $fallback) {
|
|
34
|
+
name
|
|
35
|
+
value
|
|
36
|
+
resolvedLocale
|
|
37
|
+
wasFallback
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}`;
|
|
42
|
+
|
|
43
|
+
const MAX_NAMES_PER_QUERY = 100; // uiapi.platform.labels rejects > 100 names per call
|
|
44
|
+
// B2E: keep "BASE_VALUE" (the shipped default). B2C: use "USER_DEFAULT" (see below).
|
|
45
|
+
const LABEL_FALLBACK = "BASE_VALUE";
|
|
46
|
+
|
|
47
|
+
@Injectable({ providedIn: "root" })
|
|
48
|
+
export class SalesforceTranslateLoader implements TranslateLoader {
|
|
49
|
+
getTranslation(lang: string): Observable<Record<string, string>> {
|
|
50
|
+
return from(this.loadLabels(lang));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private async loadLabels(lang: string): Promise<Record<string, string>> {
|
|
54
|
+
const dataSDK = await createDataSDK();
|
|
55
|
+
if (!dataSDK.graphql) throw new Error("Data SDK GraphQL surface unavailable");
|
|
56
|
+
// Hoist to a local const: TypeScript drops the `!dataSDK.graphql` narrowing inside the
|
|
57
|
+
// async batch closures below, so referencing `dataSDK.graphql.query` there re-widens to
|
|
58
|
+
// possibly-undefined (TS18048). The local const keeps the narrowed type.
|
|
59
|
+
const graphql = dataSDK.graphql;
|
|
60
|
+
|
|
61
|
+
// Group "namespace:Key" manifest entries by namespace.
|
|
62
|
+
const byNamespace = new Map<string, string[]>();
|
|
63
|
+
for (const entry of labelManifest) {
|
|
64
|
+
const idx = entry.indexOf(":");
|
|
65
|
+
const ns = idx === -1 ? "c" : entry.slice(0, idx);
|
|
66
|
+
const name = idx === -1 ? entry : entry.slice(idx + 1);
|
|
67
|
+
(byNamespace.get(ns) ?? byNamespace.set(ns, []).get(ns)!).push(name);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const result: Record<string, string> = {};
|
|
71
|
+
// Fetch each namespace, chunked into <=100-name batches, all in parallel.
|
|
72
|
+
await Promise.all(
|
|
73
|
+
[...byNamespace].flatMap(([ns, names]) => {
|
|
74
|
+
const unique = [...new Set(names)];
|
|
75
|
+
const batches: string[][] = [];
|
|
76
|
+
for (let i = 0; i < unique.length; i += MAX_NAMES_PER_QUERY) {
|
|
77
|
+
batches.push(unique.slice(i, i + MAX_NAMES_PER_QUERY));
|
|
78
|
+
}
|
|
79
|
+
return batches.map(async (batch) => {
|
|
80
|
+
const res = await graphql.query<any>({
|
|
81
|
+
operationName: "Labels",
|
|
82
|
+
query: LABELS_QUERY,
|
|
83
|
+
variables: { ns, names: batch, locale: lang, fallback: LABEL_FALLBACK },
|
|
84
|
+
});
|
|
85
|
+
const labels = res?.data?.uiapi?.platform?.labels ?? [];
|
|
86
|
+
for (const label of labels) {
|
|
87
|
+
if (label?.value != null) {
|
|
88
|
+
// Default "c" labels use the bare key; other namespaces keep the prefix.
|
|
89
|
+
const key = ns === "c" ? label.name : `${ns}:${label.name}`;
|
|
90
|
+
result[key] = label.value;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Notes:
|
|
102
|
+
- `dataSDK.graphql.query` **never rejects** — transport/GraphQL errors arrive on `res.errors`.
|
|
103
|
+
Inspect it if you want to surface a hard failure; the example simply treats missing data as
|
|
104
|
+
"no labels for this batch."
|
|
105
|
+
- ngx-translate has a single flat key space per language (no namespace concept). Default `c`
|
|
106
|
+
labels are keyed by bare name (`{{ 'Welcome_Text' | translate }}`); non-`c` namespaces keep
|
|
107
|
+
their prefix (`{{ 'LightningDatatable:sort' | translate }}`). Most bundles use only `c`.
|
|
108
|
+
- `createDataSDK()` is called here per language load. To avoid re-initializing it, wrap it in a
|
|
109
|
+
root-provided service and inject that into the loader and the boot initializer below.
|
|
110
|
+
|
|
111
|
+
### B2C variant
|
|
112
|
+
|
|
113
|
+
For a **B2C** bundle, set the fallback to follow the guest/site language context:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
const LABEL_FALLBACK = "USER_DEFAULT"; // B2C only — do NOT use for B2E
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Before using B2C wiring, have an org admin confirm `GraphQLApiOrgPrefForGuestUsers` is already
|
|
120
|
+
enabled. This workflow must never enable it. Without the preference, unauthenticated GraphQL
|
|
121
|
+
label requests return HTTP 403 (dependency W-23854208).
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## File 2: `src/i18n/label-manifest.ts` (the list of labels your app uses)
|
|
126
|
+
|
|
127
|
+
Identical format to every framework — a flat `"namespace:Key"` array (see
|
|
128
|
+
[`../common/platform-sdk-i18n.md`](../common/platform-sdk-i18n.md) and
|
|
129
|
+
[`../common/label-xml.md`](../common/label-xml.md)):
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
export const labelManifest = [
|
|
133
|
+
"c:Welcome_Text",
|
|
134
|
+
"c:Save_Button",
|
|
135
|
+
"c:Save_Failed_Message",
|
|
136
|
+
// one entry per label, "namespace:Key"
|
|
137
|
+
];
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
An **unregistered key fails silently**: it renders as its own literal name (e.g. `"Welcome_Text"`
|
|
141
|
+
instead of "Welcome") with no console warning. Keep the manifest in sync with your `translate`
|
|
142
|
+
call sites — `check-manifest-registered.sh --framework angular` verifies this.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Wiring: `src/app/app.config.ts`
|
|
147
|
+
|
|
148
|
+
Register ngx-translate with the custom loader in the standalone providers, and set the app's
|
|
149
|
+
`{0}/{1}` interpolation parser (see [interpolation.md](./interpolation.md)):
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
import { ApplicationConfig, provideAppInitializer, inject } from "@angular/core";
|
|
153
|
+
import {
|
|
154
|
+
provideTranslateService,
|
|
155
|
+
TranslateLoader,
|
|
156
|
+
TranslateService,
|
|
157
|
+
} from "@ngx-translate/core";
|
|
158
|
+
import { createDataSDK } from "@salesforce/platform-sdk";
|
|
159
|
+
import { fetchI18nContext } from "@salesforce/platform-sdk/i18n";
|
|
160
|
+
import { firstValueFrom } from "rxjs";
|
|
161
|
+
import { SalesforceTranslateLoader } from "../i18n/salesforce-translate-loader";
|
|
162
|
+
|
|
163
|
+
export const appConfig: ApplicationConfig = {
|
|
164
|
+
providers: [
|
|
165
|
+
// …your existing providers (provideRouter(routes), etc.)
|
|
166
|
+
provideTranslateService({
|
|
167
|
+
loader: { provide: TranslateLoader, useClass: SalesforceTranslateLoader },
|
|
168
|
+
// Fallback language when a key isn't loaded for the active language.
|
|
169
|
+
// v16+ renamed this option `defaultLanguage` → `fallbackLang`; older majors use
|
|
170
|
+
// `defaultLanguage`. Match your installed @ngx-translate/core.
|
|
171
|
+
fallbackLang: "en",
|
|
172
|
+
// parser: { provide: TranslateParser, useClass: PositionalParser }, // see interpolation.md
|
|
173
|
+
}),
|
|
174
|
+
|
|
175
|
+
// Boot: read the org locale context, set <html> dir/lang, load the active language,
|
|
176
|
+
// and block bootstrap until labels are ready.
|
|
177
|
+
provideAppInitializer(async () => {
|
|
178
|
+
const translate = inject(TranslateService);
|
|
179
|
+
const dataSDK = await createDataSDK();
|
|
180
|
+
const ctx = await fetchI18nContext(dataSDK);
|
|
181
|
+
|
|
182
|
+
// B2E: the session language is the display language.
|
|
183
|
+
document.documentElement.dir = ctx.dir;
|
|
184
|
+
document.documentElement.lang = ctx.lang;
|
|
185
|
+
|
|
186
|
+
await firstValueFrom(translate.use(ctx.lang)); // triggers the loader for that language
|
|
187
|
+
}),
|
|
188
|
+
],
|
|
189
|
+
};
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
`main.ts` stays as generated (`bootstrapApplication(App, appConfig)`); the app initializer runs
|
|
193
|
+
and resolves before the root component renders, so labels are present on first paint.
|
|
194
|
+
|
|
195
|
+
### B2C: display language from the site route
|
|
196
|
+
|
|
197
|
+
For B2C, drive the document language from the **route-selected** display language, not `ctx.dir`
|
|
198
|
+
— the GraphQL i18n context direction reflects the guest session profile and can stay `ltr` after
|
|
199
|
+
the site switches to an RTL language:
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
const resolvedLang =
|
|
203
|
+
(globalThis as { SFDC_ENV?: { language?: string } }).SFDC_ENV?.language || ctx.lang;
|
|
204
|
+
document.documentElement.lang = resolvedLang.replace(/_/g, "-");
|
|
205
|
+
await firstValueFrom(translate.use(resolvedLang));
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
A B2C language switcher must **navigate to the target language URL and perform a full page
|
|
209
|
+
reload** — changing `translate.use(...)` in place leaves the boot-time SDK context and cached
|
|
210
|
+
labels stale. Confirm the route and `SFDC_ENV.language` agree.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Dependencies
|
|
215
|
+
|
|
216
|
+
Install ngx-translate (Step 4 of the main workflow), choosing a `@ngx-translate/core` release
|
|
217
|
+
compatible with the bundle's Angular version (the standalone `provideTranslateService` API is
|
|
218
|
+
v16+; Angular 21 bundles use the current major):
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
npm install @ngx-translate/core
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Keep the `@salesforce/platform-sdk`, `@salesforce/ui-bundle`, and Angular build-plugin siblings
|
|
225
|
+
aligned at **≥11.49.3** (see [`../common/platform-sdk-i18n.md`](../common/platform-sdk-i18n.md)
|
|
226
|
+
for the version-floor rationale).
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## What you DON'T write
|
|
231
|
+
|
|
232
|
+
The Platform SDK ships the runtime engine — you do **not** re-implement locale detection or the
|
|
233
|
+
context fetch:
|
|
234
|
+
- `fetchI18nContext` / `reloadI18nContext`: language, locale, direction, currency, time zone
|
|
235
|
+
- `createDataSDK`: the GraphQL surface your loader queries
|
|
236
|
+
- `createI18nFormatters`: Intl date/number/currency formatters
|
|
237
|
+
|
|
238
|
+
You **do** write the `TranslateLoader` (the Angular analog of React's `SalesforceBackend`),
|
|
239
|
+
because ngx-translate loads through that contract. See
|
|
240
|
+
[`../common/platform-sdk-i18n.md`](../common/platform-sdk-i18n.md) for the query it issues.
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Related
|
|
245
|
+
|
|
246
|
+
- [../common/platform-sdk-i18n.md](../common/platform-sdk-i18n.md): the shared runtime engine
|
|
247
|
+
- [../common/label-xml.md](../common/label-xml.md): the Custom Labels metadata XML shape
|
|
248
|
+
- [interpolation.md](./interpolation.md): how `{0}/{1}` placeholders work with ngx-translate
|
|
249
|
+
- [../common/verifying.md](../common/verifying.md): the serve/verify flow
|
|
250
|
+
- [../common/gotchas.md](../common/gotchas.md): silent-fail traps to avoid
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Interpolation: `{0}`, `{1}` placeholders with ngx-translate
|
|
2
|
+
|
|
3
|
+
Salesforce Custom Labels use **positional interpolation**: `{0}`, `{1}`, `{N}` placeholders that
|
|
4
|
+
get replaced with runtime values. This is the same syntax used by Apex `String.format`, LWC
|
|
5
|
+
`@salesforce/label`, and the React path — so one label works across all of them. The
|
|
6
|
+
placeholder convention itself is framework-neutral (see
|
|
7
|
+
[`../common/label-xml.md`](../common/label-xml.md)); this file covers the **Angular render**.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## The gotcha: ngx-translate defaults to `{{param}}`, not `{0}`
|
|
12
|
+
|
|
13
|
+
Out of the box, ngx-translate interpolates **named** placeholders in double braces
|
|
14
|
+
(`{{name}}`). Salesforce labels use **positional single-brace** placeholders (`{0}`). To bridge
|
|
15
|
+
them, register a small custom `TranslateParser` that substitutes `{0}`, `{1}`, … This is the
|
|
16
|
+
Angular analog of React's i18next `interpolation: { prefix: "{", suffix: "}" }` config.
|
|
17
|
+
|
|
18
|
+
### `src/i18n/positional-translate-parser.ts`
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import { Injectable } from "@angular/core";
|
|
22
|
+
import { TranslateDefaultParser } from "@ngx-translate/core";
|
|
23
|
+
|
|
24
|
+
@Injectable()
|
|
25
|
+
export class PositionalTranslateParser extends TranslateDefaultParser {
|
|
26
|
+
override interpolate(
|
|
27
|
+
expr: string | ((...args: unknown[]) => string),
|
|
28
|
+
params?: Record<string, unknown>,
|
|
29
|
+
): string | undefined {
|
|
30
|
+
if (typeof expr !== "string") return super.interpolate(expr, params);
|
|
31
|
+
if (!params) return expr;
|
|
32
|
+
// Replace {0}, {1}, … with params["0"], params["1"], …; leave unmatched ones as-is.
|
|
33
|
+
return expr.replace(/\{(\d+)\}/g, (match, index) =>
|
|
34
|
+
params[index] != null ? String(params[index]) : match,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Register it in `provideTranslateService(...)` (see [i18n-setup.md](./i18n-setup.md)):
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { TranslateParser } from "@ngx-translate/core";
|
|
44
|
+
|
|
45
|
+
provideTranslateService({
|
|
46
|
+
loader: { provide: TranslateLoader, useClass: SalesforceTranslateLoader },
|
|
47
|
+
parser: { provide: TranslateParser, useClass: PositionalTranslateParser },
|
|
48
|
+
defaultLanguage: "en",
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Passing values
|
|
55
|
+
|
|
56
|
+
Params are an object keyed by the **placeholder number as a string** (`"0"`, `"1"`). A plain
|
|
57
|
+
`{ 0: … }` literal works — JS object keys are strings.
|
|
58
|
+
|
|
59
|
+
```html
|
|
60
|
+
<!-- Label: "Hello, {0}" -->
|
|
61
|
+
<h1>{{ 'Greeting' | translate:{ '0': userName } }}</h1>
|
|
62
|
+
<!-- → "Hello, Tosin" -->
|
|
63
|
+
|
|
64
|
+
<!-- Label: "Failed to save {0}: {1}" -->
|
|
65
|
+
<p>{{ 'Save_Failed' | translate:{ '0': objectName, '1': reason } }}</p>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// imperative form
|
|
70
|
+
this.translate.instant("Save_Failed", { 0: "Account", 1: "Permission denied" });
|
|
71
|
+
// → "Failed to save Account: Permission denied"
|
|
72
|
+
|
|
73
|
+
this.translate.get("Record_Count", { 0: 42, 1: 100 }).subscribe(/* … */);
|
|
74
|
+
// label "Showing {0} of {1} records" → "Showing 42 of 100 records"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Order in the object doesn't matter; the placeholder **number** binds. The same placeholder can
|
|
78
|
+
appear more than once (`"User {0} cannot edit {0}'s own profile"`) — every `{0}` is replaced.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Translations must preserve placeholders
|
|
83
|
+
|
|
84
|
+
The placeholders stay as `{0}`, `{1}` in every translation; they can move (grammar may reorder
|
|
85
|
+
them) but the **numbers must match**:
|
|
86
|
+
|
|
87
|
+
```xml
|
|
88
|
+
<!-- English --> <value>Failed to save {0}: {1}</value>
|
|
89
|
+
<!-- Spanish --> <label>Error al guardar {0}: {1}</label>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Numeric / date values are not locale-formatted
|
|
95
|
+
|
|
96
|
+
Interpolation is a string replace — `{ 0: 1234.5 }` renders JS's default `toString`, not
|
|
97
|
+
`1.234,5`. For locale-aware output, format first and pass the **formatted string**:
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
const formatted = new Intl.NumberFormat(ctx.locale).format(count);
|
|
101
|
+
this.translate.instant("Record_Count", { 0: formatted, 1: total });
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The Platform SDK's `createI18nFormatters(ctx)` gives you `formatDate` / `formatNumber` /
|
|
105
|
+
`formatCurrency` bound to `ctx.locale` (see [`../common/platform-sdk-i18n.md`](../common/platform-sdk-i18n.md)).
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Failure modes
|
|
110
|
+
|
|
111
|
+
- **Missing placeholder value** — `instant("Greeting", { 0: "Tosin" })` on label
|
|
112
|
+
`"Hello, {0} from {1}"` renders `"Hello, Tosin from {1}"` (the unmatched placeholder leaks).
|
|
113
|
+
No error; always pass every placeholder the label expects.
|
|
114
|
+
- **Missing label entirely** — `instant("Nonexistent_Key")` renders the literal key string.
|
|
115
|
+
That's the **unregistered manifest key** trap (see [`../common/gotchas.md`](../common/gotchas.md)):
|
|
116
|
+
the label was never fetched, so there's nothing to interpolate.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Pluralization
|
|
121
|
+
|
|
122
|
+
Unlike i18next, ngx-translate has **no built-in plural suffixing**. Two options:
|
|
123
|
+
|
|
124
|
+
1. **Separate labels + manual selection** (portable, matches the React label set): author
|
|
125
|
+
`Item_Count_one` / `Item_Count_other`, register both, and pick the key in code from
|
|
126
|
+
`new Intl.PluralRules(ctx.lang).select(count)`:
|
|
127
|
+
```typescript
|
|
128
|
+
const form = new Intl.PluralRules(ctx.lang).select(count); // "one" | "other" | …
|
|
129
|
+
this.translate.instant(`Item_Count_${form}`, { 0: count });
|
|
130
|
+
```
|
|
131
|
+
2. **ICU MessageFormat** via `@ngx-translate/message-format-compiler` — install it and register
|
|
132
|
+
its compiler in `provideTranslateService({ compiler: … })` if you want ICU `{count, plural, …}`
|
|
133
|
+
syntax inside label values. This changes the label authoring format, so coordinate with the
|
|
134
|
+
translation team.
|
|
135
|
+
|
|
136
|
+
Languages with more plural forms (Russian, Polish, Arabic) need the extra label variants
|
|
137
|
+
(`_zero`, `_few`, `_many`).
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Why positional (`{0}`) instead of named?
|
|
142
|
+
|
|
143
|
+
**Portability.** The same Custom Label metadata is consumed by Apex (`String.format`), LWC
|
|
144
|
+
(`@salesforce/label` with `{0}/{1}`), and both UI Bundle frameworks. Positional placeholders let
|
|
145
|
+
one authored/translated string serve all of them; a named format would fork the label per
|
|
146
|
+
framework and per translator. The custom parser above is what lets Angular join that convention.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Related
|
|
151
|
+
|
|
152
|
+
- [../common/label-xml.md](../common/label-xml.md): authoring labels with placeholders
|
|
153
|
+
- [i18n-setup.md](./i18n-setup.md): where the parser is registered
|
|
154
|
+
- [../common/platform-sdk-i18n.md](../common/platform-sdk-i18n.md): the shared runtime engine
|
|
155
|
+
- [../common/verifying.md](../common/verifying.md): testing interpolated labels
|
|
156
|
+
- [../common/gotchas.md](../common/gotchas.md): silent-fail traps
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Angular reference — Localize an Angular UI Bundle
|
|
2
|
+
|
|
3
|
+
Framework-specific companion to `SKILL.md` for the **Angular** path. `SKILL.md` owns the
|
|
4
|
+
neutral workflow + guardrail spine (Step 0 routing, preconditions, the five steps, and the
|
|
5
|
+
guardrails); this file owns everything Angular/ngx-translate-specific: the runtime library, the
|
|
6
|
+
call convention, the wiring shape, and the framework depth doc. The shared engine and the
|
|
7
|
+
framework-neutral depth docs live in [`../common/`](../common/).
|
|
8
|
+
|
|
9
|
+
## Library & call convention
|
|
10
|
+
|
|
11
|
+
An Angular UI Bundle can't use `@salesforce/label/*` the way LWC does — those imports resolve at
|
|
12
|
+
compile time inside the platform's compiler, which your standalone Angular bundle doesn't go
|
|
13
|
+
through. Instead the app **fetches labels at runtime** through the Salesforce GraphQL UI API and
|
|
14
|
+
hands them to **[ngx-translate](https://ngx-translate.org/)** (`@ngx-translate/core`) to render.
|
|
15
|
+
|
|
16
|
+
The engine that does the fetching is the same one React uses — see
|
|
17
|
+
[`../common/platform-sdk-i18n.md`](../common/platform-sdk-i18n.md). What's Angular-specific is
|
|
18
|
+
that ngx-translate loads translations through a **`TranslateLoader`**, so instead of React's
|
|
19
|
+
shipped `SalesforceBackend` you write a small custom loader that issues the same
|
|
20
|
+
`uiapi.platform.labels` GraphQL query.
|
|
21
|
+
|
|
22
|
+
```html
|
|
23
|
+
<!-- template: pipe form (most common) -->
|
|
24
|
+
<h1>{{ 'Welcome_Text' | translate }}</h1>
|
|
25
|
+
|
|
26
|
+
<!-- template: directive form -->
|
|
27
|
+
<h1 [translate]="'Welcome_Text'"></h1>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
// component/service: imperative form
|
|
32
|
+
constructor(private translate: TranslateService) {}
|
|
33
|
+
label = this.translate.instant("Welcome_Text"); // sync (translations already loaded)
|
|
34
|
+
this.translate.get("Welcome_Text").subscribe(v => …); // async Observable
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- **Call sites:** the `translate` pipe / directive in templates, and
|
|
38
|
+
`TranslateService.instant / get / stream("Key")` in code. The key is the bare `<fullName>`
|
|
39
|
+
(the `c` namespace is the default).
|
|
40
|
+
- **Files scanned for user-facing strings / call sites:** `.html` (templates) and `.ts`
|
|
41
|
+
(inline templates + service calls).
|
|
42
|
+
- **To use `t`-style translation in a component:** import ngx-translate's `TranslatePipe` (and/or
|
|
43
|
+
`TranslateDirective`) into the standalone component's `imports`, and/or inject
|
|
44
|
+
`TranslateService`.
|
|
45
|
+
|
|
46
|
+
## Step 2 (Extract) — Angular specifics
|
|
47
|
+
|
|
48
|
+
Replace the template literal with the `translate` pipe and import `TranslatePipe` into the
|
|
49
|
+
component's `imports` array:
|
|
50
|
+
|
|
51
|
+
```html
|
|
52
|
+
<!-- Before: <h1>Welcome</h1> -->
|
|
53
|
+
<!-- After: <h1>{{ 'Welcome_Text' | translate }}</h1> -->
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { TranslatePipe } from "@ngx-translate/core";
|
|
58
|
+
|
|
59
|
+
@Component({
|
|
60
|
+
selector: "app-welcome",
|
|
61
|
+
standalone: true,
|
|
62
|
+
imports: [TranslatePipe], // ← add
|
|
63
|
+
templateUrl: "./welcome.html",
|
|
64
|
+
})
|
|
65
|
+
export class Welcome {}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
For strings built in TypeScript (dynamic messages, `aria-label` bindings), inject
|
|
69
|
+
`TranslateService` and call `instant(...)` / `get(...)`.
|
|
70
|
+
|
|
71
|
+
## Step 4 (Wire) — Angular specifics
|
|
72
|
+
|
|
73
|
+
Install ngx-translate (tell the user to run, from the UI bundle dir — pick a `@ngx-translate/core`
|
|
74
|
+
release compatible with the bundle's Angular version; the standalone `provideTranslateService`
|
|
75
|
+
API is v16+):
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npm install @ngx-translate/core
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Then scaffold the two files — the custom `SalesforceTranslateLoader` (`src/i18n/salesforce-translate-loader.ts`)
|
|
82
|
+
and the label manifest (`src/i18n/label-manifest.ts`) — register the loader via
|
|
83
|
+
`provideTranslateService(...)` in `src/app/app.config.ts`, and at boot fetch the i18n context and
|
|
84
|
+
call `TranslateService.use(lang)` before the app renders. Full loader code and the B2E vs B2C
|
|
85
|
+
configuration are in [i18n-setup.md](./i18n-setup.md).
|
|
86
|
+
|
|
87
|
+
## Scripts
|
|
88
|
+
|
|
89
|
+
Run them from the UI bundle dir (they scan `src/` relative to the current directory). The first
|
|
90
|
+
three are framework-neutral and live in the skill's shared `scripts/` folder; `check-i18n-wired.sh`
|
|
91
|
+
is Angular-specific (ngx-translate `provideTranslateService` / `TranslateLoader` detection) and
|
|
92
|
+
ships here in the Angular reference folder.
|
|
93
|
+
|
|
94
|
+
| Script | Purpose |
|
|
95
|
+
|--------|---------|
|
|
96
|
+
| [`check-org-api-version.sh`](../../scripts/check-org-api-version.sh) | Precondition 4 — org supports API v68.0+ |
|
|
97
|
+
| [`detect-bundle-type.sh`](../../scripts/detect-bundle-type.sh) | Precondition 5 — classify B2E / B2C / internal |
|
|
98
|
+
| [`check-manifest-registered.sh --framework angular`](../../scripts/check-manifest-registered.sh) | Step 3 — every `translate` key is registered in the manifest (`--framework angular` selects the `.html` pipe/directive + `.ts` service-call grammar) |
|
|
99
|
+
| [`check-i18n-wired.sh`](./check-i18n-wired.sh) | Step 4 — the loader + `provideTranslateService` are wired, called at boot, and the manifest reaches the loader |
|
|
100
|
+
|
|
101
|
+
## Depth docs
|
|
102
|
+
|
|
103
|
+
Framework-neutral (shared with React), in [`../common/`](../common/):
|
|
104
|
+
- [platform-sdk-i18n.md](../common/platform-sdk-i18n.md) — the shared runtime engine: the Labels GraphQL query, `fetchI18nContext`, the manifest format, batching, and fallback
|
|
105
|
+
- [label-xml.md](../common/label-xml.md) — Custom Labels and translation metadata XML shapes; the `namespace:Key` rules
|
|
106
|
+
- [verifying.md](../common/verifying.md) — serve URL, locale flip, and verifying labels render
|
|
107
|
+
- [gotchas.md](../common/gotchas.md) — the silent-fail traps: unregistered manifest keys, API-version bake-in, stale label cache
|
|
108
|
+
|
|
109
|
+
Angular-specific (this folder):
|
|
110
|
+
- [i18n-setup.md](./i18n-setup.md) — the two files you write: the custom `TranslateLoader` + the label manifest
|
|
111
|
+
- [interpolation.md](./interpolation.md) — positional `{0}/{1}` placeholder interpolation with ngx-translate
|