experience-ui-bundle-localize

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Localize a React UI Bundle

本地化React UI Bundle

Walk a developer through localizing a React UI Bundle: detect hardcoded user-facing strings, extract them into Salesforce Custom Labels, wire up i18next over the Platform SDK GraphQL backend, and verify labels render across locales.
This file is the workflow + guardrail spine. Depth lives in linked docs:
  • references/i18n-setup.md: the two files you write: the i18next init and the label manifest
  • references/label-xml.md: Custom Labels and translation metadata XML shapes; the
    namespace:Key
    rules
  • references/interpolation.md: positional
    {0}/{1}
    placeholder interpolation in labels
  • references/verifying.md: serve URL, locale flip, and verifying labels render
  • references/gotchas.md: the three silent-fail traps: unregistered manifest keys, API-version bake-in, stale label cache
引导开发者完成React UI Bundle的本地化流程:检测硬编码的用户可见字符串,将其提取到Salesforce Custom Labels中,通过Platform SDK GraphQL后端配置i18next,并验证标签在多语言环境下的渲染效果。
本文档是工作流与约束规则的核心框架,详细内容请参考链接文档:
  • references/i18n-setup.md:需编写的两个文件:i18next初始化文件和标签清单
  • references/label-xml.md:Custom Labels与翻译元数据XML结构;
    namespace:Key
    命名规则
  • references/interpolation.md:标签中的位置占位符
    {0}/{1}
    插值方法
  • references/verifying.md:服务URL、语言切换及标签渲染验证方法
  • references/gotchas.md:三个静默失败陷阱:未注册的清单键、API版本固化、标签缓存过期

The one-paragraph mental model

核心概念概述

A React UI Bundle can't use
@salesforce/label/*
the way LWC does, those imports resolve at compile time inside the platform's compiler, which your standalone React bundle doesn't go through. Instead, your app fetches labels at runtime through the Salesforce GraphQL UI API and hands them to i18next (a standard React i18n library) to render. The Platform SDK provides the runtime plumbing for this, a detector that reads the user's language, a backend that fetches labels over GraphQL, and a context fetch. You write two thin files: a short init that wires the SDK pieces into i18next, and a manifest listing which labels your app uses. The rest is authoring the labels themselves as Salesforce Custom Labels metadata.
typescript
import { useTranslation } from "react-i18next";

function WelcomeBanner() {
  const { t } = useTranslation("c"); // "c" = custom label namespace
  return <h1>{t("Welcome_Text")}</h1>; // renders "Welcome" or "Bienvenido" per user's language
}

React UI Bundle无法像LWC那样使用
@salesforce/label/*
,这类导入会在平台编译器的编译阶段解析,而独立的React Bundle不会经过该编译流程。取而代之的是,你的应用会在运行时通过Salesforce GraphQL UI API获取标签,并将其传递给i18next(一款标准的React国际化库)进行渲染。Platform SDK提供了实现此功能的运行时基础组件:检测用户语言的探测器、通过GraphQL获取标签的后端,以及上下文获取工具。你只需编写两个简单文件:一个将SDK组件接入i18next的短初始化文件,以及一个列出应用所用标签的清单。其余工作则是将标签本身创建为Salesforce Custom Labels元数据。
typescript
import { useTranslation } from "react-i18next";

function WelcomeBanner() {
  const { t } = useTranslation("c"); // "c" = 自定义标签命名空间
  return <h1>{t("Welcome_Text")}</h1>; // 根据用户语言渲染"Welcome"或"Bienvenido"
}

Step 0: Route the task

步骤0:任务路由

The task is…Go to
Bundle doesn't exist yetexperience-ui-bundle-frontend-generate skill
Deploying the app with its labelsexperience-ui-bundle-deploy skill
Localizing an existing bundleWorkflow below

任务类型跳转至
Bundle尚未创建experience-ui-bundle-frontend-generate技能
部署包含标签的应用experience-ui-bundle-deploy技能
本地化已有Bundle下方工作流

Preconditions: verify before editing

前置条件:编辑前验证

#RequirementVerifyIf missing
1It's a
uiBundles/*/src/
React project
Project structure matchesNot a UI Bundle → route to the correct skill
2
@salesforce/platform-sdk
installed (≥11.42.1)
package.json
in the UI bundle dir
Tell user to install it; cannot proceed
3You can identify where the app mountsRead the entry file (usually
src/index.tsx
)
No clear mount point → ask user to point it out
4Target org actually supports API v68.0+ (runtime label GraphQL for UI Bundles ships in Release 264)Run the runtime org-release check belowOrg's max API version is below v68.0 (Release 262 or older) → cannot proceed; retarget a Release 264+ org or upgrade the org
5The bundle is an authenticated app (B2E, or an in-core internal app), not a public siteRun the authenticated-app detection belowBundle is a site (B2C/B2B) app → localization is not yet supported for site bundles; stop and tell the user B2C support is planned for when B2C localization is ready
Runtime org-release check (precondition 4). The
platform.labels
GraphQL path that resolves labels at runtime for UI Bundles ships in Salesforce Release 264 (API v68.0 or higher). A
sourceApiVersion
in
sfdx-project.json
records what you declared, not what the org supports, so a newer CLI pointed at an older org can pass a static file check and then fail at runtime. Query the org's actual maximum API version before wiring anything:
bash
bash <skill-dir>/scripts/check-org-api-version.sh <org-alias-or-username>
Exit
0
→ the org supports v68.0+, proceed. Exit
1
→ the org is too old or unreachable; do not write i18n wiring or labels, report the version mismatch to the user and stop. (
sf api request rest
inside the script keeps authentication at the CLI transport layer, so no access token enters context.)
Authenticated-app detection (precondition 5). The bundle's type decides whether localization is supported, and it's decided by deterministic file and string checks. Pass the full path to the bundle dir; the script derives the metadata root from it, so the current directory does not matter:
bash
bash <skill-dir>/scripts/detect-bundle-type.sh <path-to-uiBundles/<name>/ dir>
Act on the exit code:
0
→ authenticated app (in-core internal or B2E), proceed;
1
→ site (B2C/B2B), localization is not yet supported for site bundles, stop and tell the user (B2C support is planned for when B2C localization is ready);
2
→ unbound or cannot auto-detect, ask the user to confirm the bundle is an authenticated app (B2E or in-core internal) and stop if they cannot.
If a precondition isn't met, stop: report the specific block to the user and record a plan item to return once it's resolved. Do not edit the bundle, in particular, never add i18n wiring or TODO markers to a site (B2C/B2B) bundle that precondition 5 gated off as unsupported.

