From 7da77292a1375f14dadb0159015cbdb1aec66168 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 06:28:34 +0000 Subject: [PATCH] fix(app-shell): the translation preview draws the groups of the schema the designer validates against (objectui#11765) The preview's CATEGORIES table had drifted from the per-app TranslationDataSchema, the groups validateMetadataDraft('translation', ...) judges a draft by (TranslationItemSchema). It omitted picklists, datasets, pages, flows and settingsCommon, so a spec-valid bundle of only one of them read as empty, and it listed the refused validationMessages and settings, which both counted toward the coverage denominator. The table is now one row per schema group, in the schema's order, typed with satisfies against the schema's shape keys so a group added or dropped upstream fails type-check. Coverage divides by that count. The headings of the five new groups are rows of the designer's own string table (en and zh); the two dropped rows are removed. The nested key-count row reads the singular for one key (keyCountOne / keyCountOther). Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude --- .../11765-translation-preview-categories.md | 11 ++ .../src/views/metadata-admin/i18n.ts | 21 ++- ...anslationPreview.categories-11765.test.tsx | 177 ++++++++++++++++++ ...lationPreview.globalActions-11755.test.tsx | 4 +- .../previews/TranslationPreview.tsx | 77 +++++--- .../adminPreviews.i18n-10862-s2.test.tsx | 11 +- 6 files changed, 261 insertions(+), 40 deletions(-) create mode 100644 .changeset/11765-translation-preview-categories.md create mode 100644 packages/app-shell/src/views/metadata-admin/previews/TranslationPreview.categories-11765.test.tsx diff --git a/.changeset/11765-translation-preview-categories.md b/.changeset/11765-translation-preview-categories.md new file mode 100644 index 0000000000..34fa916897 --- /dev/null +++ b/.changeset/11765-translation-preview-categories.md @@ -0,0 +1,11 @@ +--- +'@object-ui/app-shell': patch +--- + +The translation designer's preview draws the groups of the schema it validates a bundle against (objectui#11765). + +The designer judges a translation draft by the per-app `TranslationDataSchema` groups, but the preview drew a hand-kept list that had drifted from them. It left out `picklists`, `datasets`, `pages`, `flows` and `settingsCommon`, so a spec-valid bundle of only one of those groups read as empty. It also listed `validationMessages`, which the spec removed, and `settings`, which only the platform schema declares. Both counted toward the coverage denominator, so a spec-valid bundle could never reach full coverage. + +The preview now draws one card per schema group, in the schema's order, and divides coverage by that count. A key the schema refuses is not drawn and not counted. A nested entry of a single key now reads `{1 key}` rather than `{1 keys}`. + +Nothing is added to the package entry: no export, prop, type member or language-pack key. The new headings are rows of the designer's own string table. diff --git a/packages/app-shell/src/views/metadata-admin/i18n.ts b/packages/app-shell/src/views/metadata-admin/i18n.ts index 87ec80a2ae..f82bfd79ab 100644 --- a/packages/app-shell/src/views/metadata-admin/i18n.ts +++ b/packages/app-shell/src/views/metadata-admin/i18n.ts @@ -1626,19 +1626,24 @@ const ENGINE_STRINGS_EN: Record = { 'The installed packages could not be read, so the extensions of this picklist are unknown.', 'engine.translationPreview.empty': 'This bundle is empty — add at least one translated string to see the coverage report.', + // One heading per group of the per-app `TranslationDataSchema`, in its order (objectui#11765). 'engine.translationPreview.category.objects': 'Objects', + 'engine.translationPreview.category.picklists': 'Picklists', 'engine.translationPreview.category.apps': 'Apps', 'engine.translationPreview.category.messages': 'Messages', - 'engine.translationPreview.category.validationMessages': 'Validation Messages', 'engine.translationPreview.category.globalActions': 'Global Actions', 'engine.translationPreview.category.dashboards': 'Dashboards', - 'engine.translationPreview.category.settings': 'Settings', + 'engine.translationPreview.category.datasets': 'Datasets', + 'engine.translationPreview.category.pages': 'Pages', + 'engine.translationPreview.category.flows': 'Flows', 'engine.translationPreview.category.metadataForms': 'Metadata Forms', + 'engine.translationPreview.category.settingsCommon': 'Settings UI', 'engine.translationPreview.coverage': 'Category coverage:', 'engine.translationPreview.totalKeys': '{count} total keys', 'engine.translationPreview.categoryEmpty': 'empty', 'engine.translationPreview.more': '+{count} more…', - 'engine.translationPreview.keyCount': '{{count} keys}', + 'engine.translationPreview.keyCountOne': '{{count} key}', + 'engine.translationPreview.keyCountOther': '{{count} keys}', // objectui#10862 (slice 2) — the two advisories `celAuthoring.lintCelPredicate` // words itself, shown beneath the CEL editors: the wrong-layer root (the // local instrument's sentence; a slot `@objectstack/lint` covers ships that @@ -4645,18 +4650,22 @@ const ENGINE_STRINGS_ZH: Record = { 'engine.picklistPreview.extensionsFailed': '无法读取已安装的软件包,因此不知道该选项列表有哪些扩展。', 'engine.translationPreview.empty': '此语言包为空 —— 至少添加一条译文,即可查看覆盖率报告。', 'engine.translationPreview.category.objects': '对象', + 'engine.translationPreview.category.picklists': '选项列表', 'engine.translationPreview.category.apps': '应用', 'engine.translationPreview.category.messages': '消息', - 'engine.translationPreview.category.validationMessages': '校验消息', 'engine.translationPreview.category.globalActions': '全局操作', 'engine.translationPreview.category.dashboards': '仪表盘', - 'engine.translationPreview.category.settings': '设置', + 'engine.translationPreview.category.datasets': '数据集', + 'engine.translationPreview.category.pages': '页面', + 'engine.translationPreview.category.flows': '流程', 'engine.translationPreview.category.metadataForms': '元数据表单', + 'engine.translationPreview.category.settingsCommon': '设置界面', 'engine.translationPreview.coverage': '分类覆盖率:', 'engine.translationPreview.totalKeys': '共 {count} 个键', 'engine.translationPreview.categoryEmpty': '空', 'engine.translationPreview.more': '另有 {count} 项…', - 'engine.translationPreview.keyCount': '{{count} 个键}', + 'engine.translationPreview.keyCountOne': '{{count} 个键}', + 'engine.translationPreview.keyCountOther': '{{count} 个键}', 'engine.celLint.notTheRow': '`{identifier}` 在此处不是记录行:行条件只把记录绑定为 `{canonical}`,别无其他。CEL 作用域词汇仍接受 `{identifier}`,所以这里不会阻止保存,但运行时表达式会以 `Unknown variable: {identifier}` 报错,规则永远不会触发。请改为以 `{canonical}` 为根引用。', 'engine.celLint.notPushdownable': diff --git a/packages/app-shell/src/views/metadata-admin/previews/TranslationPreview.categories-11765.test.tsx b/packages/app-shell/src/views/metadata-admin/previews/TranslationPreview.categories-11765.test.tsx new file mode 100644 index 0000000000..c038d5454f --- /dev/null +++ b/packages/app-shell/src/views/metadata-admin/previews/TranslationPreview.categories-11765.test.tsx @@ -0,0 +1,177 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * objectui#11765 — the translation preview's categories are the groups of the + * schema the designer validates against. + * + * The designer judges a translation draft with `validateMetadataDraft`, which + * binds `TranslationItemSchema`: the groups of the per-app + * `TranslationDataSchema` plus the item's own keys. The preview used to draw a + * hand-kept list that had drifted from those groups. It omitted `picklists`, + * `datasets`, `pages`, `flows` and `settingsCommon`, so a spec-valid bundle of + * only one of them read as empty. It listed `validationMessages` (removed by + * the spec) and `settings` (a platform-only group the per-app schema refuses), + * so both counted toward the coverage denominator and a spec-valid bundle could + * never reach full coverage. + * + * Every spec-valid fixture here is parsed by the installed spec's + * `TranslationDataSchema` and passed through the designer's own + * `validateMetadataDraft('translation', …)` before it is mounted. The expected + * groups, their order and their count are read from `TranslationDataSchema` + * itself, so a spec group added or dropped turns these pins red instead of + * drifting. Headings and count rows are read back through `t` / `tFormat` + * rather than restated. + */ + +import * as React from 'react'; +import { describe, it, expect, afterEach } from 'vitest'; +import { render, cleanup, screen } from '@testing-library/react'; +import { TranslationDataSchema } from '@objectstack/spec/system'; + +import { validateMetadataDraft } from '../clientValidation'; +import { t, tFormat } from '../i18n'; +import { TranslationPreview } from './TranslationPreview'; + +afterEach(cleanup); + +const LOCALE = 'en-US'; + +/** The groups the spec declares, in its own order. */ +const GROUPS = Object.keys(TranslationDataSchema.shape); + +/** One spec-valid entry per group, the entry's key, and how its sample row reads. */ +const ONE_ENTRY: Record; key: string; row: () => string }> = { + objects: { data: { account: { label: 'Compte' } }, key: 'account', row: () => keys(1) }, + picklists: { + data: { stage: { label: 'Étape', options: { open: 'Ouvert', won: 'Gagné' } } }, + key: 'stage', + row: () => keys(2), + }, + apps: { data: { crm: { label: 'CRM' } }, key: 'crm', row: () => keys(1) }, + messages: { data: { welcome: 'Bienvenue' }, key: 'welcome', row: () => '"Bienvenue"' }, + globalActions: { data: { portfolio_snapshot: { label: 'Instantané' } }, key: 'portfolio_snapshot', row: () => '"Instantané"' }, + dashboards: { data: { pipeline: { label: 'Pipeline' } }, key: 'pipeline', row: () => keys(1) }, + datasets: { + data: { revenue: { label: 'Revenu', measures: { amount: { label: 'Montant' } } } }, + key: 'revenue', + row: () => keys(2), + }, + pages: { data: { home: { label: 'Accueil' } }, key: 'home', row: () => keys(1) }, + flows: { data: { onboarding: { label: 'Intégration' } }, key: 'onboarding', row: () => keys(1) }, + metadataForms: { data: { object: { label: 'Objet' } }, key: 'object', row: () => keys(1) }, + // `settingsCommon` is one strict object, not a record of author-named nodes: + // its members (`sourceLabels`) are what the card counts and samples. + settingsCommon: { + data: { sourceLabels: { env: 'Environnement', tenant: 'Locataire' } }, + key: 'sourceLabels', + row: () => keys(2), + }, +}; + +/** The nested sample row for a node of `n` keys, in the row the preview picks for `n`. */ +function keys(n: number): string { + return tFormat(n === 1 ? 'engine.translationPreview.keyCountOne' : 'engine.translationPreview.keyCountOther', LOCALE, { + count: n, + }); +} + +function heading(group: string): string { + return t(`engine.translationPreview.category.${group}`, LOCALE); +} + +/** A bundle the spec and the designer both accept, mounted in the real preview as the designer hands it a draft. */ +async function mountSpecValid(data: Record) { + const parsed = TranslationDataSchema.safeParse(data); + expect(parsed.success, JSON.stringify(parsed.error?.issues)).toBe(true); + const draft = { name: 'fr', locale: 'fr-FR', ...data }; + const gate = await validateMetadataDraft('translation', draft); + expect(gate, 'the designer accepts the draft').toEqual({ ok: true, issues: [] }); + render(); +} + +/** The heading of every category card, in the order the preview draws them. */ +function cardHeadings(): string[] { + const grid = document.querySelector('.grid'); + expect(grid, 'the category grid').toBeTruthy(); + return Array.from(grid!.children).map((card) => card.querySelector('span')?.textContent ?? ''); +} + +/** The card headed by `group`'s row. */ +function card(group: string): HTMLElement { + const el = screen.getByText(heading(group)).closest('.rounded.border') as HTMLElement | null; + expect(el, `${group}: the category card`).toBeTruthy(); + return el!; +} + +/** The value text of the sample row for `key` in `group`'s card. */ +function sampleValue(group: string, key: string): string { + const row = Array.from(card(group).querySelectorAll('li')).find((li) => li.querySelector('code')?.textContent === key); + expect(row, `${group}: a sample row for ${key}`).toBeTruthy(); + return row!.querySelector('span')?.textContent ?? ''; +} + +function coverage(populated: number): string { + return `${populated}/${GROUPS.length} (${Math.round((populated / GROUPS.length) * 100)}%)`; +} + +describe('TranslationPreview draws the groups of the schema the designer validates against (objectui#11765)', () => { + it('every group the spec declares has a spec-valid fixture here', () => { + expect(Object.keys(ONE_ENTRY).sort()).toEqual([...GROUPS].sort()); + }); + + it('draws one card per schema group, in the schema order, and divides coverage by their count', async () => { + await mountSpecValid({ messages: ONE_ENTRY.messages.data }); + expect(cardHeadings()).toEqual(GROUPS.map(heading)); + expect(screen.getByText(coverage(1))).toBeTruthy(); + }); + + for (const group of GROUPS) { + it(`a spec-valid bundle of only ${group} reads non-empty and shows its card`, async () => { + const entry = ONE_ENTRY[group]; + await mountSpecValid({ [group]: entry.data }); + expect(screen.queryByText(t('engine.translationPreview.empty', LOCALE))).toBeNull(); + expect(sampleValue(group, entry.key)).toBe(entry.row()); + expect(screen.getByText(coverage(1))).toBeTruthy(); + }); + } + + it('a bundle carrying every group reaches full coverage', async () => { + await mountSpecValid(Object.fromEntries(GROUPS.map((g) => [g, ONE_ENTRY[g].data]))); + expect(screen.getByText(coverage(GROUPS.length))).toBeTruthy(); + expect(screen.queryByText(t('engine.translationPreview.categoryEmpty', LOCALE))).toBeNull(); + }); + + it('a group the schema refuses is neither drawn as a category nor counted', async () => { + const refused = { validationMessages: { required: 'Requis' }, settings: { theme: 'Thème' } }; + for (const [group, value] of Object.entries(refused)) { + expect(TranslationDataSchema.safeParse({ [group]: value }).success, `${group} is refused by the spec`).toBe(false); + const gate = await validateMetadataDraft('translation', { name: 'fr', locale: 'fr-FR', [group]: value }); + expect(gate.ok, `${group} is refused by the designer`).toBe(false); + } + render( + , + ); + expect(cardHeadings()).toEqual(GROUPS.map(heading)); + expect(screen.getByText(coverage(1))).toBeTruthy(); + expect(screen.queryByText('required')).toBeNull(); + expect(screen.queryByText('theme')).toBeNull(); + }); + + it('a nested node of one key reads the singular row, and of two keys the plural row', async () => { + expect(keys(1), 'the singular row is not the plural row read with 1').not.toBe(keys(2).replace('2', '1')); + await mountSpecValid({ pages: { home: { label: 'Accueil' }, about: { label: 'À propos', title: 'À propos de nous' } } }); + expect(sampleValue('pages', 'home')).toBe(keys(1)); + expect(sampleValue('pages', 'about')).toBe(keys(2)); + }); + + it('control: the messages card renders as before', async () => { + await mountSpecValid({ messages: { welcome: 'Bienvenue', saved: 'Enregistré' } }); + expect(sampleValue('messages', 'welcome')).toBe('"Bienvenue"'); + expect(sampleValue('messages', 'saved')).toBe('"Enregistré"'); + }); +}); diff --git a/packages/app-shell/src/views/metadata-admin/previews/TranslationPreview.globalActions-11755.test.tsx b/packages/app-shell/src/views/metadata-admin/previews/TranslationPreview.globalActions-11755.test.tsx index 92bc8f55c3..7eb0a32804 100644 --- a/packages/app-shell/src/views/metadata-admin/previews/TranslationPreview.globalActions-11755.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/previews/TranslationPreview.globalActions-11755.test.tsx @@ -67,7 +67,7 @@ describe('TranslationPreview draws a globalActions node by its label (objectui#1 it('a node without a label renders its inner key count', () => { mountSpecValid({ globalActions: { archive: { confirmText: 'Archive?', successMessage: 'Archived' } } }); expect(sampleValue(GLOBAL_ACTIONS, 'archive')).toBe( - tFormat('engine.translationPreview.keyCount', LOCALE, { count: 2 }), + tFormat('engine.translationPreview.keyCountOther', LOCALE, { count: 2 }), ); expect(document.body.textContent).not.toContain('[object Object]'); }); @@ -80,7 +80,7 @@ describe('TranslationPreview draws a globalActions node by its label (objectui#1 expect(sampleValue('engine.translationPreview.category.messages', 'welcome')).toBe('"Bienvenue"'); // An object node keeps its key count: only the globalActions category reads a node's label. expect(sampleValue('engine.translationPreview.category.objects', 'account')).toBe( - tFormat('engine.translationPreview.keyCount', LOCALE, { count: 2 }), + tFormat('engine.translationPreview.keyCountOther', LOCALE, { count: 2 }), ); }); }); diff --git a/packages/app-shell/src/views/metadata-admin/previews/TranslationPreview.tsx b/packages/app-shell/src/views/metadata-admin/previews/TranslationPreview.tsx index ca2d00a4bc..06de276d84 100644 --- a/packages/app-shell/src/views/metadata-admin/previews/TranslationPreview.tsx +++ b/packages/app-shell/src/views/metadata-admin/previews/TranslationPreview.tsx @@ -10,18 +10,21 @@ * 1. "Which locale is this and how complete is it?" → header strip * with the locale badge, total-key count, and an overall coverage * bar (categories that have at least one entry). - * 2. "Where are the strings?" → a card per category in - * `TranslationDataSchema`: objects · apps · messages · - * validationMessages · globalActions · dashboards · settings · - * metadataForms. Each card shows the count of top-level keys and + * 2. "Where are the strings?" → a card per group of the per-app + * `TranslationDataSchema`, the groups the designer's + * `validateMetadataDraft('translation', …)` judges a draft by + * (objectui#11765). Each card shows the count of top-level keys and * a sample of up to 5 keys so the user can confirm the right - * bundle is loaded. + * bundle is loaded. A key the schema refuses (the removed + * `validationMessages`, the platform-only `settings`) is no group: + * it is neither drawn nor counted in the coverage denominator. * - * For flat string maps (messages, validationMessages, settings) we - * render a small key→value sample table. For nested objects (objects, - * apps, dashboards, metadataForms) we list the top-level keys with their - * inner key count. globalActions is nested too: each entry is an action - * translation node (`TranslationDataSchema.globalActions.NAME`), so its + * messages is the one flat string map, so its sample is a small key→value + * table. Every other group is nested, and its sample lists the top-level + * keys with their inner key count. settingsCommon is one strict object + * rather than a record of named nodes; its members (`sourceLabels`) are + * its top-level keys, read the same way. globalActions entries are action + * translation nodes (`TranslationDataSchema.globalActions.NAME`), so that * sample shows the node's `label`, quoted as a flat string is, and the * inner key count when the node carries no `label` (objectui#11755). */ @@ -30,6 +33,7 @@ import * as React from 'react'; import { AppWindow, ClipboardList, + Database, FileText, Gauge, Globe2, @@ -37,10 +41,13 @@ import { Languages, ListChecks, MessageCircle, + PanelsTopLeft, Settings2, - ShieldAlert, + SquareChevronDown, + Workflow, } from 'lucide-react'; import { EmptyDescription } from '@object-ui/components'; +import type { TranslationDataSchema } from '@objectstack/spec/system'; import type { MetadataPreviewProps } from '../preview-registry.js'; import { t as tr, tFormat } from '../i18n.js'; import { PreviewShell, PreviewMessage, PreviewErrorBoundary } from './PreviewShell.js'; @@ -61,27 +68,37 @@ interface CategoryDef { nodeLabel?: boolean; } -const CATEGORIES: CategoryDef[] = [ - { key: 'objects', label: 'engine.translationPreview.category.objects', icon: ListChecks, flat: false }, - { key: 'apps', label: 'engine.translationPreview.category.apps', icon: AppWindow, flat: false }, - { key: 'messages', label: 'engine.translationPreview.category.messages', icon: MessageCircle, flat: true }, - { - key: 'validationMessages', - label: 'engine.translationPreview.category.validationMessages', - icon: ShieldAlert, - flat: true, - }, - { - key: 'globalActions', +/** A group of the per-app `TranslationDataSchema`. */ +type TranslationGroup = keyof (typeof TranslationDataSchema)['shape']; + +/** + * One row per group of the per-app `TranslationDataSchema`, in the schema's + * order (objectui#11765). The schema cannot supply a group's icon or heading, + * so the rows are written here, and `satisfies` keeps them exhaustive both + * ways: a group the spec adds without a row here, or a row for a key the spec + * does not declare, fails type-check. `TranslationPreview.categories-11765.test.tsx` + * pins the same against the schema at runtime. + */ +const CATEGORY_DEFS = { + objects: { label: 'engine.translationPreview.category.objects', icon: ListChecks, flat: false }, + picklists: { label: 'engine.translationPreview.category.picklists', icon: SquareChevronDown, flat: false }, + apps: { label: 'engine.translationPreview.category.apps', icon: AppWindow, flat: false }, + messages: { label: 'engine.translationPreview.category.messages', icon: MessageCircle, flat: true }, + globalActions: { label: 'engine.translationPreview.category.globalActions', icon: ClipboardList, flat: false, nodeLabel: true, }, - { key: 'dashboards', label: 'engine.translationPreview.category.dashboards', icon: LayoutDashboard, flat: false }, - { key: 'settings', label: 'engine.translationPreview.category.settings', icon: Settings2, flat: true }, - { key: 'metadataForms', label: 'engine.translationPreview.category.metadataForms', icon: FileText, flat: false }, -]; + dashboards: { label: 'engine.translationPreview.category.dashboards', icon: LayoutDashboard, flat: false }, + datasets: { label: 'engine.translationPreview.category.datasets', icon: Database, flat: false }, + pages: { label: 'engine.translationPreview.category.pages', icon: PanelsTopLeft, flat: false }, + flows: { label: 'engine.translationPreview.category.flows', icon: Workflow, flat: false }, + metadataForms: { label: 'engine.translationPreview.category.metadataForms', icon: FileText, flat: false }, + settingsCommon: { label: 'engine.translationPreview.category.settingsCommon', icon: Settings2, flat: false }, +} satisfies Record>; + +const CATEGORIES: CategoryDef[] = Object.entries(CATEGORY_DEFS).map(([key, def]) => ({ key, ...def })); /** * `locale` is the designer's language, the one the preview's own words read @@ -217,7 +234,11 @@ function renderSampleValue( const nodeLabel = (v as Dict).label; if (cat.nodeLabel && typeof nodeLabel === 'string') return `"${nodeLabel}"`; const n = Object.keys(v as Dict).length; - return tFormat('engine.translationPreview.keyCount', locale, { count: n }); + return tFormat( + n === 1 ? 'engine.translationPreview.keyCountOne' : 'engine.translationPreview.keyCountOther', + locale, + { count: n }, + ); } return String(v ?? '∅'); } diff --git a/packages/app-shell/src/views/metadata-admin/previews/adminPreviews.i18n-10862-s2.test.tsx b/packages/app-shell/src/views/metadata-admin/previews/adminPreviews.i18n-10862-s2.test.tsx index 8266473f51..50fb14f69f 100644 --- a/packages/app-shell/src/views/metadata-admin/previews/adminPreviews.i18n-10862-s2.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/previews/adminPreviews.i18n-10862-s2.test.tsx @@ -438,19 +438,22 @@ describe('TranslationPreview reads the designer locale (objectui#10862)', () => { key: 'engine.translationPreview.coverage' }, { key: 'engine.translationPreview.totalKeys', vars: { count: 7 } }, { key: 'engine.translationPreview.category.objects' }, + { key: 'engine.translationPreview.category.picklists' }, { key: 'engine.translationPreview.category.apps' }, { key: 'engine.translationPreview.category.messages' }, - { key: 'engine.translationPreview.category.validationMessages' }, { key: 'engine.translationPreview.category.globalActions' }, { key: 'engine.translationPreview.category.dashboards' }, - { key: 'engine.translationPreview.category.settings' }, + { key: 'engine.translationPreview.category.datasets' }, + { key: 'engine.translationPreview.category.pages' }, + { key: 'engine.translationPreview.category.flows' }, { key: 'engine.translationPreview.category.metadataForms' }, + { key: 'engine.translationPreview.category.settingsCommon' }, { key: 'engine.translationPreview.categoryEmpty' }, { key: 'engine.translationPreview.more', vars: { count: 1 } }, - { key: 'engine.translationPreview.keyCount', vars: { count: 1 } }, + { key: 'engine.translationPreview.keyCountOne', vars: { count: 1 } }, ]); // The bundle's own locale is author data, not the designer's language. - expectAsWritten(['French', 'fr-FR', 'account', '"Un"', '2/8 (25%)']); + expectAsWritten(['French', 'fr-FR', 'account', '"Un"', '2/11 (18%)']); }); } });