Skip to content

feat(admin): add module-level Lingui i18n string extraction#470

Merged
ascorbic merged 49 commits into
emdash-cms:mainfrom
ophirbucai:i18n/module-level-extractions
Apr 12, 2026
Merged

feat(admin): add module-level Lingui i18n string extraction#470
ascorbic merged 49 commits into
emdash-cms:mainfrom
ophirbucai:i18n/module-level-extractions

Conversation

@ophirbucai

@ophirbucai ophirbucai commented Apr 11, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Wraps module-level strings in the admin UI with Lingui's lazy translation pattern, enabling i18n for strings declared outside React components.

The challenge: Module-level strings (constants, helper functions, config objects) can't use the t macro directly because it requires an active i18n instance from useLingui(), which is only available inside React components.

Solution: Use Lingui's msg macro for lazy message descriptors:

  1. Module-level: Define strings with msg`placeholder` (returns a MessageDescriptor)
  2. Component-level: Unwrap with t(descriptor) from useLingui() at render time

This defers translation until the component renders, when i18n is guaranteed to be initialized.

Files with module-level extractions:

  • AdminCommandPalette: Navigation items
  • PortableTextEditor: Block type definitions, embed config
  • BlockMenu, Widgets: Block/widget labels and descriptions
  • ContentTypeEditor: Field type labels and descriptions
  • ApiTokenSettings: Expiry options
  • roleDefinitions.ts: User role names and descriptions
  • WelcomeModal: Welcome cards
  • Additionally added an API token scopes contract test ensuring server and client enums don't drift 🪄

Follows up #234

Type of change

  • Refactor (no behavior change)
  • Translation

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation and pnpm locale:extract has been run (if applicable)
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion: N/A

AI-generated code disclosure

  • This PR includes AI-generated code

Screenshots / test output

Admin browser tests: 725/725 passing

Creates a dedicated init module that pre-initializes Lingui's i18n
instance with English locale before any other modules execute. This
prevents "no locale set" errors when module-level t`` macros evaluate.

The init module is imported in both the admin package entry point and
the Astro admin route to ensure early initialization regardless of
Astro's island hydration order.

Added src/locales/init.ts to tsdown entry array so it builds to
dist/locales/init.js.
Wraps user-facing strings in module-level constants and helper functions
with the t\`\` macro from @lingui/core/macro. These strings are defined
outside React components but are called lazily within useMemo hooks or
during render, ensuring they evaluate after i18n initialization.

Changes include:
- AdminCommandPalette: buildNavItems function
- PortableTextEditor: block type definitions and embed config
- BlockMenu, Widgets: block/widget labels and descriptions
- ContentTypeEditor: field labels and descriptions
- ApiTokenSettings: expiry options
- api-tokens.ts: scope labels and descriptions
- RoleBadge: role names and descriptions
- WelcomeModal: role names
- AllowedDomainsSettings: role names
- MediaPickerModal, MediaLibrary: tab labels

All wrapped strings will be extracted by lingui extract in the next
commit. Components using these strings have i18n.locale in their useMemo
dependencies to trigger re-render on locale change.
Runs lingui extract to discover and catalog all newly wrapped t\`\`
strings from the previous commit. Adds 130+ new message IDs to both
English and German catalogs.

English catalog includes auto-filled translations (msgid = msgstr for en).
German catalog entries are marked for translation (empty msgstr).

Generated by: pnpm locale:extract
…rModal

These components use runtime t from useLingui() inside useMemo, which is
standard React i18n, not module-level extraction. Removing them from this
PR as they're out of scope.
Removed 'Library' string that was erroneously included from MediaLibrary
and MediaPickerModal (runtime t usage, not module-level).
Copilot AI review requested due to automatic review settings April 11, 2026 23:08
@changeset-bot

changeset-bot Bot commented Apr 11, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: cd03c0a

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions

Copy link
Copy Markdown
Contributor

Scope check

This PR changes 1,054 lines across 17 files. Large PRs are harder to review and more likely to be closed without review.

If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs.

See CONTRIBUTING.md for contribution guidelines.

@github-actions

github-actions Bot commented Apr 11, 2026

Copy link
Copy Markdown
Contributor

Lunaria Status Overview

🌕 This pull request will trigger status changes.

Learn more

By default, every PR changing files present in the Lunaria configuration's files property will be considered and trigger status changes accordingly.

You can change this by adding one of the keywords present in the ignoreKeywords property in your Lunaria configuration file in the PR's title (ignoring all files) or by including a tracker directive in the merged commit's description.

Tracked Files

File Note
packages/admin/src/locales/de/messages.po Localization changed, will be marked as complete.
packages/admin/src/locales/en/messages.po Source changed, localizations will be marked as outdated.
Warnings reference
Icon Description
🔄️ The source for this localization has been updated since the creation of this pull request, make sure all changes in the source have been applied.

@pkg-pr-new

pkg-pr-new Bot commented Apr 11, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@470

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@470

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@470

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@470

emdash

npm i https://pkg.pr.new/emdash@470

create-emdash

npm i https://pkg.pr.new/create-emdash@470

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@470

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@470

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@470

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@470

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@470

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@470

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@470

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@470

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@470

commit: cd03c0a

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Enables i18n extraction/usage for module-level (non-component) admin strings by pre-initializing Lingui and wrapping many constant/config strings with t\`` to prevent early-evaluation crashes in Astro island hydration.