序号要求验证方式缺失时处理
1项目为
uiBundles/*/src/
下的React项目
核对项目结构若不是UI Bundle → 跳转至对应技能
2已安装
@salesforce/platform-sdk
(版本≥11.42.1)
检查UI Bundle目录下的
package.json
告知用户安装该依赖;无法继续操作
3可确定应用挂载位置读取入口文件(通常为
src/index.tsx
无明确挂载点 → 请用户指出
4目标组织支持API v68.0+(UI Bundle运行时标签GraphQL功能在Release 264中发布)运行下方的组织版本检查脚本组织的最高API版本低于v68.0(Release 262或更早)→ 无法继续操作;重新定位到Release 264+的组织或升级现有组织
5Bundle为已认证应用(B2E或核心内部应用),而非公开站点运行下方的已认证应用检测脚本Bundle为站点(B2C/B2B)应用 → 站点Bundle暂不支持本地化;停止操作并告知用户B2C支持计划将在B2C本地化功能就绪后推出
运行时组织版本检查(前置条件4)。为UI Bundle在运行时解析标签的
platform.labels
GraphQL路径在Salesforce Release 264(API v68.0及以上)中发布。
sfdx-project.json
中的
sourceApiVersion
记录的是你声明的版本,而非组织实际支持的版本,因此较新的CLI指向较旧组织时,静态文件检查可能通过,但运行时会失败。在配置任何内容前,请查询组织的实际最高API版本:
bash
bash <skill-dir>/scripts/check-org-api-version.sh <组织别名或用户名>
返回码
0
→ 组织支持v68.0+,继续操作。返回码
1
→ 组织版本过旧或无法访问;请勿编写i18n配置或标签,向用户报告版本不匹配问题并停止操作。(脚本中的
sf api request rest
将认证保留在CLI传输层,因此不会将访问令牌带入上下文。)
已认证应用检测(前置条件5)。Bundle的类型决定了是否支持本地化,可通过确定性的文件和字符串检查来判断。传入Bundle目录的完整路径;脚本会从中推导元数据根目录,因此当前目录不影响结果:
bash
bash <skill-dir>/scripts/detect-bundle-type.sh <uiBundles/<名称>/目录的完整路径>
根据返回码操作:
0
→ 已认证应用(核心内部或B2E),继续操作;
1
→ 站点(B2C/B2B),站点Bundle暂不支持本地化,停止操作并告知用户(B2C支持计划将在B2C本地化功能就绪后推出);
2
→ 未绑定或无法自动检测,请用户确认该Bundle为已认证应用(B2E或核心内部应用),若用户无法确认则停止操作。
若前置条件未满足,请停止操作:向用户报告具体的阻塞问题,并记录待解决项以便后续处理。请勿修改Bundle,尤其注意:绝不为前置条件5判定为不支持的站点(B2C/B2B)Bundle添加i18n配置或TODO标记。

Workflow: the five steps

工作流:五个步骤

Each step has a checkable completion criterion and a confirm-before-continue pause.
每个步骤都有可检查的完成标准确认后再继续的暂停点。

Step 1: Detect

步骤1:检测

Goal: Scan
.tsx
/
.jsx
files for user-facing hardcoded strings.
What to scan:
  • String literals inside JSX tags:
    <h1>Welcome</h1>
    → candidate
  • String props shown to users:
    placeholder="Enter name"
    → candidate
  • User-facing accessible text:
    aria-label
    ,
    aria-describedby
    ,
    alt
    → candidate (a screen-reader user hears these, so they must localize too)
What to skip:
  • Import statements
  • Object keys / property names
  • data-*
    attributes (machine-readable)
  • Test IDs (
    data-testid
    ,
    id
    attributes)
  • Text already wrapped in
    t()
    calls
  • Console logs, error messages thrown to developers (not user-facing)
  • Class names, file paths, technical constants
Action:
  1. Scan the
    src/
    directory for
    .tsx
    and
    .jsx
    files
  2. Extract candidates, showing file path + line number for each
  3. Show the list to the developer
Completion criterion: Developer confirms the list (or edits it to remove false positives).
Pause: "I found N user-facing strings across M components. Here's the list: [show file:line + string]. Look right? [confirm / edit the list / skip some]"

目标:扫描
.tsx
/
.jsx
文件中的用户可见硬编码字符串。
扫描范围
  • JSX标签内的字符串字面量:
    <h1>Welcome</h1>
    → 候选字符串
  • 用户可见的字符串属性:
    placeholder="Enter name"
    → 候选字符串
  • 用户可见的无障碍文本:
    aria-label
    aria-describedby
    alt
    → 候选字符串(屏幕阅读器用户会听到这些内容,因此必须本地化)
跳过范围
  • 导入语句
  • 对象键/属性名
  • data-*
    属性(机器可读)
  • 测试ID(
    data-testid
    id
    属性)
  • 已包裹在
    t()
    调用中的文本
  • 控制台日志、抛出给开发者的错误信息(非用户可见)
  • 类名、文件路径、技术常量
操作
  1. 扫描
    src/
    目录下的
    .tsx
    .jsx
    文件
  2. 提取候选字符串,显示每个字符串的文件路径+行号
  3. 将列表展示给开发者
完成标准: 开发者确认列表正确(或编辑列表以移除误报项)。
暂停点:"我在M个组件中找到了N个用户可见字符串。列表如下:[显示文件:行号 + 字符串]。是否正确?[确认/编辑列表/跳过部分内容]"

Step 2: Extract

步骤2:提取

Goal: For each confirmed string, add a Custom Label and replace the JSX literal with a
t()
call.
Action for each string:
  1. Propose a key name, format:
    <Context>_<Role>
    (e.g.,
    "Welcome"
    Welcome_Text
    ,
    "Save"
    Save_Button
    ,
    "Failed to save"
    Save_Failed_Message
    ). Follow naming: PascalCase words, underscores between parts, descriptive enough to be unique.
  2. Add the label to
    force-app/main/default/labels/CustomLabels.labels-meta.xml
    :
    xml
    <labels>
      <fullName>Welcome_Text</fullName>
      <language>en_US</language>
      <protected>false</protected>
      <shortDescription>Welcome banner heading</shortDescription>
      <value>Welcome</value>
    </labels>
    (Full XML structure: references/label-xml.md)
  3. Replace the string in the component with
    {t("Key")}
    :
    tsx
    // Before: <h1>Welcome</h1>
    // After:  <h1>{t("Welcome_Text")}</h1>
  4. Add the import if not present:
    import { useTranslation } from "react-i18next";
    and
    const { t } = useTranslation("c");
    at the top of the component function.
Completion criterion: Every confirmed string has both a
CustomLabels
entry and a
t()
call in its original location.
Pause: "For each string I'll add a Custom Label and replace the JSX with t(). Here are the proposed keys: [show string → namespace:Key mapping]. Apply these edits? [y / review each]"

目标:为每个确认的字符串添加Custom Label,并将JSX字面量替换为
t()
调用。
每个字符串的操作
  1. 建议键名,格式:
    <上下文>_<角色>
    (例如:
    "Welcome"
    Welcome_Text
    "Save"
    Save_Button
    "Failed to save"
    Save_Failed_Message
    )。遵循命名规则:单词首字母大写,各部分用下划线分隔,描述性强且唯一。
  2. 将标签添加
    force-app/main/default/labels/CustomLabels.labels-meta.xml
    xml
    <labels>
      <fullName>Welcome_Text</fullName>
      <language>en_US</language>
      <protected>false</protected>
      <shortDescription>欢迎横幅标题</shortDescription>
      <value>Welcome</value>
    </labels>
    (完整XML结构:references/label-xml.md
  3. 替换组件中的字符串
    {t("Key")}
    tsx
    // 替换前: <h1>Welcome</h1>
    // 替换后:  <h1>{t("Welcome_Text")}</h1>
  4. 添加导入语句(若未存在):在组件函数顶部添加
    import { useTranslation } from "react-i18next";
    const { t } = useTranslation("c");
完成标准: 每个确认的字符串都在CustomLabels中有对应条目,且在原位置替换为
t()
调用。
暂停点:"我将为每个字符串添加Custom Label并将JSX替换为t()调用。以下是建议的键名:[显示字符串 → 命名空间:Key映射]。是否应用这些修改?[是/逐个审核]"

Step 3: Register

步骤3:注册

Goal: Add each key to the label manifest so i18next knows to fetch it.
Action:
  1. Add each key to the manifest array in
    src/i18n/label-manifest.ts
    :
    typescript
    export const labelManifest = [
      "c:Welcome_Text",
      "c:Save_Button",
      "c:Save_Failed_Message",
    ];
    If the file doesn't exist yet, Step 4 scaffolds it; the completion check below reports its absence, so don't test for the file by hand.
Completion criterion: Run
check-manifest-registered.sh
from the UI bundle dir (it scans
src/
relative to the current directory) and report any errors it returns. It owns the deterministic inspection: it cross-checks every
t("Key")
call site against the manifest and treats a missing
label-manifest.ts
(when
t()
calls exist) as a failure. A key that's called but not registered renders as its own literal name at runtime with no error, the silent-fail trap this guards.
bash
cd <path-to-uiBundles/<name>/ dir>   # scripts scan src/ relative to here
bash <skill-dir>/scripts/check-manifest-registered.sh
Branch on the exit code:
0
, every key is registered (or there are no
t()
calls to gate), proceed.
1
, the manifest is missing or the listed keys aren't in it; scaffold or add them (Step 4 scaffolds the file) and re-run.
64
, usage error, the source dir doesn't exist (wrong cwd or bad argument); this is not a "keys missing" result, do not scaffold or register, fix the path and re-run.
Pause: "Added N entries to label-manifest.ts. check-manifest-registered.sh passed: [confirm]."

目标:将每个键添加到标签清单中,以便i18next知道需要获取该标签。
操作
  1. 将每个键添加到
    src/i18n/label-manifest.ts
    的清单数组中:
    typescript
    export const labelManifest = [
      "c:Welcome_Text",
      "c:Save_Button",
      "c:Save_Failed_Message",
    ];
    若该文件尚未存在,步骤4会自动生成;下方的完成检查会报告文件缺失,因此无需手动检查文件是否存在。
完成标准: 在UI Bundle目录下运行
check-manifest-registered.sh
(脚本会扫描当前目录下的
src/
),并报告返回的任何错误。该脚本负责确定性检查:它会将每个
t("Key")
调用位置与清单进行交叉核对,若存在
t()
调用但
label-manifest.ts
缺失,则判定为失败。未注册的键在运行时会直接渲染为键名本身且无任何错误提示,这正是该脚本要防范的静默失败陷阱。
bash
cd <uiBundles/<名称>/目录路径>   # 脚本会扫描当前目录下的src/
bash <skill-dir>/scripts/check-manifest-registered.sh
根据返回码分支处理:
0
→ 所有键均已注册(或无
t()
调用需要检查),继续操作。
1
→ 清单缺失或列出的键未包含在其中;生成文件或添加键(步骤4会生成文件)后重新运行。
64
→ 使用错误,源目录不存在(当前目录错误或参数无效);这不是"键缺失"的结果,请勿生成文件或注册键,修正路径后重新运行。
暂停点:"已向label-manifest.ts添加N个条目。check-manifest-registered.sh执行通过:[确认]"

Step 4: Wire

步骤4:配置

Goal: Ensure the i18next init exists; scaffold it if the app has no i18n yet.
Check: Run
check-i18n-wired.sh
from the UI bundle dir (it scans
src/
relative to the current directory) and report what it returns. The script owns the whole deterministic inspection: it looks for an init file defining
initI18n()
and a boot-time call to it, and when those exist it also reports whether the label manifest is imported and actually passed into the backend config. Do not re-derive any of this by reading files yourself.
bash
cd <path-to-uiBundles/<name>/ dir>   # scripts scan src/ relative to here
bash <skill-dir>/scripts/check-i18n-wired.sh
Branch on the exit code (the printed message names the specific file/symbol for your report, but the decision is the code):
  • Exit
    0
    → fully wired, the manifest is passed into the backend; go to "If i18n already exists" below and just add new keys.
  • Exit
    1
    → no
    initI18n()
    exists; scaffold the whole setup via "If no i18n setup exists yet".
  • Exit
    2
    → the init already exists but isn't called at boot; do not re-scaffold or overwrite it. Add only the boot-time
    initI18n()
    call in the entry file (step 4 of "If no i18n setup exists yet"), then re-run.
  • Exit
    3
    → wired at boot but the script could not confirm the manifest is passed into the backend. It scans the whole
    src
    tree, but this last check is a textual heuristic: the manifest may be wired through a variable, spread, or helper the script can't see, so treat exit 3 as "verify before editing," not "definitely broken." Open the file the message names and confirm the manifest really isn't in
    backendOptions
    . Only if it genuinely dangles, do what the message names: if the manifest is imported but unused, pass it into the existing
    backendOptions
    without clobbering it; if there's no
    backendOptions
    /
    SalesforceBackend
    config at all, add that backend block to the existing init (see references/i18n-setup.md). Never re-scaffold the init file or duplicate wiring that already works.
  • Exit
    64
    → usage error: the source dir doesn't exist (wrong cwd or bad argument). This is not a "no init" result; do not scaffold. Fix the path (run from the UI bundle dir, or pass its
    src
    path) and re-run.
If no i18n setup exists yet:
  1. Install dependencies (tell the user to run):
    bash
    npm install i18next react-i18next i18next-chained-backend i18next-localstorage-backend
  2. Create
    src/i18n/index.ts
    with the init wiring (full code: references/i18n-setup.md)
  3. Create
    src/i18n/label-manifest.ts
    with an empty array (Step 3 will populate it)
  4. Call
    initI18n()
    once at boot in the entry file (before mounting the app):
    typescript
    import { initI18n } from "./i18n";
    
    initI18n().then(() => {
      // mount app
    });
If i18n already exists: Act on the message
check-i18n-wired.sh
already printed (above): if it reports the manifest wired, just add new keys to it; if it reports a reconcile is needed, do exactly what its message names (import the manifest and/or pass it into the backend config) without clobbering existing wiring.
Completion criterion:
initI18n()
exists and is called once at boot; the manifest is wired into the backend.
Pause: "i18next setup [exists / created]. initI18n() is called at boot: [confirm]."

目标:确保i18next初始化文件存在;若应用尚未配置i18n,则自动生成相关文件。
检查: 在UI Bundle目录下运行
check-i18n-wired.sh
(脚本会扫描当前目录下的
src/
),并报告返回结果。该脚本负责完整的确定性检查:它会查找定义了
initI18n()
的初始化文件及启动时对该函数的调用,若这些均存在,还会检查标签清单是否已导入并传入后端配置。请勿手动读取文件推导这些信息。
bash
cd <uiBundles/<名称>/目录路径>   # 脚本会扫描当前目录下的src/
bash <skill-dir>/scripts/check-i18n-wired.sh
根据返回码分支处理(打印消息会指明具体文件/符号供你报告,但决策基于返回码):
  • 返回码
    0
    → 配置完成,清单已传入后端;跳至下方"若已存在i18n配置"部分,仅添加新键即可。
  • 返回码
    1
    → 不存在
    initI18n()
    ;通过"若尚未配置i18n"部分生成完整配置。
  • 返回码
    2
    → 初始化文件已存在但未在启动时调用;请勿重新生成或覆盖该文件。仅在入口文件中添加启动时的
    initI18n()
    调用("若尚未配置i18n"部分的步骤4),然后重新运行脚本。
  • 返回码
    3
    → 已在启动时配置,但脚本无法确认清单已传入后端。脚本会扫描整个
    src
    目录,但此最终检查为文本启发式检查:清单可能通过变量、扩展运算符或脚本无法识别的辅助函数进行配置,因此将返回码3视为"编辑前验证",而非"确定已损坏"。打开消息指明的文件,确认清单是否真的未在
    backendOptions
    中。若确实未配置,则按照消息提示操作:若已导入清单但未使用,将其传入现有
    backendOptions
    (请勿覆盖现有配置);若完全没有
    backendOptions
    /
    SalesforceBackend
    配置,则将该后端块添加到现有初始化文件中(参考references/i18n-setup.md)。绝不要重新生成初始化文件或重复已有的配置。
  • 返回码
    64
    → 使用错误:源目录不存在(当前目录错误或参数无效)。这不是"无初始化文件"的结果;请勿生成文件。修正路径(在UI Bundle目录下运行,或传入其
    src
    路径)后重新运行。
若尚未配置i18n
  1. 安装依赖(告知用户运行):
    bash
    npm install i18next react-i18next i18next-chained-backend i18next-localstorage-backend
  2. 创建
    src/i18n/index.ts
    并添加初始化配置(完整代码:references/i18n-setup.md
  3. 创建
    src/i18n/label-manifest.ts
    并添加空数组(步骤3会填充该数组)
  4. 在入口文件的启动阶段调用一次
    initI18n()
    (在挂载应用之前):
    typescript
    import { initI18n } from "./i18n";
    
    initI18n().then(() => {
      // 挂载应用
    });
若已存在i18n配置: 根据
check-i18n-wired.sh
打印的消息操作(如上):若报告清单已配置,仅需向清单添加新键;若报告需要协调配置,则严格按照消息提示操作(导入清单和/或传入后端配置),请勿覆盖现有配置。
完成标准
initI18n()
存在且在启动时调用一次;清单已接入后端配置。
暂停点:"i18next配置[已存在/已创建]。initI18n()已在启动时调用:[确认]"

Step 5: Verify

步骤5:验证

Goal: Guide the developer to verify labels render in a second language.
Action:
  1. Activate a second language (if not already active), tell the user: "In your org, go to Setup → Translation Workbench → Translation Settings → add a language (e.g., Spanish)."
  2. Author a translation, scaffold an empty translation file for the language:
    xml
    <!-- force-app/main/default/translations/es.translation-meta.xml -->
    <Translations xmlns="http://soap.sforce.com/2006/04/metadata">
      <customLabels>
        <label>Bienvenido</label>
        <name>Welcome_Text</name>
      </customLabels>
    </Translations>
    (Full structure: references/label-xml.md)
    Tell the user to either:
    • Edit the XML file by hand (for a small number of labels), or
    • Use Translation Workbench (Setup → Translate → Custom Label → pick language → enter translations), then retrieve with
      sf project retrieve start --metadata Translations:es
      .
  3. Build and deploy, tell the user:
    bash
    sf config set target-org=<alias>  # API version bakes in; point at the deploy target first
    npm run build
    sf project deploy start --source-dir force-app --target-org <alias>
  4. Open the app at the
    /lwr/application/ai/<namespace>-<bundleName>
    URL on the
    lightning.force.com
    domain (redirects to the app host).
  5. Change the user's Language (not Locale), Setup → My Settings → Language & Time Zone → Language → pick the translated language → Save.
  6. Reload the app, labels should flip to the translated language.
If it doesn't render: Check the three gotchas in references/gotchas.md:
  • Unregistered manifest key (Step 3 missed a label)
  • API-version mismatch (built against a different org)
  • Stale localStorage cache (clear
    i18next_res_*
    keys in DevTools)
Completion criterion: Labels render in ≥2 locales, or the blocking gotcha is identified.
Pause: "To verify: activate a second language in Translation Workbench, author a translation (I can scaffold the XML), build/deploy, and reload. Want me to scaffold the translation file for [language]? [y / I'll do it manually]"

目标:引导开发者验证标签在第二种语言下的渲染效果。
操作
  1. 激活第二种语言(若尚未激活),告知用户:"在你的组织中,进入Setup → Translation Workbench → Translation Settings → 添加一种语言(例如西班牙语)。"
  2. 编写翻译内容,为该语言生成空的翻译文件:
    xml
    <!-- force-app/main/default/translations/es.translation-meta.xml -->
    <Translations xmlns="http://soap.sforce.com/2006/04/metadata">
      <customLabels>
        <label>Bienvenido</label>
        <name>Welcome_Text</name>
      </customLabels>
    </Translations>
    (完整结构:references/label-xml.md
    告知用户可选择以下两种方式之一:
    • 手动编辑XML文件(适用于少量标签),或
    • 使用Translation Workbench(Setup → Translate → Custom Label → 选择语言 → 输入翻译内容),然后通过
      sf project retrieve start --metadata Translations:es
      获取翻译文件。
  3. 构建并部署,告知用户:
    bash
    sf config set target-org=<别名>  # API版本会固化;请先指向部署目标
    npm run build
    sf project deploy start --source-dir force-app --target-org <别名>
  4. 打开应用,访问
    lightning.force.com
    域名下的
    /lwr/application/ai/<命名空间>-<bundleName>
    URL(会重定向到应用主机)。
  5. 更改用户语言(注意不是区域设置),进入Setup → My Settings → Language & Time Zone → Language → 选择已翻译的语言 → Save。
  6. 重新加载应用,标签应切换为已翻译的语言。
若标签未渲染: 检查references/gotchas.md中的三个陷阱:
  • 未注册的清单键(步骤3遗漏了某个标签)
  • API版本不匹配(针对不同组织构建)
  • localStorage缓存过期(在开发者工具中清除
    i18next_res_*
    键)
完成标准: 标签在≥2种语言环境下正常渲染,或已识别出阻塞问题。
暂停点:"验证步骤:在Translation Workbench中激活第二种语言,编写翻译内容(我可以生成XML模板),构建/部署并重新加载应用。是否需要我为[语言]生成翻译文件模板?[是/我手动完成]"

Edge cases: handle gracefully

边缘情况:优雅处理

  • Already-localized code: detect existing
    t()
    usage / a populated manifest; offer to add to the setup rather than re-scaffold everything.
  • No strings found: report cleanly and stop; do not invent work.
  • App has no i18n setup yet: Step 4 scaffolds the two files first before Step 3 can register anything.
  • Partial setup (manifest exists but init missing, or vice-versa), reconcile what's present; never clobber existing wiring.

  • 已本地化代码:检测现有的
    t()
    用法/已填充的清单;提供"添加到现有配置"的选项,而非重新生成所有内容。
  • 未找到字符串:清晰报告并停止操作;请勿凭空创建工作任务。
  • 应用尚未配置i18n:步骤4先生成两个文件,之后步骤3才能注册键。
  • 部分配置(清单存在但初始化文件缺失,或反之):协调现有配置;绝不要覆盖已有的配置。

Guardrails: never regress these

约束规则:绝不能违反

  1. Never machine-translate into deployable metadata. Scaffold empty translation files and guide the developer to author translations (by hand or via Translation Workbench). Do not call any MT API and paste the result into
    translation-meta.xml
    ; unreviewed machine translations are a quality liability.
  2. Never register a key that has no label. Manifest entry count must equal label count (Step 3 criterion). An unregistered key renders as its own literal name with no console warning. It's the most common localization bug.
  3. Never clobber existing i18n wiring. If Step 4 finds an existing
    initI18n()
    , reconcile (add the manifest import if missing) rather than replace the whole file.
  4. Every file must be customer-safe. No
    webapps
    , core-only paths, or internal infrastructure references anywhere. Write as if for an external customer in an SFDX project.

  1. 绝不自动翻译可部署的元数据。生成空的翻译文件并引导开发者编写翻译内容(手动或通过Translation Workbench)。请勿调用任何机器翻译API并将结果粘贴到
    translation-meta.xml
    中;未经审核的机器翻译会带来质量风险。
  2. 绝不注册无对应标签的键。清单条目数量必须与标签数量相等(步骤3标准)。未注册的键会直接渲染为键名本身且无控制台警告,这是最常见的本地化bug。
  3. 绝不覆盖已有的i18n配置。若步骤4发现已存在
    initI18n()
    ,则协调配置(若缺失则添加清单导入),而非替换整个文件。
  4. 所有文件必须对客户安全。不得包含
    webapps
    、核心专属路径或内部基础设施引用。编写内容时假设面向外部客户的SFDX项目。

Commands & layout

命令与目录结构

text
<project-root>/                          ← SFDX project root
└── force-app/main/default/
    ├── labels/CustomLabels.labels-meta.xml          ← English base labels
    ├── translations/<locale>.translation-meta.xml   ← one per translated language
    └── uiBundles/<your-bundle>/
        ├── package.json
        └── src/
            ├── i18n/
            │   ├── index.ts              ← init wiring (you write this once)
            │   └── label-manifest.ts     ← list of labels to fetch (you maintain this)
            └── components/               ← components call t()
CommandRun fromPurpose
npm install i18next react-i18next i18next-chained-backend i18next-localstorage-backend
UI bundle dirInstall i18n dependencies (Step 4)
npm run build
UI bundle dirBuild the app (API version bakes in, set target-org first)
sf project deploy start --source-dir force-app
Project rootDeploy the app + labels + translations
sf project retrieve start --metadata Translations:<locale>
Project rootPull translations authored in Translation Workbench

text
<项目根目录>/                          ← SFDX项目根目录
└── force-app/main/default/
    ├── labels/CustomLabels.labels-meta.xml          ← 英文基础标签
    ├── translations/<语言代码>.translation-meta.xml   ← 每种翻译语言对应一个文件
    └── uiBundles/<你的Bundle>/
        ├── package.json
        └── src/
            ├── i18n/
            │   ├── index.ts              ← 初始化配置(编写一次)
            │   └── label-manifest.ts     ← 需获取的标签列表(维护此文件)
            └── components/               ← 组件中调用t()
命令运行目录用途
npm install i18next react-i18next i18next-chained-backend i18next-localstorage-backend
UI Bundle目录安装i18n依赖(步骤4)
npm run build
UI Bundle目录构建应用(API版本会固化,请先设置target-org)
sf project deploy start --source-dir force-app
项目根目录部署应用+标签+翻译内容
sf project retrieve start --metadata Translations:<语言代码>
项目根目录获取在Translation Workbench中编写的翻译内容

Pre-flight checklist: completion criteria for the whole run

预检查清单:整个流程的完成标准

  • Every confirmed string has both a
    CustomLabels
    entry and a
    t()
    call
  • label-manifest.ts
    entry count == label count (no unregistered keys)
  • initI18n()
    present and called once at boot
  • Labels render in ≥2 locales (or the blocking gotcha is named)
  • No hand-written machine translations landed in
    *-meta.xml
    (only scaffold-and-guide)
  • 每个确认的字符串都有对应的CustomLabels条目和
    t()
    调用
  • label-manifest.ts
    条目数量 == 标签数量(无未注册的键)
  • initI18n()
    存在且在启动时调用一次
  • 标签在≥2种语言环境下正常渲染(或已识别出阻塞问题)
  • *-meta.xml
    中无手动添加的机器翻译内容(仅生成模板并引导用户编写) ",