From 992ac77293e2063e0063a8b20074294997670779 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 11:18:06 +0000 Subject: [PATCH] fix(i18n): the zh-CN Activity group and the forecast period fields get labels of their own MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit zh-CN labelled the Activity navigation group and its Events item both 活动. The item keeps 活动, crm_event's own label and the heading of the page it opens; the group becomes 互动, the word the pack already uses for the interactions it gathers. The zh-Hans meetings page quotes the sidebar path and follows. en, es-ES and ja-JP labelled crm_forecast.period_label the same as the period picklist beside it. It now reads Period Label / Etiqueta del periodo / 期間ラベル; zh-CN already kept them apart. A pin holds both pairs apart in every pack. Approval flow and step names are not translated here: the platform's translation contract reaches neither on any approval surface. Claude-Session: https://claude.ai/code/session_01KqkTQNLcxVc588eZpJmp1r Co-authored-by: Claude --- .changeset/2060-item-13-locale-labels.md | 9 +++ .../docs/sales/meetings-and-calls.zh-Hans.mdx | 2 +- src/sales/translations/en/objects.pipeline.ts | 2 +- .../translations/es-ES/objects.pipeline.ts | 2 +- .../translations/ja-JP/objects.pipeline.ts | 2 +- src/sales/translations/zh-CN/app.ts | 8 ++- test/locale-label-collisions.test.ts | 69 +++++++++++++++++++ 7 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 .changeset/2060-item-13-locale-labels.md create mode 100644 test/locale-label-collisions.test.ts diff --git a/.changeset/2060-item-13-locale-labels.md b/.changeset/2060-item-13-locale-labels.md new file mode 100644 index 000000000..98c4a1ac2 --- /dev/null +++ b/.changeset/2060-item-13-locale-labels.md @@ -0,0 +1,9 @@ +--- +"hotcrm": patch +--- + +**The Chinese sidebar names the Activity group and its Events item apart, and the New Forecast form names its two period fields apart.** + +- **Chinese (zh-CN) sidebar.** The Activity group is now 互动, and its Events item stays 活动, the name of the page it opens. Before, both read 活动, so the sidebar showed 活动 › 活动. English, Spanish and Japanese already used two words (Activity › Events, Actividad › Eventos, 活動 › イベント). +- **New Forecast form.** The free-text period name is now labelled **Period Label** in English, **Etiqueta del periodo** in Spanish and **期間ラベル** in Japanese. Before, it was labelled **Period** (**Periodo**, **期間**), the same as the Month / Quarter picklist beside it, so the form showed two fields with one name. Chinese already labelled them apart (周期 / 周期标签). +- **Approval names stay in English for now.** In every language, the approvals inbox and the approval panel on a record still show an approval's name and its step's name in English, for example "Large Deal Approval (on create) · Sales Manager Review". The app's language packs cannot change that today: the platform shows each name as it was written when the approval was opened, and has no translation for an approval step. This release does not change it; the fix belongs to the platform. diff --git a/content/docs/sales/meetings-and-calls.zh-Hans.mdx b/content/docs/sales/meetings-and-calls.zh-Hans.mdx index 2ffa4678a..fa4f29afc 100644 --- a/content/docs/sales/meetings-and-calls.zh-Hans.mdx +++ b/content/docs/sales/meetings-and-calls.zh-Hans.mdx @@ -131,7 +131,7 @@ description: 在任意记录上记录电话、记录会议或预约会议—— ## 到哪里找活动记录 -侧边栏里只有**活动 › 活动**这一个入口;查看这些记录的每一种方式,都是它打开的那个页面顶部的一个标签页,标签上写的就是视图自己的名字: +侧边栏里只有**互动 › 活动**这一个入口;查看这些记录的每一种方式,都是它打开的那个页面顶部的一个标签页,标签上写的就是视图自己的名字: | 标签页 | 显示什么 | | --- | --- | diff --git a/src/sales/translations/en/objects.pipeline.ts b/src/sales/translations/en/objects.pipeline.ts index adabbad82..9df369711 100644 --- a/src/sales/translations/en/objects.pipeline.ts +++ b/src/sales/translations/en/objects.pipeline.ts @@ -402,7 +402,7 @@ export const pipeline: Record = { label: 'Period End', help: 'Normally derived automatically from Period and Period Start. If set by hand, it must be the last day of that period — e.g. 2026-09-30 for a quarter starting 2026-07-01, or 2026-08-31 for Aug 2026.', }, - period_label: { label: 'Period', help: 'Human-friendly label, e.g. "Q3 2026" or "Aug 2026".' }, + period_label: { label: 'Period Label', help: 'Human-friendly label, e.g. "Q3 2026" or "Aug 2026".' }, snapshot_date: { label: 'Snapshot Date', help: 'The day this snapshot was captured.' }, source: { label: 'Source', diff --git a/src/sales/translations/es-ES/objects.pipeline.ts b/src/sales/translations/es-ES/objects.pipeline.ts index fa169c3b7..dc37d9016 100644 --- a/src/sales/translations/es-ES/objects.pipeline.ts +++ b/src/sales/translations/es-ES/objects.pipeline.ts @@ -476,7 +476,7 @@ export const pipeline: Record = { label: 'Fin del periodo', help: 'Normalmente se calcula automáticamente a partir de Periodo e Inicio del periodo. Si se introduce a mano, debe ser el último día de ese periodo — p. ej. 2026-09-30 para un trimestre que empieza el 2026-07-01, o 2026-08-31 para agosto de 2026.', }, - period_label: { label: 'Periodo', help: 'Etiqueta legible, p. ej. «T3 2026» o «Ago 2026».' }, + period_label: { label: 'Etiqueta del periodo', help: 'Etiqueta legible, p. ej. «T3 2026» o «Ago 2026».' }, display_title: { label: 'Título Mostrado' }, snapshot_date: { label: 'Fecha de instantánea', help: 'El día en que se capturó esta instantánea.' }, source: { diff --git a/src/sales/translations/ja-JP/objects.pipeline.ts b/src/sales/translations/ja-JP/objects.pipeline.ts index 2d140795f..611075eca 100644 --- a/src/sales/translations/ja-JP/objects.pipeline.ts +++ b/src/sales/translations/ja-JP/objects.pipeline.ts @@ -445,7 +445,7 @@ export const pipeline: Record = { help: '通常は「期間」と「期間開始」から自動的に算出されます。手動で入力する場合は、その期間の最終日である必要があります — 例: 2026-07-01 開始の四半期なら 2026-09-30、2026 年 8 月なら 2026-08-31。', }, period_label: { - label: '期間', + label: '期間ラベル', help: '「2026 年第 3 四半期」「2026 年 8 月」のような読みやすい期間ラベル。', }, display_title: { label: '表示名' }, diff --git a/src/sales/translations/zh-CN/app.ts b/src/sales/translations/zh-CN/app.ts index 970c12663..cbc232d3e 100644 --- a/src/sales/translations/zh-CN/app.ts +++ b/src/sales/translations/zh-CN/app.ts @@ -27,7 +27,13 @@ export const appSurface: Omit = { description: '涵盖销售、服务和市场营销的客户关系管理系统', // Keyed by navigation-node `id` (a flat keyspace regardless of depth). navigation: { - group_activity: { label: '活动' }, + // The group and its Events item are two words, as in every other pack + // (Activity / Events, Actividad / Eventos, 活動 / イベント). The item + // keeps 活动: it is `crm_event`'s own label, the heading of the page it + // opens. The group takes 互动, the word this pack already uses for the + // interactions it gathers (`crm_event`'s description, the 互动历史 + // view, the Sales Activity tiles). #2060 item 13. + group_activity: { label: '互动' }, nav_event: { label: '活动' }, nav_activity_dashboard: { label: '销售活动' }, nav_my_calendar: { label: '我的日历' }, diff --git a/test/locale-label-collisions.test.ts b/test/locale-label-collisions.test.ts new file mode 100644 index 000000000..023c24b62 --- /dev/null +++ b/test/locale-label-collisions.test.ts @@ -0,0 +1,69 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import { describe, it, expect } from 'vitest'; +import { type AnyRec, localePacks, packFor, servedApp } from './helpers/composed-stack'; +import { CrmApp } from '../src/sales/apps/crm.app'; +import { Event } from '../src/sales/objects/event.object'; +import { Forecast } from '../src/sales/objects/forecast.object'; + +/** + * Two things a user reads side by side carry two labels, in every locale + * (#2060 item 13, findings C-16 and the forecast fold). + * + * - **The Activity group and its items.** zh-CN labelled the group and its + * Events item both 活动, so the sidebar read 活动 › 活动. Every other pack + * names the group by the activity it gathers and the item by the object it + * opens: the item's label is the heading of `crm_event`'s list page, its + * plural label. The item keeps that, so the fix renamed the group. + * - **The two period fields of the New Forecast form.** en, es-ES and ja-JP gave + * `period` (the Month / Quarter picklist) and `period_label` (the free-text + * name) one label, so the form showed "Period" twice, while the field itself + * declares "Period Label". + * + * Only these pairs are asserted. ⛔ This is not a rule that no two labels may + * collide anywhere: that would be a lint, and lint is the platform's. + */ + +type Locale = string; +const LOCALES: Locale[] = localePacks.map(([locale]) => locale); +const pack = (locale: Locale): AnyRec => packFor(locale) ?? {}; + +const APP = servedApp((CrmApp as AnyRec).name as string); +const ACTIVITY = ((APP.navigation ?? []) as AnyRec[]).find((entry) => entry?.id === 'group_activity'); +const navLabel = (locale: Locale, entry: AnyRec): string => + pack(locale).apps?.[APP.name]?.navigation?.[entry.id]?.label ?? entry.label; + +const forecastLabel = (locale: Locale, field: 'period' | 'period_label'): string => + pack(locale).objects?.crm_forecast?.fields?.[field]?.label ?? (Forecast as AnyRec).fields[field].label; + +describe('locale label collisions (#2060 item 13)', () => { + it('reads the four shipped packs and the Activity group', () => { + expect(LOCALES).toEqual(expect.arrayContaining(['en', 'zh-CN', 'ja-JP', 'es-ES'])); + expect(ACTIVITY?.type).toBe('group'); + expect((ACTIVITY?.children ?? []).map((child: AnyRec) => child.id)).toContain('nav_event'); + }); + + it.each(LOCALES)('%s: the Activity group shares its label with none of its items', (locale) => { + const group = navLabel(locale, ACTIVITY!); + const clashes = ((ACTIVITY!.children ?? []) as AnyRec[]) + .filter((child) => navLabel(locale, child) === group) + .map((child) => child.id); + expect(clashes, `the group and these items both read "${group}"`).toEqual([]); + }); + + it.each(LOCALES)('%s: the Events item reads as the page it opens', (locale) => { + const item = ((ACTIVITY!.children ?? []) as AnyRec[]).find((child) => child.id === 'nav_event')!; + const heading = pack(locale).objects?.crm_event?.pluralLabel ?? (Event as AnyRec).pluralLabel; + expect(navLabel(locale, item)).toBe(heading); + }); + + it.each(LOCALES)('%s: the forecast form labels Period and Period Label apart', (locale) => { + expect(forecastLabel(locale, 'period_label')).not.toBe(forecastLabel(locale, 'period')); + }); + + it('en: the two period fields read as they are declared', () => { + for (const field of ['period', 'period_label'] as const) { + expect(forecastLabel('en', field)).toBe((Forecast as AnyRec).fields[field].label); + } + }); +});