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
38 changes: 38 additions & 0 deletions .changeset/6310-designer-formula-key-retired.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
'@object-ui/i18n': patch
'@object-ui/plugin-designer': patch
---

`appDesigner.fieldDesigner.formula` is retired — one row removed from each of the ten
locale packs plus the designer defaults map, 11 lines, zero readers (objectui#6310).

objectui#6043 retired the Field Designer's formula-expression textarea, which was the
key's only call site (`FieldDesigner.tsx`, the `{ name: 'formula', label:
t('appDesigner.fieldDesigner.formula') }` field descriptor). The value outlived it in
eleven places: `DESIGNER_DEFAULT_TRANSLATIONS` in
`packages/plugin-designer/src/hooks/useDesignerTranslation.ts`, and the `appDesigner >
fieldDesigner > formula` leaf of `packages/i18n/src/locales/{en,de,es,fr,pt,ru,ja,ko,zh,ar}.ts`.

Removed under objectui#4658's evidence standard, re-measured on this branch rather than
inherited from the card: zero `t()`/`tt()` call sites, no dynamic template head that could
reach it (`appDesigner.fieldDesigner.typeCategory.` is the namespace's only one), and its
sole textual occurrence anywhere in the repo was the defaults-map row this change removes
with it — so the key goes from NEEDS-REVIEW to no footprint at all.

The map and all ten packs move in one commit, which is what keeps
`defaults-maps-mirror-en-pack` green: that gate fails a map row whose key the `en` pack
lacks, and `all-locales-key-parity` fails a pack left behind.

Not touched: `designer.field.formula` (`'Formula (CEL)'`) in
`packages/app-shell/src/views/metadata-admin/i18n.ts`, a different and live key belonging
to metadata-admin's `ObjectFieldInspector` — the surface that still authors formula
expressions.

`packages/i18n/src/__tests__/appDesigner-fieldDesigner-formula-retired-6310.test.ts` pins the
removal by name, following the four prior retirements (objectui#4145, objectui#4392,
objectui#4730, objectui#5504). Every i18n gate here runs call site → key, so none of them can
see a dead key come BACK into the packs: the reverse sweep that found this one is report-only
by design, `all-locales-key-parity` is fully satisfied by ten packs agreeing on a dead key, and
`check:i18n-drift` only fires when a value changes. Reverse-verified rather than asserted —
reviving the row in all ten packs turns exactly that one case red, naming each pack, while the
parity gate and the defaults-map mirror stay green.
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* `appDesigner.fieldDesigner.formula` is retired from all ten packs, and the
* Field Designer vocabulary it sat in must keep naming real controls
* (objectui#6310).
*
* ## What was removed and why
*
* One row per pack (`en: 'Formula'`, `zh: '公式'`, `ru: 'Формула'`, …). objectui#6043
* retired the Field Designer's formula-expression textarea, which held the key's
* ONLY call site — the `{ name: 'formula', label:
* t('appDesigner.fieldDesigner.formula') }` field descriptor in
* `FieldDesigner.tsx`. The label outlived the control it labelled by eleven
* files: the ten packs, plus the `DESIGNER_DEFAULT_TRANSLATIONS` row that mirrors
* them.
*
* ## Why this pin is NEGATIVE, and why it is needed at all
*
* Every i18n gate in this repo runs **call site -> key**, never key -> call site
* (objectui#4145's mechanism, restated by objectui#4392, objectui#4730 and
* objectui#5504):
*
* - `scripts/check-i18n-call-site-keys.mjs` asks whether each call site's key
* resolves in `en`. A key with no call site is never visited.
* - `all-locales-key-parity.test.ts` compares the ten packs' key SETS to each
* other. One dead key present in all ten is exactly what it wants.
* - `scripts/check-i18n-en-drift.mjs` only fires when an `en` value CHANGES.
* - `scripts/check-i18n-dead-keys.mjs` IS the reverse direction, and it is
* report-only by design and wired into no workflow (objectui#4658).
*
* So the retired row can return to all ten packs with every gate green, and a
* translator filling in "the missing formula label" is a plausible way for that
* to happen — the surrounding vocabulary still describes a field editor, so the
* gap reads like an oversight rather than a decision. Restoring it goes red here.
*
* ## The other half of the retirement is guarded elsewhere, on purpose
*
* The `DESIGNER_DEFAULT_TRANSLATIONS` row is pinned by
* `app-shell/src/__tests__/defaults-maps-mirror-en-pack.test.tsx` (objectui#4401),
* which fails any map row whose key the `en` pack lacks. This file cannot assert
* it: `@object-ui/plugin-designer` depends on `@object-ui/i18n`, so importing the
* map back into this package inverts the dependency — the same reason #4401's
* gate lives in `app-shell` and `gantt-count-interpolation-4157.test.ts` asserts
* `en` values as literals. The two halves interlock: re-add the pack key and this
* file reds; re-add the map row alone and #4401's gate reds.
*
* ## What this file does NOT claim
*
* - **`designer.field.formula` (`'Formula (CEL)'`) is a DIFFERENT, LIVE key** —
* metadata-admin's `ObjectFieldInspector`, the surface that still authors
* formula expressions, reads it from
* `packages/app-shell/src/views/metadata-admin/i18n.ts`. It is not in the
* locale packs and is out of this file's reach by the same dependency
* direction as above. Two keys end in `.formula`; only the
* `appDesigner.fieldDesigner.*` spelling is retired. Grep the full dotted path.
* - **`appDesigner.fieldDesigner` is a live namespace.** Only the one leaf went;
* {@link SURVIVING} exists so a green here cannot be bought by deleting the
* neighbourhood.
* - **{@link SURVIVING} is deliberately not "the rows next to it".** The row that
* followed the retired one (`options`) is absent from that list, along with
* `addOption`, `addRule`, `noFields`, `searchPlaceholder`, `systemBadge`,
* `ungrouped` and `validationRules`: objectui#6310 measured all eight as
* NEEDS-REVIEW candidates of the same reverse sweep, with the same
* defaults-map-only footprint the retired key had, and none of them has been
* individually confirmed either way (that is objectui#4730's job). Pinning
* "these survive" against keys that may themselves be dead would bake an
* unverified claim into the guard. Every key in {@link SURVIVING} was confirmed
* live by call site in `packages/plugin-designer/src/FieldDesigner.tsx`.
*/
import { describe, it, expect } from 'vitest';
import { builtInLocales } from '../locales/index';

type LocaleCode = keyof typeof builtInLocales;
const LANGS = Object.keys(builtInLocales) as LocaleCode[];

const at = (pack: unknown, path: string): unknown =>
path.split('.').reduce<unknown>((n, k) => (n as Record<string, unknown> | undefined)?.[k], pack);

/** The retired leaf, named rather than counted. */
const RETIRED = 'appDesigner.fieldDesigner.formula';

/**
* Rows the deletion swept around — each one confirmed live by a `t()` call site
* in `FieldDesigner.tsx`, not merely by sitting nearby. `referenceTo` is the row
* that immediately PRECEDED the retired one in every pack, and
* `typeSpecificSection` is the drawer section the formula textarea used to be
* rendered in, so a sweep that over-reached would land on them first.
*/
const SURVIVING = [
'appDesigner.fieldDesigner.referenceTo',
'appDesigner.fieldDesigner.typeSpecificSection',
'appDesigner.fieldDesigner.defaultValue',
'appDesigner.fieldDesigner.placeholder',
'appDesigner.fieldDesigner.fieldType',
'appDesigner.fieldDesigner.title',
'appDesigner.fieldDesigner.allTypes',
] as const;

/**
* The namespace's only dynamically built family —
* ``t(`appDesigner.fieldDesigner.typeCategory.${cat}`)`` at
* `FieldDesigner.tsx`. No member is spelled literally at any call site, so a
* future reverse sweep reading only literal arguments is precisely where these
* would look dead. They are the live half of the same namespace and are pinned
* by name here.
*/
const TYPE_CATEGORY = [
'appDesigner.fieldDesigner.typeCategory.text',
'appDesigner.fieldDesigner.typeCategory.number',
'appDesigner.fieldDesigner.typeCategory.date',
'appDesigner.fieldDesigner.typeCategory.choice',
'appDesigner.fieldDesigner.typeCategory.relation',
'appDesigner.fieldDesigner.typeCategory.advanced',
] as const;

describe('`appDesigner.fieldDesigner.formula` is retired from the ten packs (objectui#6310)', () => {
it('covers all ten packs and a live `appDesigner.fieldDesigner` root', () => {
// Guards the premise the rest of the file rests on: a pin that iterates an
// empty pack list, or asserts absence inside a namespace that itself
// vanished, is green for the wrong reason.
expect(LANGS).toHaveLength(10);
for (const lang of LANGS) {
const root = at(builtInLocales[lang], 'appDesigner.fieldDesigner');
expect(root, `${lang} lost the appDesigner.fieldDesigner root`).toBeDefined();
expect(Object.keys(root as Record<string, unknown>).length, lang).toBeGreaterThanOrEqual(20);
}
});

it('no pack defines the retired formula label', () => {
const revived: string[] = [];
for (const lang of LANGS) {
if (at(builtInLocales[lang], RETIRED) !== undefined) revived.push(`${lang} :: ${RETIRED}`);
}
// Named, not counted: a half-reverted retirement is repaired pack by pack.
expect(
revived,
'The retired Field Designer formula label is back in a locale pack. It ' +
'labelled a formula-expression textarea that objectui#6043 removed, so ' +
'nothing reads it — and no other i18n gate can see a dead key return, ' +
'because every one of them runs call site -> key (objectui#6310). If a ' +
'formula control is being reintroduced to the Field Designer, author ' +
'its label alongside the control rather than restoring this row. The ' +
'live key for the surface that DOES author formula expressions is ' +
'`designer.field.formula` in ' +
'packages/app-shell/src/views/metadata-admin/i18n.ts — a different key, ' +
'not this one.',
).toEqual([]);
});

it('the deletion swept around its neighbours', () => {
for (const lang of LANGS) {
for (const key of SURVIVING) {
const value = at(builtInLocales[lang], key);
expect(typeof value, `${lang} :: ${key}`).toBe('string');
expect((value as string).length, `${lang} :: ${key}`).toBeGreaterThan(0);
}
}
});

it('keeps every member of the dynamically built `typeCategory` family', () => {
// `all-locales-key-parity` would be satisfied by all ten packs dropping a
// member together, and no literal-argument scan can see these are read at
// all. State the vocabulary here, by name, so the substitution cannot be
// left pointing at a key nothing defines.
const missing: string[] = [];
for (const lang of LANGS) {
for (const key of TYPE_CATEGORY) {
if (typeof at(builtInLocales[lang], key) !== 'string') missing.push(`${lang} :: ${key}`);
}
}
expect(
missing,
'a member of the `appDesigner.fieldDesigner.typeCategory.*` family is gone; ' +
'FieldDesigner.tsx builds these keys by substitution, so the loss renders ' +
'as a raw key in the type filter rather than failing any other gate',
).toEqual([]);
});
});
1 change: 0 additions & 1 deletion packages/i18n/src/locales/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1360,7 +1360,6 @@ const ar = {
defaultValue: "القيمة الافتراضية",
placeholder: "نص تلميحي",
referenceTo: "مرجع إلى",
formula: "صيغة",
options: "خيارات",
addOption: "إضافة خيار",
validationRules: "قواعد التحقق",
Expand Down
1 change: 0 additions & 1 deletion packages/i18n/src/locales/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1353,7 +1353,6 @@ const de = {
defaultValue: "Standardwert",
placeholder: "Platzhalter",
referenceTo: "Verweis auf",
formula: "Formel",
options: "Optionen",
addOption: "Option hinzufügen",
validationRules: "Validierungsregeln",
Expand Down
1 change: 0 additions & 1 deletion packages/i18n/src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1522,7 +1522,6 @@ const en = {
defaultValue: 'Default Value',
placeholder: 'Placeholder',
referenceTo: 'Reference To',
formula: 'Formula',
options: 'Options',
addOption: 'Add Option',
validationRules: 'Validation Rules',
Expand Down
1 change: 0 additions & 1 deletion packages/i18n/src/locales/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1357,7 +1357,6 @@ const es = {
defaultValue: "Valor predeterminado",
placeholder: "Marcador de posición",
referenceTo: "Referencia a",
formula: "Fórmula",
options: "Opciones",
addOption: "Agregar opción",
validationRules: "Reglas de validación",
Expand Down
1 change: 0 additions & 1 deletion packages/i18n/src/locales/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1355,7 +1355,6 @@ const fr = {
defaultValue: "Valeur par défaut",
placeholder: "Espace réservé",
referenceTo: "Référence à",
formula: "Formule",
options: "Options",
addOption: "Ajouter une option",
validationRules: "Règles de validation",
Expand Down
1 change: 0 additions & 1 deletion packages/i18n/src/locales/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1353,7 +1353,6 @@ const ja = {
defaultValue: "デフォルト値",
placeholder: "プレースホルダー",
referenceTo: "参照先",
formula: "数式",
options: "オプション",
addOption: "オプションを追加",
validationRules: "検証ルール",
Expand Down
1 change: 0 additions & 1 deletion packages/i18n/src/locales/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1353,7 +1353,6 @@ const ko = {
defaultValue: "기본값",
placeholder: "자리 표시자",
referenceTo: "참조 대상",
formula: "수식",
options: "옵션",
addOption: "옵션 추가",
validationRules: "유효성 검사 규칙",
Expand Down
1 change: 0 additions & 1 deletion packages/i18n/src/locales/pt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1352,7 +1352,6 @@ const pt = {
defaultValue: "Valor padrão",
placeholder: "Espaço reservado",
referenceTo: "Referência a",
formula: "Fórmula",
options: "Opções",
addOption: "Adicionar opção",
validationRules: "Regras de validação",
Expand Down
1 change: 0 additions & 1 deletion packages/i18n/src/locales/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1363,7 +1363,6 @@ const ru = {
defaultValue: "Значение по умолчанию",
placeholder: "Заполнитель",
referenceTo: "Ссылка на",
formula: "Формула",
options: "Параметры",
addOption: "Добавить параметр",
validationRules: "Правила валидации",
Expand Down
1 change: 0 additions & 1 deletion packages/i18n/src/locales/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1418,7 +1418,6 @@ const zh = {
defaultValue: '默认值',
placeholder: '占位文本',
referenceTo: '引用对象',
formula: '公式',
options: '选项',
addOption: '添加选项',
validationRules: '验证规则',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,6 @@ export const DESIGNER_DEFAULT_TRANSLATIONS: Record<string, string> = {
'appDesigner.fieldDesigner.defaultValue': 'Default Value',
'appDesigner.fieldDesigner.placeholder': 'Placeholder',
'appDesigner.fieldDesigner.referenceTo': 'Reference To',
'appDesigner.fieldDesigner.formula': 'Formula',
'appDesigner.fieldDesigner.options': 'Options',
'appDesigner.fieldDesigner.addOption': 'Add Option',
'appDesigner.fieldDesigner.validationRules': 'Validation Rules',
Expand Down
Loading