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
22 changes: 16 additions & 6 deletions .changeset/8165-translatefn-one-authority.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
---
---

Re-point `saveAdvisoryToast`'s `TranslateFn` at its one authority,
`writeWarningToast` (objectui#8165), and shrink the `KNOWN_COLLISIONS` baseline
in `scripts/__tests__/one-authority-per-exported-name-6273.test.ts` by that
site. Type-level only: the declaration was byte-identical to the one it now
re-exports, the name is on no package's published face, and `export type { X }
from '…'` erases at build — so no package is released by this change.
At this change, re-point `saveAdvisoryToast`'s `TranslateFn` at its one
authority, `writeWarningToast` (objectui#8165), and shrink the
`KNOWN_COLLISIONS` baseline in
`scripts/__tests__/one-authority-per-exported-name-6273.test.ts` by that site.
Type-level only: the declaration was byte-identical to the one it re-exported,
at this change the name was on no package's published face, and
`export type { X } from '…'` erases at build — so no package is released by
this change.

⚠️ **Dated note, 2026-09-25 — the one authority has since moved to
`@object-ui/i18n` — objectui#8261.** Later in this same release `TranslateFn` is
declared once in `@object-ui/i18n` and published from that package's entry;
app-shell's toast modules and `@object-ui/fields`' `file-size-guard` re-export
it from there, and the `KNOWN_COLLISIONS` entry is gone. The rest of this
entry is kept as the reading of this change; the objectui#8261 entry states
what ships.
27 changes: 27 additions & 0 deletions .changeset/8261-translatefn-i18n-authority.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
'@object-ui/i18n': minor
'@object-ui/app-shell': patch
'@object-ui/fields': patch
---

`@object-ui/i18n` now publishes `TranslateFn` — i18next's `t` narrowed to
`(key: string, options?: Record<string, unknown>) => string` — as the one
authority for that name (objectui#8261, on the maintainer's ruling on
objectui#8165, option A):

```ts
import type { TranslateFn } from '@object-ui/i18n';
```

`@object-ui/app-shell` (`writeWarningToast`, which its sibling toast modules
re-export) and
`@object-ui/fields` (`file-size-guard`) each declared their own copy of this
type. Both copies were byte-identical to the new declaration, and both modules
now re-export it from `@object-ui/i18n` instead of declaring it, so every
existing import of `TranslateFn` from those modules resolves to the same type
it did before — no call site's contract moves. Neither package publishes the
name from its own entry; `@object-ui/i18n` is the one place to import it from.

The `TranslateFn` entry in the one-authority gate's `KNOWN_COLLISIONS` baseline
(`scripts/__tests__/one-authority-per-exported-name-6273.test.ts`) is removed:
the name now has one declaration.
6 changes: 4 additions & 2 deletions packages/app-shell/src/preview/usePublishAllDrafts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ import { publishHealthFromResponse, type PublishHealth } from '@object-ui/plugin
import { useMetadataClient } from '../views/metadata-admin/useMetadata.js';
import { emitMetadataRefresh } from '../assistant/assistantBus.js';
import { lintDraftCapabilityReferences } from './capabilityLint.js';

type TranslateFn = (key: string, opts?: Record<string, unknown>) => string;
// The one authority for the narrowed `t` (objectui#8261) — imported, not
// re-declared. The local copy this replaces differed only in its parameter's
// NAME (`opts`), which TypeScript does not compare, so no caller's contract moves.
import type { TranslateFn } from '@object-ui/i18n';

export interface PublishAllResult {
ok: boolean;
Expand Down
47 changes: 18 additions & 29 deletions packages/app-shell/src/providers/metadataReadWarningToast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,40 +60,29 @@
*/

import type { MetadataReadWarningEvent } from '@object-ui/data-objectstack';
import type { TranslateFn } from './writeWarningToast.js';
import type { TranslateFn } from '@object-ui/i18n';

/**
* i18next's `t`, narrowed to what this module uses — RE-EXPORTED from
* `writeWarningToast`, never re-declared.
* i18next's `t`, narrowed to what this module uses — RE-EXPORTED from its one
* authority in `@object-ui/i18n`, never re-declared.
*
* The rest of this module was modelled on its two siblings, and a fourth copy
* of their local `export type TranslateFn = …` came along with the pattern.
* That name already has three declarations (`writeWarningToast`,
* `saveAdvisoryToast`, `fields/src/widgets/file-size-guard`), and the
* objectui#6172 甲/A1 ruling is that every exported name has exactly one
* authority — so a fourth is the one thing this file must not add.
* `scripts/__tests__/one-authority-per-exported-name-6273.test.ts` caught it,
* and its remedy is this: `export type { X } from '<the-owner>'` is a
* re-export, not a second declaration, and the gate does not count it. ⛔ The
* baseline it also carries is SHRINK-ONLY and is not an option here.
* The rest of this module was modelled on its two siblings, and when it was
* written (objectui#7741) a further copy of their local
* `export type TranslateFn = …` came along with the pattern. The objectui#6172
* 甲/A1 ruling is that every exported name has exactly one authority, and
* `scripts/__tests__/one-authority-per-exported-name-6273.test.ts` caught it;
* its remedy is this: `export type { X } from '<the-owner>'` is a re-export,
* not a second declaration, and the gate does not count it. ⛔ The baseline it
* also carries is SHRINK-ONLY and is not an option here.
*
* Why `writeWarningToast` is the one pointed at, from evidence already in the
* tree rather than a judgement made here:
*
* - `AdapterProvider` — this module's only caller — already imports
* `TranslateFn` from `./writeWarningToast.js` and passes that very value
* into all three emitters, including this one. Pointing here is therefore
* the wiring that already exists, not a new claim about which file owns
* the name.
* - `file-size-guard.ts`'s own declaration names
* `app-shell/src/providers/writeWarningToast` as "the established
* `TranslateFn` pattern" it was copied from.
*
* ⛔ This does NOT resolve the pre-existing three-way collision, and is not an
* attempt to: that predates this card and repairing it belongs to its own.
* This file's obligation is only to stop adding to it.
* The owner was `writeWarningToast` then. objectui#8261 moved the one
* declaration DOWN into `@object-ui/i18n` (the maintainer's ruling on
* objectui#8165, option A) — the only home the `@object-ui/fields` copy could
* re-export from, since `@object-ui/app-shell` depends on `@object-ui/fields`
* and not the other way round. This module points at the owner itself rather
* than at `writeWarningToast`, which is now a re-export too.
*/
export type { TranslateFn } from './writeWarningToast.js';
export type { TranslateFn } from '@object-ui/i18n';

/**
* Where the message goes. Structurally satisfied by sonner's `toast`, which is
Expand Down
51 changes: 21 additions & 30 deletions packages/app-shell/src/providers/saveAdvisoryToast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,39 +38,30 @@
*/

import type { MetadataSaveAdvisoryEvent } from '@object-ui/data-objectstack';
import type { TranslateFn } from './writeWarningToast.js';
import type { TranslateFn } from '@object-ui/i18n';

/**
* i18next's `t`, narrowed to what this module uses — RE-EXPORTED from
* `writeWarningToast`, never re-declared.
*
* This module carried its own byte-identical copy until objectui#8165. The
* objectui#6172 甲/A1 ruling is that every exported name has exactly one
* authority, and
* `scripts/__tests__/one-authority-per-exported-name-6273.test.ts` had been
* carrying `TranslateFn` as accepted debt across three files. The remedy that
* gate names is exactly this: `export type { X } from '<the-owner>'` is a
* re-export, not a second declaration, and the gate does not count it. ⛔ Its
* baseline is SHRINK-ONLY, so the entry lost this site in the same PR.
*
* Why `writeWarningToast` is the one pointed at — read off the tree rather
* than decided here:
*
* - `AdapterProvider` is the single caller of all three emitters, and it
* already imports `TranslateFn` from `./writeWarningToast.js` and passes
* that one value into `emitSaveAdvisories` alongside the other two.
* - `metadataReadWarningToast.ts` re-points at it for the same reason, and
* `file-size-guard.ts` names it in its own docblock as the established
* pattern it was copied from.
*
* ⚠️ The THIRD site — `packages/fields/src/widgets/file-size-guard.ts` — is
* deliberately NOT re-pointed and stays on the baseline. `@object-ui/app-shell`
* DEPENDS ON `@object-ui/fields`, so a re-export in that direction is a package
* cycle, and this name is on neither package's published face. Retiring that
* copy means moving the authority DOWN into a package both depend on — a
* different change, and one nobody has ruled on (objectui#8165 reports it).
* i18next's `t`, narrowed to what this module uses — RE-EXPORTED from its one
* authority in `@object-ui/i18n`, never re-declared.
*
* This module carried its own byte-identical copy until objectui#8165, which
* re-pointed it at `writeWarningToast` — then the de facto owner, and the
* module `AdapterProvider` imports it from when it hands one `t` to all three
* emitters. The objectui#6172 甲/A1 ruling is that every exported name has
* exactly one authority, and
* `scripts/__tests__/one-authority-per-exported-name-6273.test.ts` is the gate
* that re-derives it: `export type { X } from '<the-owner>'` is a re-export,
* not a second declaration, and the gate does not count it.
*
* objectui#8261 then moved the one declaration DOWN into `@object-ui/i18n`
* (the maintainer's ruling on objectui#8165, option A), because the copy in
* `packages/fields/src/widgets/file-size-guard.ts` could not re-export from
* app-shell — `@object-ui/app-shell` depends on `@object-ui/fields`, so that
* arrow is a package cycle. This module points at the owner itself rather than
* at `writeWarningToast`, which is now a re-export too: a reader following the
* import lands on the declaration in one hop.
*/
export type { TranslateFn } from './writeWarningToast.js';
export type { TranslateFn } from '@object-ui/i18n';

/**
* Where the message goes. Structurally satisfied by sonner's `toast`, which is
Expand Down
15 changes: 13 additions & 2 deletions packages/app-shell/src/providers/writeWarningToast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,20 @@ import type {
ObjectStackAdapter,
WriteWarningEvent,
} from '@object-ui/data-objectstack';
import type { TranslateFn } from '@object-ui/i18n';

/** i18next's `t`, narrowed to what this module uses. */
export type TranslateFn = (key: string, options?: Record<string, unknown>) => string;
/**
* i18next's `t`, narrowed to what this module uses — RE-EXPORTED from its one
* authority in `@object-ui/i18n`, never re-declared (objectui#8261).
*
* Its callers keep importing it from here, so no import path moves. This
* module declared it itself until the maintainer's ruling on objectui#8165
* (option A) moved the one declaration down into `@object-ui/i18n`, the
* package both `@object-ui/app-shell` and `@object-ui/fields` already depend
* on — the copy in `@object-ui/fields` could never re-export from here,
* because app-shell depends on fields and not the other way round.
*/
export type { TranslateFn } from '@object-ui/i18n';

/** Convention-based field-label resolver (`useObjectLabel().fieldLabel`). */
export type FieldLabelFn = (objectName: string, fieldName: string, fallback: string) => string;
Expand Down
11 changes: 4 additions & 7 deletions packages/app-shell/src/utils/managedByEmptyState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,17 @@
* story without repeating themselves verbatim.
*/
import { resolveEffectiveCrudAffordances, type UserActionsOverride } from '@object-ui/core';
// The one authority for the narrowed `t` (objectui#8261): a key and optional
// options (including a `defaultValue` used as the English fallback when a
// locale lacks the key) — imported, not re-declared.
import type { TranslateFn } from '@object-ui/i18n';

export interface ManagedByEmptyState {
title: string;
message: string;
icon: string;
}

/**
* Translator function, structurally compatible with the `t` returned by
* `useObjectTranslation()`. Accepts a key and optional options (including a
* `defaultValue` used as the English fallback when a locale lacks the key).
*/
type TranslateFn = (key: string, options?: Record<string, unknown>) => string;

export function resolveManagedByEmptyState(
managedBy: string | undefined | null,
t: TranslateFn,
Expand Down
9 changes: 6 additions & 3 deletions packages/fields/src/widgets/file-size-guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,14 @@
* post-mortem that restates it lights that census up forever.
*/

import type { TranslateFn } from '@object-ui/i18n';

/**
* Minimal shape of the i18next `t` this module needs, matching the established
* `TranslateFn` pattern in `app-shell/src/providers/writeWarningToast`.
* Minimal shape of the i18next `t` this module needs — RE-EXPORTED from its one
* authority in `@object-ui/i18n`, never re-declared (objectui#8261). The
* sibling widgets that import it from here keep doing so; no import path moves.
*/
export type TranslateFn = (key: string, options?: Record<string, unknown>) => string;
export type { TranslateFn } from '@object-ui/i18n';

/** Divisor for rendering a byte limit as MB, matching the widgets' size display. */
const BYTES_PER_MB = 1024 * 1024;
Expand Down
16 changes: 16 additions & 0 deletions packages/i18n/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,22 @@ const i18n = createI18n({ defaultLanguage: 'de' });
i18n.t('common.cancel'); // "Abbrechen"
```

### TranslateFn — typing a `t` you are handed

A helper that receives `t` from its caller, rather than calling a hook itself,
types that parameter with `TranslateFn`: i18next's `t` narrowed to
`(key: string, options?: Record<string, unknown>) => string`. This package is
the one authority for that type — import it rather than declaring a local
copy:

```ts
import type { TranslateFn } from '@object-ui/i18n';

export function saveLabel(t: TranslateFn): string {
return t('common.save');
}
```

### Formatting Utilities

Locale-aware formatting functions:
Expand Down
5 changes: 5 additions & 0 deletions packages/i18n/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ export { createSafeTranslation, useSafeTranslate } from './useSafeTranslation.js
// handed to core's `buildChartSeries` as `builtinAggregateLabels`.
export { builtinAggregateLabels, type SafeTranslate } from './builtinAggregateLabels.js';

// objectui#8261 — the ONE authority for i18next's `t` narrowed to
// `(key, options?) => string`. `@object-ui/app-shell` and `@object-ui/fields`
// re-export it from here rather than declaring their own copies.
export type { TranslateFn } from './translateFn.js';

// Convention-based object/field label i18n
export { useObjectLabel, useSafeFieldLabel } from './useObjectLabel.js';

Expand Down
30 changes: 30 additions & 0 deletions packages/i18n/src/translateFn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* ObjectUI
* Copyright (c) 2024-present ObjectStack Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

/**
* i18next's `t`, narrowed to the one call shape a plain helper needs: a key,
* optional interpolation / `defaultValue` options, and a string back.
*
* ⭐ This is the ONE authority for the name (objectui#8261, executing the
* maintainer's ruling on objectui#8165, option A). A module that is handed a
* `t` rather than calling a translation hook itself — a toast emitter, a size
* guard, a message builder — types that parameter with this and imports it.
* ⛔ It does not declare its own copy: `@object-ui/app-shell` and
* `@object-ui/fields` each carried a byte-identical one until this change, and
* both now re-export this declaration instead
* (`export type { TranslateFn } from '@object-ui/i18n'`).
*
* `scripts/__tests__/one-authority-per-exported-name-6273.test.ts` is what
* re-derives that there is one authority: it reads such a re-export as a
* re-export, and a second `export type TranslateFn = …` anywhere in a
* published package's source as a collision.
*
* Why this package and not `@object-ui/types`: it is a translation-function
* shape, not a schema type — the ruling's own ground.
*/
export type TranslateFn = (key: string, options?: Record<string, unknown>) => string;
33 changes: 12 additions & 21 deletions scripts/__tests__/one-authority-per-exported-name-6273.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -470,27 +470,18 @@ const KNOWN_COLLISIONS: ReadonlyMap<string, readonly string[]> = new Map([
['RecordDetailDrawerProps', ['packages/plugin-dashboard/src/RecordDetailDrawer.tsx', 'packages/plugin-detail/src/RecordDetailDrawer.tsx']],
['SchemaNode', ['packages/sdui-parser/src/types.ts', 'packages/types/src/base.ts']],
['ThemeProviderProps', ['packages/providers/src/types.ts', 'packages/react/src/context/ThemeContext.tsx']],
// `TranslateFn` lost its `packages/app-shell/src/providers/saveAdvisoryToast.ts`
// site in objectui#8165. All three declarations were BYTE-IDENTICAL when that
// was measured (86 bytes each, one sha256 across the three), so there was no
// shape to reconcile; and the authority did not need choosing, because
// `AdapterProvider` is the single caller of all three emitters and already
// imported the type from `./writeWarningToast.js`, passing that one value into
// each of them. `saveAdvisoryToast.ts` now re-exports it, which this gate does
// not count.
//
// ⚠️ The `packages/fields` site STAYS, and that is a measurement rather than an
// oversight. The re-export remedy is dependency-illegal there:
// `@object-ui/app-shell` DEPENDS ON `@object-ui/fields`, so pointing fields at
// app-shell is a package cycle — and `TranslateFn` is on neither package's
// published face (`app-shell/src/index.ts` names it nowhere and has no star
// re-export; `fields/src/index.tsx` stars 50-odd widget modules but not
// `file-size-guard.js`, and none of its three importers re-export the name), so
// there would be nothing to import either. Retiring the last copy means moving
// the authority DOWN into a package both depend on — the `KanbanSchema` route
// above — which publishes a new name from that package and is a decision
// nobody has made. objectui#8165 reports it instead of guessing.
['TranslateFn', ['packages/app-shell/src/providers/writeWarningToast.ts', 'packages/fields/src/widgets/file-size-guard.ts']],
// `TranslateFn` sat here — i18next's `t` narrowed to `(key, options?) => string`,
// declared BYTE-IDENTICALLY in three files (one sha256 across the three).
// objectui#8165 re-pointed `packages/app-shell/src/providers/saveAdvisoryToast.ts`
// at `writeWarningToast.ts`, but could not do the same for
// `packages/fields/src/widgets/file-size-guard.ts`: `@object-ui/app-shell`
// DEPENDS ON `@object-ui/fields`, so that re-export would be a package cycle.
// The maintainer's ruling on objectui#8165 (option A) moved the ONE declaration
// DOWN into `@object-ui/i18n` — the `KanbanSchema` route above — and
// objectui#8261 landed it: `packages/i18n/src/translateFn.ts` declares it, the
// package entry publishes it, and both former declarations re-export it, which
// this gate does not count. One authority now, so the entry would fail the
// stale-baseline direction.
['UndoRedoState', ['packages/plugin-designer/src/hooks/useUndoRedo.ts', 'packages/types/src/ui-action.ts']],
['UserDataAdapter', ['packages/app-shell/src/context/UserStateAdapters.tsx', 'packages/data-objectstack/src/userState.ts']],
// `ValidationFunction` sat here, colliding between
Expand Down
Loading