From 8c7b59285173d39d3df3ae10d8218e3505e59f7a Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 02:43:25 +0000 Subject: [PATCH] fix(i18n): translate every select option into Simplified Chinese (#645) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fifteen select fields across nine objects had no option labels in any of the four locales. A missing entry is not a runtime error — the resolver falls back to the English label in code, and where a field had no entry at all the picklist rendered the raw stored value. On an otherwise fully translated Chinese screen that surfaced `net_30` in Contract payment terms and `crm_account` in a Task's related-to picker; `crm_product.billing_type` and `crm_product.unit_of_measure` had no zh-CN entry at all, so even their field labels were English. Wording pairs with each field's neighbours in the same bundle rather than rendering the English literally: `crm_task.related_to_type`'s options are object names and reuse each object's own label from this bundle, and `crm_quote.payment_terms` matches `crm_contract.payment_terms`, which shares its option set. PENDING_SELECT_LABELS shrinks by these 15 rows' zh-CN entries. No row lists zh-CN any more, so the UNTRANSLATED_EVERYWHERE shorthand is retired; en, ja-JP and es-ES keep their rows and remain the open scope of #645. Full `objectstack lint` i18n warnings drop from 873 to 803. Refs #645. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01J8UZqd6mJMPHAXKb6XcHmi --- .changeset/zh-cn-select-option-labels.md | 45 +++++++++++ src/translations/zh-CN.ts | 99 +++++++++++++++++++++--- test/metadata-references.test.ts | 42 +++++----- 3 files changed, 155 insertions(+), 31 deletions(-) create mode 100644 .changeset/zh-cn-select-option-labels.md diff --git a/.changeset/zh-cn-select-option-labels.md b/.changeset/zh-cn-select-option-labels.md new file mode 100644 index 00000000..445a4323 --- /dev/null +++ b/.changeset/zh-cn-select-option-labels.md @@ -0,0 +1,45 @@ +--- +'hotcrm': patch +--- + +Simplified Chinese now translates every select field's options, so no picklist in the zh-CN UI renders a raw stored value. + +Fifteen select fields across nine objects had no option labels in **any** of the +four locales. A missing entry is not a runtime error — the resolver falls back to +the English `label` in code, and where a field had no entry at all the picklist +rendered the raw stored value. On an otherwise fully translated Chinese screen a +rep saw `net_30` in Contract payment terms, `waiting_customer`-style bare keys in +Task type, and `crm_account` in a Task's "related to" picker. Contract, Quote and +Product were the worst hit: `crm_product.billing_type` and +`crm_product.unit_of_measure` had no zh-CN entry whatsoever, so even the field +labels were English. + +The fields completed here: + +| Object | Fields | +|---|---| +| `crm_case` | `type` | +| `crm_contact` | `salutation` | +| `crm_contract` | `billing_frequency`, `payment_terms`, `contract_type` | +| `crm_knowledge_article` | `category`, `tags` (partial — 2 and 4 values were missing) | +| `crm_opportunity` | `competitors` | +| `crm_product` | `family`, `billing_type`, `unit_of_measure` | +| `crm_quote` | `payment_terms` | +| `crm_task` | `type`, `related_to_type`, `recurrence_type` | + +Wording pairs with each field's neighbours in the same bundle rather than +rendering the English literally: `crm_task.related_to_type`'s options are object +names, so each one reuses that object's own `label` from this bundle (`crm_account` +→ 客户), and `crm_quote.payment_terms` is worded identically to +`crm_contract.payment_terms`, which shares its option set. + +`PENDING_SELECT_LABELS` in `test/metadata-references.test.ts` shrinks by these 15 +rows' zh-CN entries — the ledger may only ever shrink, and the guard fails a row +that has since been translated as stale. No row lists `zh-CN` any more, so the +`UNTRANSLATED_EVERYWHERE` shorthand is retired; Simplified Chinese is now the one +locale with complete select coverage. `en`, `ja-JP` and `es-ES` keep their rows +and remain the open scope of #645. + +Full `objectstack lint` i18n warnings drop from 873 to 803. + +Refs #645. diff --git a/src/translations/zh-CN.ts b/src/translations/zh-CN.ts index 29ba1eaf..2e276925 100644 --- a/src/translations/zh-CN.ts +++ b/src/translations/zh-CN.ts @@ -87,7 +87,10 @@ export const zhCN: TranslationData = { pluralLabel: '联系人', description: '客户与商机的关键人物联系人', fields: { - salutation: { label: '称谓' }, + salutation: { + label: '称谓', + options: { mr: '先生', ms: '女士', mrs: '夫人', dr: '博士', prof: '教授' }, + }, first_name: { label: '名' }, last_name: { label: '姓' }, full_name: { label: '全名' }, @@ -170,9 +173,17 @@ export const zhCN: TranslationData = { options: { getting_started: '入门指南', how_to: '操作指南', troubleshooting: '故障排查', billing: '账务与价格', api: 'API 与集成', + release_notes: '版本说明', + policy: '政策制度', + }, + }, + tags: { + label: '标签', + options: { + auth: '身份认证', sso: '单点登录', mobile: '移动端', email: '邮件', + reports: '报表', performance: '性能', data_import: '数据导入', webhooks: 'Webhook', }, }, - tags: { label: '标签', options: { auth: '身份认证', sso: '单点登录', mobile: '移动端', email: '邮件' } }, status: { label: '状态', options: { draft: '草稿', in_review: '审核中', published: '已发布', archived: '已归档' }, @@ -396,7 +407,13 @@ export const zhCN: TranslationData = { discount_amount: { label: '折扣金额' }, tax: { label: '税额' }, shipping_handling: { label: '运费及手续费' }, - payment_terms: { label: '付款条款' }, + payment_terms: { + label: '付款条款', + options: { + net_15: '15 天账期', net_30: '30 天账期', net_60: '60 天账期', + net_90: '90 天账期', due_on_receipt: '货到付款', + }, + }, shipping_terms: { label: '运输条款' }, billing_address: { label: '账单地址' }, shipping_address: { label: '收货地址' }, @@ -438,11 +455,26 @@ export const zhCN: TranslationData = { crm_opportunity: { label: '关联商机' }, owner: { label: '合同负责人' }, contract_term_months: { label: '合同期限(月)' }, - billing_frequency: { label: '计费周期' }, - payment_terms: { label: '付款条款' }, + billing_frequency: { + label: '计费周期', + options: { monthly: '按月', quarterly: '按季度', annually: '按年', one_time: '一次性' }, + }, + payment_terms: { + label: '付款条款', + options: { + net_15: '15 天账期', net_30: '30 天账期', net_60: '60 天账期', + net_90: '90 天账期', due_on_receipt: '货到付款', + }, + }, auto_renewal: { label: '自动续约' }, renewal_notice_days: { label: '续约通知(天)' }, - contract_type: { label: '合同类型' }, + contract_type: { + label: '合同类型', + options: { + subscription: '订阅合同', service: '服务协议', license: '授权许可', + partnership: '合作协议', nda: '保密协议', msa: '主服务协议', + }, + }, signed_date: { label: '签署日期' }, signed_by: { label: '签署人' }, document_url: { label: '合同文档' }, @@ -487,7 +519,12 @@ export const zhCN: TranslationData = { label: '优先级', options: { low: '低', medium: '中', high: '高', critical: '紧急' }, }, - type: { label: '类型' }, + type: { + label: '类型', + options: { + question: '咨询', problem: '故障', feature_request: '功能需求', bug: '缺陷', + }, + }, owner: { label: '负责人' }, origin: { label: '工单来源', @@ -583,18 +620,33 @@ export const zhCN: TranslationData = { options: { low: '低', normal: '普通', high: '高', urgent: '紧急' }, }, due_date: { label: '截止日期' }, - type: { label: '任务类型' }, + type: { + label: '任务类型', + options: { + call: '电话', email: '邮件', meeting: '会议', + follow_up: '跟进', demo: '演示', other: '其他', + }, + }, reminder_date: { label: '提醒时间' }, completed_date: { label: '完成日期' }, owner: { label: '负责人' }, - related_to_type: { label: '关联对象类型' }, + related_to_type: { + label: '关联对象类型', + options: { + crm_account: '客户', crm_contact: '联系人', crm_opportunity: '商机', + crm_lead: '线索', crm_case: '工单', + }, + }, related_to_account: { label: '关联客户' }, related_to_contact: { label: '关联联系人' }, related_to_opportunity: { label: '关联商机' }, related_to_lead: { label: '关联线索' }, related_to_case: { label: '关联工单' }, is_recurring: { label: '重复任务' }, - recurrence_type: { label: '重复类型' }, + recurrence_type: { + label: '重复类型', + options: { daily: '每日', weekly: '每周', monthly: '每月', yearly: '每年' }, + }, recurrence_interval: { label: '重复间隔' }, recurrence_end_date: { label: '重复结束日期' }, is_completed: { label: '是否完成' }, @@ -691,9 +743,29 @@ export const zhCN: TranslationData = { }, }, cost: { label: '成本' }, + billing_type: { + label: '计费类型', + options: { + one_time: '一次性', monthly: '按月', quarterly: '按季度', + annual: '按年', usage: '按用量', + }, + }, + unit_of_measure: { + label: '计量单位', + options: { + each: '个', license: '许可证', seat: '席位', + hour: '小时', day: '天', month: '月', + }, + }, is_active: { label: '是否启用' }, description: { label: '描述' }, - family: { label: '产品系列' }, + family: { + label: '产品系列', + options: { + enterprise: '企业级方案', smb: '中小企业方案', + services: '专业服务', cloud: '云服务', + }, + }, list_price: { label: '标价' }, sku: { label: 'SKU' }, quantity_on_hand: { label: '库存数量' }, @@ -763,7 +835,10 @@ export const zhCN: TranslationData = { cold_call: '陌生拜访', email_campaign: '邮件营销', other: '其他', }, }, - competitors: { label: '竞争对手' }, + competitors: { + label: '竞争对手', + options: { competitor_a: '竞争对手 A', competitor_b: '竞争对手 B', competitor_c: '竞争对手 C' }, + }, crm_campaign: { label: '营销活动' }, days_in_stage: { label: '当前阶段天数' }, stage_entry_date: { label: '进入当前阶段日期' }, diff --git a/test/metadata-references.test.ts b/test/metadata-references.test.ts index 9c1ce387..a8fe7a1d 100644 --- a/test/metadata-references.test.ts +++ b/test/metadata-references.test.ts @@ -1327,11 +1327,15 @@ describe('select fields are translated in every locale', () => { * field the PR is actually about. */ /** - * Shorthand for a field NO locale translates. Written out literally rather - * than derived from `localePacks`, so that adding a fifth locale surfaces as - * new gaps to fill instead of silently extending every exemption to cover it. + * Every row is written out literally rather than derived from `localePacks`, + * so that adding a fifth locale surfaces as new gaps to fill instead of + * silently extending every exemption to cover it. + * + * No row lists `zh-CN` any more: the 15 fields that were untranslated in all + * four locales were completed in Chinese first, ahead of a zh-CN customer + * trial. Simplified Chinese is therefore the one locale with full select + * coverage — a row regaining `zh-CN` is a regression, not a re-scope. */ - const UNTRANSLATED_EVERYWHERE = ['en', 'zh-CN', 'ja-JP', 'es-ES']; const PENDING_SELECT_LABELS: Record = { 'crm_account.tier': ['ja-JP', 'es-ES'], 'crm_account.segment': ['ja-JP', 'es-ES'], @@ -1341,19 +1345,19 @@ describe('select fields are translated in every locale', () => { 'crm_campaign.status': ['en', 'ja-JP', 'es-ES'], 'crm_case.status': ['en', 'ja-JP', 'es-ES'], 'crm_case.priority': ['en', 'ja-JP', 'es-ES'], - 'crm_case.type': UNTRANSLATED_EVERYWHERE, - 'crm_contact.salutation': UNTRANSLATED_EVERYWHERE, + 'crm_case.type': ['en', 'ja-JP', 'es-ES'], + 'crm_contact.salutation': ['en', 'ja-JP', 'es-ES'], 'crm_contact.lead_source': ['en', 'ja-JP', 'es-ES'], 'crm_contract.status': ['en', 'ja-JP', 'es-ES'], - 'crm_contract.billing_frequency': UNTRANSLATED_EVERYWHERE, - 'crm_contract.payment_terms': UNTRANSLATED_EVERYWHERE, - 'crm_contract.contract_type': UNTRANSLATED_EVERYWHERE, + 'crm_contract.billing_frequency': ['en', 'ja-JP', 'es-ES'], + 'crm_contract.payment_terms': ['en', 'ja-JP', 'es-ES'], + 'crm_contract.contract_type': ['en', 'ja-JP', 'es-ES'], // Partial: 5 of 7 categories and 4 of 8 tags are translated everywhere. - 'crm_knowledge_article.category': UNTRANSLATED_EVERYWHERE, - 'crm_knowledge_article.tags': UNTRANSLATED_EVERYWHERE, + 'crm_knowledge_article.category': ['en', 'ja-JP', 'es-ES'], + 'crm_knowledge_article.tags': ['en', 'ja-JP', 'es-ES'], 'crm_lead.salutation': ['en', 'ja-JP', 'es-ES'], 'crm_lead.industry': ['en', 'ja-JP', 'es-ES'], - 'crm_opportunity.competitors': UNTRANSLATED_EVERYWHERE, + 'crm_opportunity.competitors': ['en', 'ja-JP', 'es-ES'], // `crm_opportunity.win_reason` / `loss_reason` left the ledger in #593: // both became REQUIRED at close and both now drive a Sales-dashboard // widget, so an untranslated option is no longer a cosmetic gap — it is a @@ -1362,16 +1366,16 @@ describe('select fields are translated in every locale', () => { // it is untouched by that change and belongs to #645's sweep. 'crm_opportunity.approval_status': ['ja-JP', 'es-ES'], 'crm_product.category': ['en', 'ja-JP', 'es-ES'], - 'crm_product.family': UNTRANSLATED_EVERYWHERE, - 'crm_product.billing_type': UNTRANSLATED_EVERYWHERE, - 'crm_product.unit_of_measure': UNTRANSLATED_EVERYWHERE, + 'crm_product.family': ['en', 'ja-JP', 'es-ES'], + 'crm_product.billing_type': ['en', 'ja-JP', 'es-ES'], + 'crm_product.unit_of_measure': ['en', 'ja-JP', 'es-ES'], 'crm_quote.status': ['en', 'ja-JP', 'es-ES'], - 'crm_quote.payment_terms': UNTRANSLATED_EVERYWHERE, + 'crm_quote.payment_terms': ['en', 'ja-JP', 'es-ES'], 'crm_task.status': ['en', 'ja-JP', 'es-ES'], 'crm_task.priority': ['en', 'ja-JP', 'es-ES'], - 'crm_task.type': UNTRANSLATED_EVERYWHERE, - 'crm_task.related_to_type': UNTRANSLATED_EVERYWHERE, - 'crm_task.recurrence_type': UNTRANSLATED_EVERYWHERE, + 'crm_task.type': ['en', 'ja-JP', 'es-ES'], + 'crm_task.related_to_type': ['en', 'ja-JP', 'es-ES'], + 'crm_task.recurrence_type': ['en', 'ja-JP', 'es-ES'], }; /** Every authored select field, with its option VALUES (what the DB stores). */