experience-ui-bundle-localize
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseLocalize 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 rules
namespace:Key - references/interpolation.md: positional placeholder interpolation in labels
{0}/{1} - 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 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.
@salesforce/label/*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那样使用,这类导入会在平台编译器的编译阶段解析,而独立的React Bundle不会经过该编译流程。取而代之的是,你的应用会在运行时通过Salesforce GraphQL UI API获取标签,并将其传递给i18next(一款标准的React国际化库)进行渲染。Platform SDK提供了实现此功能的运行时基础组件:检测用户语言的探测器、通过GraphQL获取标签的后端,以及上下文获取工具。你只需编写两个简单文件:一个将SDK组件接入i18next的短初始化文件,以及一个列出应用所用标签的清单。其余工作则是将标签本身创建为Salesforce Custom Labels元数据。
@salesforce/label/*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 yet | experience-ui-bundle-frontend-generate skill |
| Deploying the app with its labels | experience-ui-bundle-deploy skill |
| Localizing an existing bundle | Workflow below |
| 任务类型 | 跳转至 |
|---|---|
| Bundle尚未创建 | experience-ui-bundle-frontend-generate技能 |
| 部署包含标签的应用 | experience-ui-bundle-deploy技能 |
| 本地化已有Bundle | 下方工作流 |
Preconditions: verify before editing
前置条件:编辑前验证
| # | Requirement | Verify | If missing |
|---|---|---|---|
| 1 | It's a | Project structure matches | Not a UI Bundle → route to the correct skill |
| 2 | | | Tell user to install it; cannot proceed |
| 3 | You can identify where the app mounts | Read the entry file (usually | No clear mount point → ask user to point it out |
| 4 | Target org actually supports API v68.0+ (runtime label GraphQL for UI Bundles ships in Release 264) | Run the runtime org-release check below | Org's max API version is below v68.0 (Release 262 or older) → cannot proceed; retarget a Release 264+ org or upgrade the org |
| 5 | The bundle is an authenticated app (B2E, or an in-core internal app), not a public site | Run the authenticated-app detection below | Bundle 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 GraphQL path that resolves labels at runtime for UI Bundles ships in Salesforce Release 264 (API v68.0 or higher). A in 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:
platform.labelssourceApiVersionsfdx-project.jsonbash
bash <skill-dir>/scripts/check-org-api-version.sh <org-alias-or-username>Exit → the org supports v68.0+, proceed. Exit → the org is too old or unreachable; do not write i18n wiring or labels, report the version mismatch to the user and stop. ( inside the script keeps authentication at the CLI transport layer, so no access token enters context.)
01sf api request restAuthenticated-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: → authenticated app (in-core internal or B2E), proceed; → 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); → 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.
012If 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 | 项目为 | 核对项目结构 | 若不是UI Bundle → 跳转至对应技能 |
| 2 | 已安装 | 检查UI Bundle目录下的 | 告知用户安装该依赖;无法继续操作 |
| 3 | 可确定应用挂载位置 | 读取入口文件(通常为 | 无明确挂载点 → 请用户指出 |
| 4 | 目标组织支持API v68.0+(UI Bundle运行时标签GraphQL功能在Release 264中发布) | 运行下方的组织版本检查脚本 | 组织的最高API版本低于v68.0(Release 262或更早)→ 无法继续操作;重新定位到Release 264+的组织或升级现有组织 |
| 5 | Bundle为已认证应用(B2E或核心内部应用),而非公开站点 | 运行下方的已认证应用检测脚本 | Bundle为站点(B2C/B2B)应用 → 站点Bundle暂不支持本地化;停止操作并告知用户B2C支持计划将在B2C本地化功能就绪后推出 |
运行时组织版本检查(前置条件4)。为UI Bundle在运行时解析标签的 GraphQL路径在Salesforce Release 264(API v68.0及以上)中发布。中的记录的是你声明的版本,而非组织实际支持的版本,因此较新的CLI指向较旧组织时,静态文件检查可能通过,但运行时会失败。在配置任何内容前,请查询组织的实际最高API版本:
platform.labelssfdx-project.jsonsourceApiVersionbash
bash <skill-dir>/scripts/check-org-api-version.sh <组织别名或用户名>返回码 → 组织支持v68.0+,继续操作。返回码 → 组织版本过旧或无法访问;请勿编写i18n配置或标签,向用户报告版本不匹配问题并停止操作。(脚本中的将认证保留在CLI传输层,因此不会将访问令牌带入上下文。)
01sf api request rest已认证应用检测(前置条件5)。Bundle的类型决定了是否支持本地化,可通过确定性的文件和字符串检查来判断。传入Bundle目录的完整路径;脚本会从中推导元数据根目录,因此当前目录不影响结果:
bash
bash <skill-dir>/scripts/detect-bundle-type.sh <uiBundles/<名称>/目录的完整路径>根据返回码操作: → 已认证应用(核心内部或B2E),继续操作; → 站点(B2C/B2B),站点Bundle暂不支持本地化,停止操作并告知用户(B2C支持计划将在B2C本地化功能就绪后推出); → 未绑定或无法自动检测,请用户确认该Bundle为已认证应用(B2E或核心内部应用),若用户无法确认则停止操作。
012若前置条件未满足,请停止操作:向用户报告具体的阻塞问题,并记录待解决项以便后续处理。请勿修改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 / files for user-facing hardcoded strings.
.tsx.jsxWhat to scan:
- String literals inside JSX tags: → candidate
<h1>Welcome</h1> - String props shown to users: → candidate
placeholder="Enter name" - User-facing accessible text: ,
aria-label,aria-describedby→ candidate (a screen-reader user hears these, so they must localize too)alt
What to skip:
- Import statements
- Object keys / property names
- attributes (machine-readable)
data-* - Test IDs (,
data-testidattributes)id - Text already wrapped in calls
t() - Console logs, error messages thrown to developers (not user-facing)
- Class names, file paths, technical constants
Action:
- Scan the directory for
src/and.tsxfiles.jsx - Extract candidates, showing file path + line number for each
- 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() - 控制台日志、抛出给开发者的错误信息(非用户可见)
- 类名、文件路径、技术常量
操作:
- 扫描目录下的
src/和.tsx文件.jsx - 提取候选字符串,显示每个字符串的文件路径+行号
- 将列表展示给开发者
完成标准:
开发者确认列表正确(或编辑列表以移除误报项)。
暂停点:"我在M个组件中找到了N个用户可见字符串。列表如下:[显示文件:行号 + 字符串]。是否正确?[确认/编辑列表/跳过部分内容]"
Step 2: Extract
步骤2:提取
Goal: For each confirmed string, add a Custom Label and replace the JSX literal with a call.
t()Action for each string:
- Propose a key name, format: (e.g.,
<Context>_<Role>→"Welcome",Welcome_Text→"Save",Save_Button→"Failed to save"). Follow naming: PascalCase words, underscores between parts, descriptive enough to be unique.Save_Failed_Message - Add the label to :
force-app/main/default/labels/CustomLabels.labels-meta.xml(Full XML structure: references/label-xml.md)xml<labels> <fullName>Welcome_Text</fullName> <language>en_US</language> <protected>false</protected> <shortDescription>Welcome banner heading</shortDescription> <value>Welcome</value> </labels> - Replace the string in the component with :
{t("Key")}tsx// Before: <h1>Welcome</h1> // After: <h1>{t("Welcome_Text")}</h1> - Add the import if not present: and
import { useTranslation } from "react-i18next";at the top of the component function.const { t } = useTranslation("c");
Completion criterion:
Every confirmed string has both a entry and a call in its original location.
CustomLabelst()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()每个字符串的操作:
- 建议键名,格式:(例如:
<上下文>_<角色>→"Welcome",Welcome_Text→"Save",Save_Button→"Failed to save")。遵循命名规则:单词首字母大写,各部分用下划线分隔,描述性强且唯一。Save_Failed_Message - 将标签添加到:
force-app/main/default/labels/CustomLabels.labels-meta.xml(完整XML结构:references/label-xml.md)xml<labels> <fullName>Welcome_Text</fullName> <language>en_US</language> <protected>false</protected> <shortDescription>欢迎横幅标题</shortDescription> <value>Welcome</value> </labels> - 替换组件中的字符串为:
{t("Key")}tsx// 替换前: <h1>Welcome</h1> // 替换后: <h1>{t("Welcome_Text")}</h1> - 添加导入语句(若未存在):在组件函数顶部添加和
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:
- Add each key to the manifest array in :
src/i18n/label-manifest.tsIf 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.typescriptexport const labelManifest = [ "c:Welcome_Text", "c:Save_Button", "c:Save_Failed_Message", ];
Completion criterion:
Run from the UI bundle dir (it scans relative to the current directory) and report any errors it returns. It owns the deterministic inspection: it cross-checks every call site against the manifest and treats a missing (when 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.
check-manifest-registered.shsrc/t("Key")label-manifest.tst()bash
cd <path-to-uiBundles/<name>/ dir> # scripts scan src/ relative to here
bash <skill-dir>/scripts/check-manifest-registered.shBranch on the exit code: , every key is registered (or there are no calls to gate), proceed. , the manifest is missing or the listed keys aren't in it; scaffold or add them (Step 4 scaffolds the file) and re-run. , 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.
0t()164Pause: "Added N entries to label-manifest.ts. check-manifest-registered.sh passed: [confirm]."
目标:将每个键添加到标签清单中,以便i18next知道需要获取该标签。
操作:
- 将每个键添加到的清单数组中:
src/i18n/label-manifest.ts若该文件尚未存在,步骤4会自动生成;下方的完成检查会报告文件缺失,因此无需手动检查文件是否存在。typescriptexport const labelManifest = [ "c:Welcome_Text", "c:Save_Button", "c:Save_Failed_Message", ];
完成标准:
在UI Bundle目录下运行(脚本会扫描当前目录下的),并报告返回的任何错误。该脚本负责确定性检查:它会将每个调用位置与清单进行交叉核对,若存在调用但缺失,则判定为失败。未注册的键在运行时会直接渲染为键名本身且无任何错误提示,这正是该脚本要防范的静默失败陷阱。
check-manifest-registered.shsrc/t("Key")t()label-manifest.tsbash
cd <uiBundles/<名称>/目录路径> # 脚本会扫描当前目录下的src/
bash <skill-dir>/scripts/check-manifest-registered.sh根据返回码分支处理: → 所有键均已注册(或无调用需要检查),继续操作。 → 清单缺失或列出的键未包含在其中;生成文件或添加键(步骤4会生成文件)后重新运行。 → 使用错误,源目录不存在(当前目录错误或参数无效);这不是"键缺失"的结果,请勿生成文件或注册键,修正路径后重新运行。
0t()164暂停点:"已向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 from the UI bundle dir (it scans relative to the current directory) and report what it returns. The script owns the whole deterministic inspection: it looks for an init file defining 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.
check-i18n-wired.shsrc/initI18n()bash
cd <path-to-uiBundles/<name>/ dir> # scripts scan src/ relative to here
bash <skill-dir>/scripts/check-i18n-wired.shBranch on the exit code (the printed message names the specific file/symbol for your report, but the decision is the code):
- Exit → fully wired, the manifest is passed into the backend; go to "If i18n already exists" below and just add new keys.
0 - Exit → no
1exists; scaffold the whole setup via "If no i18n setup exists yet".initI18n() - Exit → the init already exists but isn't called at boot; do not re-scaffold or overwrite it. Add only the boot-time
2call in the entry file (step 4 of "If no i18n setup exists yet"), then re-run.initI18n() - Exit → wired at boot but the script could not confirm the manifest is passed into the backend. It scans the whole
3tree, 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 insrc. Only if it genuinely dangles, do what the message names: if the manifest is imported but unused, pass it into the existingbackendOptionswithout clobbering it; if there's nobackendOptions/backendOptionsconfig 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.SalesforceBackend - Exit → 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
64path) and re-run.src
If no i18n setup exists yet:
- Install dependencies (tell the user to run):
bash
npm install i18next react-i18next i18next-chained-backend i18next-localstorage-backend - Create with the init wiring (full code: references/i18n-setup.md)
src/i18n/index.ts - Create with an empty array (Step 3 will populate it)
src/i18n/label-manifest.ts - Call once at boot in the entry file (before mounting the app):
initI18n()typescriptimport { initI18n } from "./i18n"; initI18n().then(() => { // mount app });
If i18n already exists:
Act on the message 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.
check-i18n-wired.shCompletion criterion:
exists and is called once at boot; the manifest is wired into the backend.
initI18n()Pause: "i18next setup [exists / created]. initI18n() is called at boot: [confirm]."
目标:确保i18next初始化文件存在;若应用尚未配置i18n,则自动生成相关文件。
检查:
在UI Bundle目录下运行(脚本会扫描当前目录下的),并报告返回结果。该脚本负责完整的确定性检查:它会查找定义了的初始化文件及启动时对该函数的调用,若这些均存在,还会检查标签清单是否已导入并传入后端配置。请勿手动读取文件推导这些信息。
check-i18n-wired.shsrc/initI18n()bash
cd <uiBundles/<名称>/目录路径> # 脚本会扫描当前目录下的src/
bash <skill-dir>/scripts/check-i18n-wired.sh根据返回码分支处理(打印消息会指明具体文件/符号供你报告,但决策基于返回码):
- 返回码→ 配置完成,清单已传入后端;跳至下方"若已存在i18n配置"部分,仅添加新键即可。
0 - 返回码→ 不存在
1;通过"若尚未配置i18n"部分生成完整配置。initI18n() - 返回码→ 初始化文件已存在但未在启动时调用;请勿重新生成或覆盖该文件。仅在入口文件中添加启动时的
2调用("若尚未配置i18n"部分的步骤4),然后重新运行脚本。initI18n() - 返回码→ 已在启动时配置,但脚本无法确认清单已传入后端。脚本会扫描整个
3目录,但此最终检查为文本启发式检查:清单可能通过变量、扩展运算符或脚本无法识别的辅助函数进行配置,因此将返回码3视为"编辑前验证",而非"确定已损坏"。打开消息指明的文件,确认清单是否真的未在src中。若确实未配置,则按照消息提示操作:若已导入清单但未使用,将其传入现有backendOptions(请勿覆盖现有配置);若完全没有backendOptions/backendOptions配置,则将该后端块添加到现有初始化文件中(参考references/i18n-setup.md)。绝不要重新生成初始化文件或重复已有的配置。SalesforceBackend - 返回码→ 使用错误:源目录不存在(当前目录错误或参数无效)。这不是"无初始化文件"的结果;请勿生成文件。修正路径(在UI Bundle目录下运行,或传入其
64路径)后重新运行。src
若尚未配置i18n:
- 安装依赖(告知用户运行):
bash
npm install i18next react-i18next i18next-chained-backend i18next-localstorage-backend - 创建并添加初始化配置(完整代码:references/i18n-setup.md)
src/i18n/index.ts - 创建并添加空数组(步骤3会填充该数组)
src/i18n/label-manifest.ts - 在入口文件的启动阶段调用一次(在挂载应用之前):
initI18n()typescriptimport { 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:
-
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)."
-
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
-
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> -
Open the app at theURL on the
/lwr/application/ai/<namespace>-<bundleName>domain (redirects to the app host).lightning.force.com -
Change the user's Language (not Locale), Setup → My Settings → Language & Time Zone → Language → pick the translated language → Save.
-
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 keys in DevTools)
i18next_res_*
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]"
目标:引导开发者验证标签在第二种语言下的渲染效果。
操作:
-
激活第二种语言(若尚未激活),告知用户:"在你的组织中,进入Setup → Translation Workbench → Translation Settings → 添加一种语言(例如西班牙语)。"
-
编写翻译内容,为该语言生成空的翻译文件: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
-
构建并部署,告知用户:bash
sf config set target-org=<别名> # API版本会固化;请先指向部署目标 npm run build sf project deploy start --source-dir force-app --target-org <别名> -
打开应用,访问域名下的
lightning.force.comURL(会重定向到应用主机)。/lwr/application/ai/<命名空间>-<bundleName> -
更改用户语言(注意不是区域设置),进入Setup → My Settings → Language & Time Zone → Language → 选择已翻译的语言 → Save。
-
重新加载应用,标签应切换为已翻译的语言。
若标签未渲染:
检查references/gotchas.md中的三个陷阱:
- 未注册的清单键(步骤3遗漏了某个标签)
- API版本不匹配(针对不同组织构建)
- localStorage缓存过期(在开发者工具中清除键)
i18next_res_*
完成标准:
标签在≥2种语言环境下正常渲染,或已识别出阻塞问题。
暂停点:"验证步骤:在Translation Workbench中激活第二种语言,编写翻译内容(我可以生成XML模板),构建/部署并重新加载应用。是否需要我为[语言]生成翻译文件模板?[是/我手动完成]"
Edge cases: handle gracefully
边缘情况:优雅处理
- Already-localized code: detect existing usage / a populated manifest; offer to add to the setup rather than re-scaffold everything.
t() - 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
约束规则:绝不能违反
- 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 ; unreviewed machine translations are a quality liability.
translation-meta.xml - 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.
- Never clobber existing i18n wiring. If Step 4 finds an existing , reconcile (add the manifest import if missing) rather than replace the whole file.
initI18n() - Every file must be customer-safe. No , core-only paths, or internal infrastructure references anywhere. Write as if for an external customer in an SFDX project.
webapps
- 绝不自动翻译可部署的元数据。生成空的翻译文件并引导开发者编写翻译内容(手动或通过Translation Workbench)。请勿调用任何机器翻译API并将结果粘贴到中;未经审核的机器翻译会带来质量风险。
translation-meta.xml - 绝不注册无对应标签的键。清单条目数量必须与标签数量相等(步骤3标准)。未注册的键会直接渲染为键名本身且无控制台警告,这是最常见的本地化bug。
- 绝不覆盖已有的i18n配置。若步骤4发现已存在,则协调配置(若缺失则添加清单导入),而非替换整个文件。
initI18n() - 所有文件必须对客户安全。不得包含、核心专属路径或内部基础设施引用。编写内容时假设面向外部客户的SFDX项目。
webapps
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()| Command | Run from | Purpose |
|---|---|---|
| UI bundle dir | Install i18n dependencies (Step 4) |
| UI bundle dir | Build the app (API version bakes in, set target-org first) |
| Project root | Deploy the app + labels + translations |
| Project root | Pull 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()| 命令 | 运行目录 | 用途 |
|---|---|---|
| UI Bundle目录 | 安装i18n依赖(步骤4) |
| UI Bundle目录 | 构建应用(API版本会固化,请先设置target-org) |
| 项目根目录 | 部署应用+标签+翻译内容 |
| 项目根目录 | 获取在Translation Workbench中编写的翻译内容 |
Pre-flight checklist: completion criteria for the whole run
预检查清单:整个流程的完成标准
- Every confirmed string has both a entry and a
CustomLabelscallt() - entry count == label count (no unregistered keys)
label-manifest.ts - present and called once at boot
initI18n() - Labels render in ≥2 locales (or the blocking gotcha is named)
- No hand-written machine translations landed in (only scaffold-and-guide)
*-meta.xml
- 每个确认的字符串都有对应的CustomLabels条目和调用
t() - 条目数量 == 标签数量(无未注册的键)
label-manifest.ts - 存在且在启动时调用一次
initI18n() - 标签在≥2种语言环境下正常渲染(或已识别出阻塞问题)
- 中无手动添加的机器翻译内容(仅生成模板并引导用户编写) ",
*-meta.xml