Changes:

  • Add an @emdash-cms/admin/locales/init side-effect module and ensure it’s imported early (admin entry + Astro route).
  • Expand the admin build entries to emit locales/init and add typing for messages.mjs imports.
  • Wrap a large set of previously hardcoded labels/descriptions with Lingui macros and update PO catalogs.

Reviewed changes

Copilot reviewed 16 out of 17 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
packages/core/src/astro/routes/admin.astro Imports i18n pre-init before client modules load; loads resolved-locale messages for the island
packages/admin/tsdown.config.ts Adds src/locales/init.ts as a build entry
packages/admin/src/locales/init.ts Pre-initializes Lingui i18n (English) via side-effect import
packages/admin/src/locales/en/messages.po Adds newly extracted English msgids
packages/admin/src/locales/de/messages.po Adds newly extracted German entries (empty msgstr)
packages/admin/src/locales/en/messages.mjs.d.ts Adds a wildcard module declaration for compiled messages.mjs
packages/admin/src/lib/api/api-tokens.ts Makes API token scope labels/descriptions translatable
packages/admin/src/index.ts Ensures init module is imported first
packages/admin/src/components/* Wraps multiple module-level labels/descriptions with t\`` and adds locale deps in a couple of memoized builders

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/admin/src/lib/api/api-tokens.ts Outdated
Comment thread packages/admin/src/components/Widgets.tsx
Comment thread packages/admin/src/components/PortableTextEditor.tsx Outdated
Comment thread packages/admin/src/components/editor/BlockMenu.tsx
Comment thread packages/admin/src/components/settings/ApiTokenSettings.tsx
Comment thread packages/admin/src/components/settings/AllowedDomainsSettings.tsx Outdated
Comment thread packages/admin/src/components/ContentTypeEditor.tsx Outdated
Comment thread packages/admin/src/components/users/RoleBadge.tsx Outdated
Comment thread packages/admin/src/locales/init.ts Outdated
ophirbucai and others added 4 commits April 12, 2026 02:18
Converted module-level constants with t\`\` calls to builder functions
that return the config on demand. This ensures translations execute at
render time (when locale is current) rather than at import time (frozen
to pre-init locale).

Changed:
- RoleBadge: ROLE_CONFIG -> buildRoleConfig(), called in useMemo
- api-tokens: API_TOKEN_SCOPES -> buildApiTokenScopes(), called in useMemo
- ApiTokenSettings: EXPIRY_OPTIONS -> buildExpiryOptions(), called in useMemo

All components using these now have i18n.locale in their useMemo
dependencies, ensuring labels/descriptions update when locale changes.
@ophirbucai ophirbucai force-pushed the i18n/module-level-extractions branch from 5360212 to ca9620e Compare April 11, 2026 23:35
Pre-initialization only needs to activate a locale, not load real messages.
App.tsx loads the real English catalog via i18n.loadAndActivate() in useEffect,
and since module-level t calls execute lazily from builder functions (called
in useMemo), they'll use the real messages.

This eliminates the hard dependency on compiled messages.mjs, allowing dev
workflows (pnpm dev, tests, fresh clones) to work without requiring
locale:compile to run first.

Bundle size: init.js reduced from 5.26 kB to 0.77 kB.
@ophirbucai ophirbucai force-pushed the i18n/module-level-extractions branch from ca9620e to 5097436 Compare April 11, 2026 23:36
ophirbucai and others added 2 commits April 12, 2026 02:53
Components using useLingui() (introduced by module-level i18n) require
I18nProvider context in tests. Instead of wrapping each test manually,
centralize i18n setup:

1. Convert render.tsx to render.ts (React.createElement, no JSX)
2. Automatically wrap components in I18nProvider
3. Update all test imports: vitest-browser-react → ../utils/render.js
4. Centralize init.js import in setup.ts (runs before all tests)

This ensures all tests have i18n context without explicit wrappers.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 57 out of 57 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

packages/admin/src/lib/api/index.ts:264

  • API_TOKEN_SCOPES was previously exported from the public lib/api barrel, but this change removes it and replaces it with buildApiTokenScopes. That’s a breaking API change for downstream consumers importing API_TOKEN_SCOPES. Consider keeping a backwards-compatible export (e.g. re-exporting API_TOKEN_SCOPES with a clear deprecation path) or add the appropriate changeset/versioning note to reflect the breaking change.
// API Tokens
export {
	type ApiTokenInfo,
	type ApiTokenCreateResult,
	type CreateApiTokenInput,
	buildApiTokenScopes,
	fetchApiTokens,
	createApiToken,
	revokeApiToken,
} from "./api-tokens.js";

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/admin/src/components/settings/AllowedDomainsSettings.tsx

@ascorbic ascorbic left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey! Great work here. I think the builder pattern isn't needed though. A simpler and more idiomatic approach would be to use lazy translations. This solves exactly the problem you're dealing with here, but without the boilerplate of builders and useMemo everywhere.

This does also highlight the fact we have lots of duplicated copies of the role labels. Is there somewhere these could be extracted so we're not translating them multiple times?

ophirbucai and others added 22 commits April 12, 2026 11:29
…ptor

Use Lingui msg for static copy and a NavItemTitle union for manifest
labels that cannot use dynamic msg ids. Result groups keep MessageDescriptor
labels resolved with t() from useLingui().
…to msg

Define SUPPORT_OPTIONS and SYSTEM_FIELDS with MessageDescriptor labels and
descriptions; resolve in the editor and SystemFieldRow via useLingui t().
ContentTypeEditor tests import the shared render helper so I18nProvider wraps
useLingui.
Use MessageDescriptor for all slash titles, descriptions, and categories.
Plugin rows use interpolated msg for label, optional description, and
"Embed a {0}" fallback. Tests use shared render for I18nProvider.
Module-level msg for roles, scope copy, titles, and actions; interpolated
welcome title with first name. Tests use shared render for I18nProvider.
BUILTIN_WIDGETS label and description use module-level msg; Widgets resolves them with useLingui t() for palette rows and drag payload labels.
Use `title: string | MessageDescriptor` on `NavItem` and resolve with inline
`typeof` checks for filtering and palette rows. Removes the old discriminated
wrapper and helper indirection (KISS, easier review).
Built-in palette items omit a hardcoded English `title` on the input; the drag
payload uses `t(item.label)` so persisted defaults match the palette. Normalize
the Widgets test render import path.
Use MessageDescriptor | string for slash title/description; built-ins stay on
msg, plugin rows keep API strings and use t(msg`Embed a ${block.label}`) only
for the fallback. Resolve menu and filter text with inline typeof checks.

Editor tests import the shared render harness without a file extension so
useLingui runs under I18nProvider (slash-menu included).
Replace buildBlockTransforms() (macro t) with module-level blockTransforms
using msg for transform labels; BlockMenu resolves labels with t() in the
Turn into submenu only. Leave main menu copy as literals (lazy-migration PR
scope). block-menu tests use shared render; transforms test imports the
exported blockTransforms array.
Use module-level msg for role descriptors; resolve with useLingui in UI.
Tests use shared render harness for I18nProvider.
Module-level msg for expiry options, scope labels (SCOPE_UI), and UI copy;
resolve with useLingui. Scope values still come from API_TOKEN_SCOPES in
api-tokens. Pass pre-translated expiry map into CreateTokenForm for Select
items. Catalog extract for en/de.
- Add roleDefinitions (ROLE_ENTRIES, getRoleConfig) for msg + badge colors
- Add useRolesConfig: roleLabels, getRoleLabel, pre-resolved roles rows
- Add useAllowedDomainsRolesConfig for default-role selects (cap at Editor)
- Wire AllowedDomainsSettings, UserList, UserDetail, InviteUserModal, users route
- RoleBadge uses getRoleConfig only; barrel exports useRolesConfig + getRoleConfig
- User invite/detail tests use shared I18n render harness
- No locale .po in this slice (per migration plan cadence)
Replace API_TOKEN_SCOPES label/description duplicates with
API_TOKEN_SCOPE_VALUES + ApiTokenScopeValue; UI copy stays in
ApiTokenSettings SCOPE_UI (msg). Re-export from lib/api index.
…est to ensure no drift

Transform API token scopes from an array to an object for improved readability and maintainability. Update the type definition to reflect the new structure while preserving existing functionality.
Ensures all browser tests have i18n context initialized.
@ophirbucai ophirbucai requested a review from ascorbic April 12, 2026 16:56

@ascorbic ascorbic left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! Thanks

@ascorbic ascorbic merged commit 3914ae8 into emdash-cms:main Apr 12, 2026
27 of 28 checks passed
fmhall pushed a commit to fmhall/emdash that referenced this pull request Apr 13, 2026
…ms#470)

* Add i18n pre-initialization to fix module-level macro timing

Creates a dedicated init module that pre-initializes Lingui's i18n
instance with English locale before any other modules execute. This
prevents "no locale set" errors when module-level t`` macros evaluate.

The init module is imported in both the admin package entry point and
the Astro admin route to ensure early initialization regardless of
Astro's island hydration order.

Added src/locales/init.ts to tsdown entry array so it builds to
dist/locales/init.js.

* Apply Lingui t\`\` macro to module-level strings

Wraps user-facing strings in module-level constants and helper functions
with the t\`\` macro from @lingui/core/macro. These strings are defined
outside React components but are called lazily within useMemo hooks or
during render, ensuring they evaluate after i18n initialization.

Changes include:
- AdminCommandPalette: buildNavItems function
- PortableTextEditor: block type definitions and embed config
- BlockMenu, Widgets: block/widget labels and descriptions
- ContentTypeEditor: field labels and descriptions
- ApiTokenSettings: expiry options
- api-tokens.ts: scope labels and descriptions
- RoleBadge: role names and descriptions
- WelcomeModal: role names
- AllowedDomainsSettings: role names
- MediaPickerModal, MediaLibrary: tab labels

All wrapped strings will be extracted by lingui extract in the next
commit. Components using these strings have i18n.locale in their useMemo
dependencies to trigger re-render on locale change.

* Extract module-level strings to locale catalogs

Runs lingui extract to discover and catalog all newly wrapped t\`\`
strings from the previous commit. Adds 130+ new message IDs to both
English and German catalogs.

English catalog includes auto-filled translations (msgid = msgstr for en).
German catalog entries are marked for translation (empty msgstr).

Generated by: pnpm locale:extract

* Add TypeScript declarations for compiled message catalogs

* Format code with oxfmt and prettier

* Revert non-module-level translations from MediaLibrary and MediaPickerModal

These components use runtime t from useLingui() inside useMemo, which is
standard React i18n, not module-level extraction. Removing them from this
PR as they're out of scope.

* Re-extract locale catalogs after removing non-module-level strings

Removed 'Library' string that was erroneously included from MediaLibrary
and MediaPickerModal (runtime t usage, not module-level).

* Clarify init.ts must be imported in both index.ts and admin.astro

* Suppress lint warnings for intentional side-effect imports

* Simplify init.ts import comment in index.ts

* Add .js extension to init module import in admin.astro

* Fix eager module-level t translations to be lazy/reactive

Converted module-level constants with t\`\` calls to builder functions
that return the config on demand. This ensures translations execute at
render time (when locale is current) rather than at import time (frozen
to pre-init locale).

Changed:
- RoleBadge: ROLE_CONFIG -> buildRoleConfig(), called in useMemo
- api-tokens: API_TOKEN_SCOPES -> buildApiTokenScopes(), called in useMemo
- ApiTokenSettings: EXPIRY_OPTIONS -> buildExpiryOptions(), called in useMemo

All components using these now have i18n.locale in their useMemo
dependencies, ensuring labels/descriptions update when locale changes.

* style: format

* Update locale catalog line number references after refactor

* Use empty messages in init.ts to avoid dependency on compiled catalogs

Pre-initialization only needs to activate a locale, not load real messages.
App.tsx loads the real English catalog via i18n.loadAndActivate() in useEffect,
and since module-level t calls execute lazily from builder functions (called
in useMemo), they'll use the real messages.

This eliminates the hard dependency on compiled messages.mjs, allowing dev
workflows (pnpm dev, tests, fresh clones) to work without requiring
locale:compile to run first.

Bundle size: init.js reduced from 5.26 kB to 0.77 kB.

* test: provide I18nProvider to all tests via custom render utility

Components using useLingui() (introduced by module-level i18n) require
I18nProvider context in tests. Instead of wrapping each test manually,
centralize i18n setup:

1. Convert render.tsx to render.ts (React.createElement, no JSX)
2. Automatically wrap components in I18nProvider
3. Update all test imports: vitest-browser-react → ../utils/render.js
4. Centralize init.js import in setup.ts (runs before all tests)

This ensures all tests have i18n context without explicit wrappers.

* style: format

* test: ensure i18n init in render utility for CI reliability

Add init.js import to render.ts in addition to setup.ts. While setup.ts
runs first in local environments, CI may have different initialization
order. Double initialization is safe (init.ts checks if locale is already
set) and ensures i18n is ready before any test component renders.

Tests pass locally but were failing in CI with "Cannot find element"
errors, suggesting components weren't rendering properly due to i18n
initialization timing.

* test: revert render utility to JSX syntax

Converting to React.createElement may have caused wrapper composition
issues in CI. Reverting to JSX syntax while keeping the I18nProvider
wrapper and init.js import.

Tests pass locally with JSX. Import paths remain `.js` (TypeScript
module resolution handles .tsx files correctly).

* test: fix render utility import paths to use .tsx extension

All test files were importing "../utils/render.js" but the file is
render.tsx, causing import resolution failures in CI. Updated all 37
test file imports to use the correct .tsx extension.

Also removed duplicate init.js import from render.tsx since setup.ts
already handles i18n initialization before tests run.

Fixes CI Browser Tests failures.

* test: fix render import paths for tests in subdirectories

Tests in components/settings/ and components/users/ subdirectories
need ../../utils/render.tsx (two levels up), not ../utils/render.tsx.

Fixes CI import resolution error in AllowedDomainsSettings.test.tsx.

* test: fix router.test.tsx import path for render utility

router.test.tsx is in the root tests/ directory, same level as utils/,
so it should import "./utils/render.tsx" not "../utils/render.tsx".

Fixes CI import error: "Failed to fetch dynamically imported module"

* chore: simplify i18n comments and builder function docs

- Simplify init.ts comment to focus on behavior (empty messages → real
  catalog), not import sites (already documented at each import)
- Remove redundant "(call at render time...)" from builder function
  comments (pattern is established, doesn't need repeating)
- Add prettier-ignore to buildApiTokenScopes for readability

Aligns with codebase pattern of documenting import discipline at call
sites rather than in the module being imported.

* refactor(admin): convert module-level i18n constants to lazy builder functions

Converts all module-level constants using t`` to lazy builder functions called
from useMemo hooks with i18n.locale dependency. This ensures translations are
reactive and update when the locale changes.

Changed:
- ContentTypeEditor: SUPPORT_OPTIONS, SYSTEM_FIELDS
- BlockMenu: blockTransforms
- Widgets: BUILTIN_WIDGETS
- AllowedDomainsSettings: ROLES, getRoleName
- WelcomeModal: getRoleLabel
- PortableTextEditor: defaultSlashCommands

All builder functions now follow the same pattern as RoleBadge.tsx and
ApiTokenSettings.tsx, ensuring consistent reactive i18n behavior.

* style: format

* refactor(admin): optimize role lookup in AllowedDomainsSettings

Refactored buildRolesConfig to return a composite object with:
- roles array for rendering Select options
- roleLabels lookup object (Record<string, string>) for O(1) access
- getRoleLabel function using the lookup

This eliminates duplicate Object.fromEntries() calls at usage sites and
provides consistent O(1) role label lookups. Renamed getRoleName to
getRoleLabel for consistency with roleLabels naming.

* chore(admin): remove unused messages.mjs.d.ts declaration file

The wildcard module declaration doesn't provide type safety for dynamic
imports with template literals (messages is still typed as any). This file
was originally added to support init.ts importing the compiled catalog,
but init.ts now uses empty messages instead.

* refactor(admin): remove i18n initialization and clean up imports

* refactor(admin): migrate AdminCommandPalette to msg and MessageDescriptor

Use Lingui msg for static copy and a NavItemTitle union for manifest
labels that cannot use dynamic msg ids. Result groups keep MessageDescriptor
labels resolved with t() from useLingui().

* refactor(admin): migrate ContentTypeEditor support and system fields to msg

Define SUPPORT_OPTIONS and SYSTEM_FIELDS with MessageDescriptor labels and
descriptions; resolve in the editor and SystemFieldRow via useLingui t().
ContentTypeEditor tests import the shared render helper so I18nProvider wraps
useLingui.

* refactor(admin): PortableTextEditor slash commands use msg and t

Use MessageDescriptor for all slash titles, descriptions, and categories.
Plugin rows use interpolated msg for label, optional description, and
"Embed a {0}" fallback. Tests use shared render for I18nProvider.

* refactor(admin): migrate WelcomeModal to msg and useLingui

Module-level msg for roles, scope copy, titles, and actions; interpolated
welcome title with first name. Tests use shared render for I18nProvider.

* refactor(admin): migrate Widgets built-in palette to msg and useLingui

BUILTIN_WIDGETS label and description use module-level msg; Widgets resolves them with useLingui t() for palette rows and drag payload labels.

* refactor(admin): simplify command palette nav item titles

Use `title: string | MessageDescriptor` on `NavItem` and resolve with inline
`typeof` checks for filtering and palette rows. Removes the old discriminated
wrapper and helper indirection (KISS, easier review).

* fix(admin): align built-in widget default title with localized label

Built-in palette items omit a hardcoded English `title` on the input; the drag
payload uses `t(item.label)` so persisted defaults match the palette. Normalize
the Widgets test render import path.

* refactor(admin): complete PortableTextEditor slash i18n and tests

Use MessageDescriptor | string for slash title/description; built-ins stay on
msg, plugin rows keep API strings and use t(msg`Embed a ${block.label}`) only
for the fallback. Resolve menu and filter text with inline typeof checks.

Editor tests import the shared render harness without a file extension so
useLingui runs under I18nProvider (slash-menu included).

* refactor(admin): BlockMenu block transforms use msg + lazy t

Replace buildBlockTransforms() (macro t) with module-level blockTransforms
using msg for transform labels; BlockMenu resolves labels with t() in the
Turn into submenu only. Leave main menu copy as literals (lazy-migration PR
scope). block-menu tests use shared render; transforms test imports the
exported blockTransforms array.

* refactor(admin): lazy Lingui pattern for AllowedDomainsSettings

Use module-level msg for role descriptors; resolve with useLingui in UI.
Tests use shared render harness for I18nProvider.

* refactor(admin): lazy Lingui for ApiTokenSettings

Module-level msg for expiry options, scope labels (SCOPE_UI), and UI copy;
resolve with useLingui. Scope values still come from API_TOKEN_SCOPES in
api-tokens. Pass pre-translated expiry map into CreateTokenForm for Select
items. Catalog extract for en/de.

* refactor(admin): centralize role labels with useRolesConfig

- Add roleDefinitions (ROLE_ENTRIES, getRoleConfig) for msg + badge colors
- Add useRolesConfig: roleLabels, getRoleLabel, pre-resolved roles rows
- Add useAllowedDomainsRolesConfig for default-role selects (cap at Editor)
- Wire AllowedDomainsSettings, UserList, UserDetail, InviteUserModal, users route
- RoleBadge uses getRoleConfig only; barrel exports useRolesConfig + getRoleConfig
- User invite/detail tests use shared I18n render harness
- No locale .po in this slice (per migration plan cadence)

* refactor(admin): API token scopes as values-only in api-tokens

Replace API_TOKEN_SCOPES label/description duplicates with
API_TOKEN_SCOPE_VALUES + ApiTokenScopeValue; UI copy stays in
ApiTokenSettings SCOPE_UI (msg). Re-export from lib/api index.

* refactor(admin): migrate ApiTokenSettings and scopes map, include a test to ensure no drift

Transform API token scopes from an array to an object for improved readability and maintainability. Update the type definition to reflect the new structure while preserving existing functionality.

* chore: import order formatting

* chore(i18n): update German and English translations for various components

* style: format

* refactor(InviteUserModal, UserDetails): reuse roleLabels in select items

* fix: initialize i18n in test setup and use I18nProvider wrapper

Ensures all browser tests have i18n context initialized.

* style: add void to floating render promises

---------

Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants