Repository navigation
fix(i18n): 回填 auth 家族 54 个缺失语言 key,十包补齐 (#3546 切片三) - #3814
Conversation
`scripts/check-i18n-call-site-keys.mjs`(#3530)实测:`auth` 26 + `oauth` 16 + `acceptInvitation` 12 = 54 个 key 被 `t()` 引用而**任何语言包 都没有**,54 个不重复 key 对应 54 个调用点(本切片恰好 1:1;切片二是 90 key 93 站点,分母一律脚本实测,不手数)。54 处全部写了内联 `t(key, { defaultValue: '英文' })`,即 #3517 那一类:英文照常渲染,十种语言 统统翻不了。裸 key 站点在切片一(PR #3583)已清完;本切片对三个命名空间的 全部 122 个调用点做了 AST 扫描,死兜底 `t(key) || '英文'` 为 0 —— 所以不动 任何组件文件。 - `en.ts` 补 54 key:`oauth.consent.*` 与 `acceptInvitation.*` 是全新顶层 命名空间,其余 26 个扩充 `auth.login` / `auth.forgotPassword` / `auth.device` / `auth.verifyEmail`。52 个字面量 defaultValue 站点的 en 值 与调用点逐字节相同(脚本比对 52/52),包路径与内联兜底不可能分叉;剩下 2 个 (`oauth.consent.title` / `.request`)的 defaultValue 是 JS 模板,字节相同 在结构上不可能(`${…}` vs `{{…}}`),按调用点实际声明的插值契约落值。 - 九包实译按同包邻键取证(fr 冒号/问号前排版空格、de 半角破折号、ru ё、 ar 动词居首避免 RTL 句首落 Latin token、zh 全角标点)。十包唯一共享的 字符串是 `phonePlaceholder`(E.164 示例号,与包内既有 `name@example.com` 同类),以精确集合钉死 18 对。 - 棘轮 `scripts/i18n-call-site-key-baseline.json` 精确减 54(163 → 109)。 - `{seconds}` 是 auth 表单自己的 `.replace()` 洞而非 i18next 插值 (`LoginForm.tsx:429`/`ForgotPasswordForm.tsx:367`),十包都必须保留单花括号, 测试双向钉扎。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
|
✅ 切片三验收:ACCEPT — PR #3814( 裁定要点:(1) 分母纪律执行到位 —— 54 key 以脚本实测(主动复核多站点陷阱,本切片恰 1:1),棘轮 163→109 四条 ASSERT 全真;(2) 九包邻键取证 + es 敬称「同一规则两个答案」(oauth 随 auth.device 的 usted、acceptInvitation 随 organization.accept 的 tú)—— 正确应用,风险项立此存照可由维护者廉价改判;(3) 反抄袭集合 18/486 全部来自 E.164 示例号单一理由,三条 presence 断言防空转 —— 切片二 B1 教训落实;(4) 纪律违规处置:commit trailer 含模型标识(push 后发现,禁 force-push 故未改历史)。PM 核验:main 近 8 个 squash commit 零该标识命中 —— 本仓 squash 落 main 只取标题行,该 trailer 不会到 main,故验收放行;违规如实记录,已在后续所有派发词加「push 前自查 trailer」硬规。衍生 #3810/#3811 两 finding 归分诊席;切片四(候选 console 41)在本 PR 落 main 后开。undraft + auto-merge。 Generated by Claude Code |
Fixes-part-of #3546— 切片三。本单不关闭 #3546:回填后仍余 109 个缺失 key + 4 个整族缺失前缀。基线
origin/main@d9ce38529。分母:脚本实测,不手数
node scripts/check-i18n-call-site-keys.mjs(#3530 的守卫)的analyze()实测本切片三命名空间:auth26 +oauth16 +acceptInvitation12)missing-prefix切片二的教训(预测 +90 实为 +93,三个 key 各有两个调用点)在这里主动复核过:本切片
keys with >1 call site为空集。守卫读数,前后对照(同一命令):
解析数 +54、en key 数 +54,与清单精确相等。
棘轮
removed === 54断言输出:严重度:全部是"英文可见、十语不可译"
按切片一验收修正后的框架叙述。54 处全部带内联
defaultValue,无一渲染裸 key —— 裸 key 站点在 PR #3583 已清完。用户实际看到的:zh会话下/login切到手机/短信分支,"Email or phone number"、"Get code"、"Resend in {seconds}s"、"Sign in with password instead" 全是英文;整个/oauth/consent页(含四条描述"这个第三方客户端即将获得什么"的 scope 句)只有英文;/accept-invitation与设备授权未启用的死胡同同理。死兜底:先量再动,量出 0
对三个命名空间全部 122 个调用点(不只 54 个缺失的)做 AST 扫描,找
t(key) || '…'/?? '…'形态(i18next miss 返回 key 恒真,兜底永不触发 —— 切片一在ObjectView.tsx删掉 5 处):唯一形似的一处是
VerifyEmailPromptPage.tsx:73的(err as Error)?.message?.trim() || t('auth.verifyEmail.resendUnavailable', …)——t()在||右侧,是正常的"服务端消息优先"链,不是死兜底。所以本 PR 零组件改动。en ↔ 内联 defaultValue:52/52 逐字节相同,2 处结构上不可能
52 个字面量站点全等,provider 两路不会分叉。剩下 2 个的 defaultValue 是 JS 模板字符串,字节相同在结构上不可能(
${appName}vs i18next 的{{appName}}),按调用点实际声明的 options 落值:oauth.consent.title— 站点传{ appName },en 落{{appName}} wants to access your account,与模板逐词对应,渲染完全一致。oauth.consent.request— 站点同时传appName和suffix,而suffix是页面预格式化的' (' + user.email + ')';它自己的 defaultValue 模板写的却是' for ' + user.email。en 落{{appName}} is requesting permission{{suffix}}.,即跟随声明的 options(contract-first:插值契约在生产端,消费端不做宽容拼接)。后果诚实记在这里:该句英文渲染由… permission for ada@example.com.变为… permission (ada@example.com).—— 三个词的括号化差异,是站点内部本就存在的两种写法取其一。已在 [finding] 8 处 auth 调用点的内联 defaultValue 与 en 包值不一致 —— key 存在故兜底是死代码,三道 i18n 门禁按设计都看不见这一类 #3810 另行立项记录(同一类的其他 8 处一并在内),不在本 PR 修改组件。九包实译:逐包邻键证据
每包都在同一个包的 auth 邻域取证(不跨包套用):
auth.device.invalidDescription=URL 中未提供设备代码。、organization.accept.*(切片二)URL 中缺少邀请 ID。;全角:()、。auth.verifyEmail.sentTo=Envoyé à :、login.forgotPasswordText=Mot de passe oublié ?(:?前普通空格,非 U+202F,已 od 核字节)Cette application pourra :login.errors.oauthCallbackFailed用半角破折号—;device.approving=Genehmige…一人称进行式Nehme an…/Lehne ab…auth.device.*用 usted(Apruebe/Puede cerrar);organization.accept.*用 tú(Te han invitado)Puede revocar…)、acceptInvitation 面 tú(Te han invitado a unirte…)—— 同一规则两个答案device.approvedDescription=Você pode voltar…Você foi convidado para entrar em uma organização.login.errors.*用 ё(ещё)、device.deniedTitle=Доступ запрещёнДоступ запрещён(复用)、Пароль сброшенdevAdminHint.body结尾用半角:;包内()全角 72 处このアプリは次のことができます:、基本プロフィール(名前、画像)を読み取るremediation.mfa.codeLabel=6자리 코드;(으)로助词括注惯例6자리 코드(复用)、{{appName}}이(가) 계정에 액세스하려고 합니다ssoHandoff/device.subtitle/organization.accept.description)占位符都不在句首يطلب {{appName}} الوصول إلى حسابك—— 动词居首,RTL 句首不落 Latin token,并有断言rendered.startsWith('Acme CLI') === falseen 相同的字符串,一律复用邻键既有译文而不另造:
oauth.consent.denied= enAccess denied与auth.device.deniedTitle同字符串,九包取同一译文;acceptInvitation.accept/accepted/decline/declined与organization.accept.*同 en 字符串,同译文(测试里显式钉了 fr 这一对相等)。otpCodePlaceholder(6-digit code)一律复用各包auth.remediation.mfa.codeLabel的既有拼写。反抄袭断言:精确集合 18 对,单一理由
实测"与 en 逐字节相同"的
lang :: key对:18 / 486,全部来自同一个 key:+1 555 000 0000是 E.164 示例号,与十包早已一致保留不译的auth.login.emailPlaceholder: 'name@example.com'同类;为九个地区各编一个示例号,是对九套拨号计划的格式主张,本仓无任何东西能校验它。断言是集合相等而非子集:第 19 对同形会红,把这 18 个之一改成本地化写法也会红、迫使出列。配 presence 断言防空转(切片二 B1 暴露的问题:集合相等在包为空时同样"绿"):
第三条是反面对照:同一个示例号外面包了一个要翻译的连接词(
または/oder/أو…),于是九包全不同形 —— 证明豁免边界划在 token 上,不是划在"含数字的串"上。provider-mounted 抽样钉扎(discrimination check)
六个组件都是裸
useObjectTranslation(),没有createSafeTranslationdefaults map 挡在前面 —— 所以挂 provider 不是仪式:不挂,回答问题的就不是 i18next,测的就不是 console 用的那条路。抽样(不是 54 key 全测):en/zh各解析 6 个代表 key(每组件一个),断言not.toBe(key)且等于包内值;acceptInvitation.accept、deauth.login.usePhoneOtpText、esoauth.consent.footer、ptauth.forgotPassword.usePhoneResetText、ruauth.forgotPassword.resetButton、jaoauth.consent.scope.profile、koacceptInvitation.declining、arauth.device.disabledTitle;suffix: ''的空后缀路径(句尾只能有一个句点)。反向验证:三处全部预测为红,三处全红
方向在跑之前就定了 —— 本切片是"包里没有 → 补上",没有被删的死枝、没有计数类下游门禁,所以不存在切片先例里的"诊断变多"或"反转"情形,三条都应当是常规的红:
en.ts(棘轮保持已删)→ 守卫应报 54 条 unexpected:zh.ts(en 保留)→ 守卫仍然绿(它只看 en,这正是 [finding] 没有任何守卫断言组件 t() 引用的 key 存在于 en 包 —— parity 测试只管包际一致,调用点→包的一致性是盲区 #3530 记录的门禁分工),而 parity 与本切片测试红,失败文本就是前提本身:第 3 条同时证明了三道门禁的边界没被本 PR 弄混:只补 en 不补九包这种半成品,守卫看不见,parity 看得见。
前缀家族
棘轮
missingPrefixes4 条(console.ai.group./gantt.linkEnd./marketplace.disclosure.runtime./organization.invitations.status.)无一属 auth 家族,原样留在棘轮,并有断言按 4 条精确钉住,防后续切片误以为已处理。验证
all-locales-key-parity.test.ts含在上面 29 个文件内,已绿(它是补完 en 后立刻要求九包补齐的那道门禁)。围栏
packages/i18n/src/locales/*.ts(十包,仅新增本切片 key)、scripts/i18n-call-site-key-baseline.json(仅删本切片 54 条)、packages/i18n/src/__tests__/auth-namespace-3546.test.tsx(新增)、organization-namespace-3546.test.tsx(仅棘轮总数 163 → 109 这一处 —— 切片二把总数硬钉了,每切片动一次且只降)、changeset。零组件改动(死兜底量出为 0)。与在飞 #3482 / #3491 / #3797 零相交;environment.*命名空间不在本切片,未触。未动content/docs/releases/**。顺手量出、未在本 PR 修的事(已另行立项,均为观察级)
auth.login.errors.invalidCredentialsen 为Invalid email or password. Please try again.、站点默认值为Invalid email or password;auth.verifyEmail.resendFailed三个站点写了两种英文,其中两处与 en 相反)。key 存在 ⇒ 包值胜出 ⇒ 那些 defaultValue 是死代码,只误导读者;三道 i18n 门禁按设计都看不见这一类。/accept-invitation/:invitationId有两个组件、两套 i18n 命名空间(acceptInvitation.*与organization.accept.*),console 路由的是功能更弱的那个 #3811 —/accept-invitation/:invitationId有两个组件:apps/console的薄页(本切片的acceptInvitation.*)与 app-shell 导出的DefaultAcceptInvitationPage(会拉取邀请、显示组织/角色/过期,用切片二补的organization.accept.*)。console 路由的是薄的那个,于是同一屏的文案维护了两套 26 个 key。顺带记录一个排除掉的假阳性:
auth.forgotPassword.successDescription的 en 含{{email}}而站点不传该 option,看起来会把{{email}}原样渲染给用户(已实测 i18next 缺变量时确实原样保留)。但packages/auth/src/ForgotPasswordForm.tsx:286自己includes('{{email}}')后.replace()—— 与{seconds}同一手法,组件侧的洞,不是缺陷。Generated by Claude Code