Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/2060-item-13-locale-labels.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 1 addition & 1 deletion content/docs/sales/meetings-and-calls.zh-Hans.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ description: 在任意记录上记录电话、记录会议或预约会议——

## 到哪里找活动记录

侧边栏里只有**活动 › 活动**这一个入口;查看这些记录的每一种方式,都是它打开的那个页面顶部的一个标签页,标签上写的就是视图自己的名字:
侧边栏里只有**互动 › 活动**这一个入口;查看这些记录的每一种方式,都是它打开的那个页面顶部的一个标签页,标签上写的就是视图自己的名字:

| 标签页 | 显示什么 |
| --- | --- |
Expand Down
2 changes: 1 addition & 1 deletion src/sales/translations/en/objects.pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ export const pipeline: Record<string, ObjectTranslationData> = {
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',
Expand Down
2 changes: 1 addition & 1 deletion src/sales/translations/es-ES/objects.pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,7 @@ export const pipeline: Record<string, ObjectTranslationData> = {
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: {
Expand Down
2 changes: 1 addition & 1 deletion src/sales/translations/ja-JP/objects.pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ export const pipeline: Record<string, ObjectTranslationData> = {
help: '通常は「期間」と「期間開始」から自動的に算出されます。手動で入力する場合は、その期間の最終日である必要があります — 例: 2026-07-01 開始の四半期なら 2026-09-30、2026 年 8 月なら 2026-08-31。',
},
period_label: {
label: '期間',
label: '期間ラベル',
help: '「2026 年第 3 四半期」「2026 年 8 月」のような読みやすい期間ラベル。',
},
display_title: { label: '表示名' },
Expand Down
8 changes: 7 additions & 1 deletion src/sales/translations/zh-CN/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@ export const appSurface: Omit<TranslationData, 'objects'> = {
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: '我的日历' },
Expand Down
69 changes: 69 additions & 0 deletions test/locale-label-collisions.test.ts
Original file line number Diff line number Diff line change
@@ -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);
}
});
});
Loading