@salesforce/afv-skills 1.50.0 → 1.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/package.json +1 -1
  2. package/skills/experience-ui-bundle-deploy/SKILL.md +43 -2
  3. package/skills/experience-ui-bundle-deploy/references/config-scaffold.md +16 -3
  4. package/skills/experience-ui-bundle-deploy/references/logout-url.md +97 -0
  5. package/skills/experience-ui-bundle-deploy/scripts/set-logout-url.mjs +304 -0
  6. package/skills/experience-ui-bundle-localize/SKILL.md +107 -90
  7. package/skills/experience-ui-bundle-localize/references/angular/check-i18n-wired.sh +159 -0
  8. package/skills/experience-ui-bundle-localize/references/angular/i18n-setup.md +250 -0
  9. package/skills/experience-ui-bundle-localize/references/angular/interpolation.md +156 -0
  10. package/skills/experience-ui-bundle-localize/references/angular/localize.md +111 -0
  11. package/skills/experience-ui-bundle-localize/references/{gotchas.md → common/gotchas.md} +27 -43
  12. package/skills/experience-ui-bundle-localize/references/{label-xml.md → common/label-xml.md} +27 -17
  13. package/skills/experience-ui-bundle-localize/references/common/platform-sdk-i18n.md +169 -0
  14. package/skills/experience-ui-bundle-localize/references/{verifying.md → common/verifying.md} +26 -16
  15. package/skills/experience-ui-bundle-localize/{scripts → references/react}/check-i18n-wired.sh +8 -3
  16. package/skills/experience-ui-bundle-localize/references/{i18n-setup.md → react/i18n-setup.md} +48 -10
  17. package/skills/experience-ui-bundle-localize/references/{interpolation.md → react/interpolation.md} +4 -4
  18. package/skills/experience-ui-bundle-localize/references/react/localize.md +76 -0
  19. package/skills/experience-ui-bundle-localize/scripts/check-manifest-registered.sh +92 -24
  20. package/skills/experience-ui-bundle-localize/scripts/detect-framework.sh +73 -0
  21. package/skills/experience-ui-bundle-site-generate/SKILL.md +1 -1
  22. package/skills/field-service-data-capture-form-deployer-configure/SKILL.md +1 -1
  23. package/skills/field-service-data-capture-form-deployer-configure/references/flow-metadata-json.md +1 -1
  24. package/skills/field-service-data-capture-form-editor-configure/SKILL.md +1 -1
  25. package/skills/field-service-data-capture-reference-configure/SKILL.md +28 -15
  26. package/skills/field-service-foundation-setup-designer-get/SKILL.md +1 -1
  27. package/skills/field-service-mobile-branding-configure/SKILL.md +1 -1
  28. package/skills/field-service-objective-designer-configure/SKILL.md +381 -64
  29. package/skills/field-service-prework-brief-deployer-configure/SKILL.md +566 -3
  30. package/skills/field-service-scheduling-policy-designer-query/SKILL.md +1097 -57
  31. package/skills/field-service-sobject-create-configure/SKILL.md +222 -7
  32. package/skills/field-service-voice-to-form-configure/SKILL.md +11 -11
  33. package/skills/field-service-work-rule-designer-configure/SKILL.md +92 -107
  34. package/skills/service-digital-engagement-channel-configure/SKILL.md +20 -37
  35. package/skills/service-digital-engagement-channel-configure/assets/messaging_channel_template.xml +3 -2
  36. package/skills/service-digital-engagement-channel-configure/examples/asa_agent_channel.xml +4 -4
  37. package/skills/service-helpagent-coordinate/SKILL.md +37 -29
  38. package/skills/service-helpagent-coordinate/assets/help-agent-spec.md +33 -28
  39. package/skills/service-helpagent-coordinate/references/agent-script.md +4 -1
  40. package/skills/service-helpagent-coordinate/references/channel-voice.md +1 -1
  41. package/skills/service-helpagent-coordinate/references/channel-web-chat.md +10 -12
@@ -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