feat(admin): add i18n with Lingui#234
Conversation
|
b4b7a60 to
ffa976f
Compare
There was a problem hiding this comment.
Pull request overview
Adds Lingui-based internationalization plumbing to the EmDash admin UI, including server-side locale resolution and client-side catalog activation, while wrapping existing admin strings with Lingui macros for future translation support.
Changes:
- Introduces Lingui configuration, dependencies, and an admin locale API (
resolveLocale,useLocale, supported locales config). - Wires Lingui into the admin shell (Astro route → plugin registry wrapper →
AdminAppwithI18nProvider). - Wraps/admin UI strings across routes and components using
t,<Trans>, and<Plural>macros.
Reviewed changes
Copilot reviewed 82 out of 84 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| skills/adding-admin-locale/SKILL.md | Adds internal documentation for adding locales and using Lingui macros. |
| pnpm-workspace.yaml | Adds Lingui packages to the pnpm catalog. |
| packages/core/src/astro/routes/PluginRegistry.tsx | Passes locale/messages props through to AdminApp. |
| packages/core/src/astro/routes/admin.astro | Resolves locale server-side and imports the locale catalog for the admin shell. |
| packages/admin/tsdown.config.ts | Expands build entrypoints to include locales index. |
| packages/admin/tsconfig.json | Adds vite/client types for import.meta.env typing. |
| packages/admin/src/routes/users.tsx | Wraps user management route strings with Lingui macros. |
| packages/admin/src/routes/bylines.tsx | Wraps bylines route strings and labels with Lingui macros. |
| packages/admin/src/locales/useLocale.ts | Adds a hook for client-side locale switching + cookie persistence + dynamic catalog import. |
| packages/admin/src/locales/index.ts | Adds locale-related barrel exports. |
| packages/admin/src/locales/config.ts | Defines supported locales and server-side locale resolution from cookie/Accept-Language. |
| packages/admin/src/index.ts | Re-exports locale APIs from the package root. |
| packages/admin/src/components/WelcomeModal.tsx | Wraps welcome modal strings and role labels with Lingui macros. |
| packages/admin/src/components/users/UserList.tsx | Wraps user list UI strings with Lingui macros. |
| packages/admin/src/components/users/UserDetail.tsx | Wraps user detail panel strings with Lingui macros. |
| packages/admin/src/components/users/InviteUserModal.tsx | Wraps invite modal strings with Lingui macros. |
| packages/admin/src/components/ThemeToggle.tsx | Wraps theme toggle labels/tooltips with Lingui macros. |
| packages/admin/src/components/ThemeMarketplaceDetail.tsx | Wraps theme marketplace detail strings with Lingui macros. |
| packages/admin/src/components/ThemeMarketplaceBrowse.tsx | Wraps theme marketplace browse strings with Lingui macros. |
| packages/admin/src/components/TaxonomySidebar.tsx | Wraps taxonomy sidebar strings and aria-labels with Lingui macros. |
| packages/admin/src/components/Sidebar.tsx | Wraps sidebar navigation group/item labels with Lingui macros. |
| packages/admin/src/components/SetupWizard.tsx | Wraps setup wizard strings and introduces pluralization with Lingui macros. |
| packages/admin/src/components/settings/SocialSettings.tsx | Wraps social settings strings with Lingui macros. |
| packages/admin/src/components/settings/SeoSettings.tsx | Wraps SEO settings strings with Lingui macros. |
| packages/admin/src/components/settings/SecuritySettings.tsx | Wraps security settings strings with Lingui macros. |
| packages/admin/src/components/settings/PasskeyItem.tsx | Wraps passkey item strings with Lingui macros. |
| packages/admin/src/components/settings/GeneralSettings.tsx | Wraps general settings strings with Lingui macros. |
| packages/admin/src/components/settings/EmailSettings.tsx | Wraps email settings strings with Lingui macros. |
| packages/admin/src/components/settings/ApiTokenSettings.tsx | Wraps API token settings strings with Lingui macros. |
| packages/admin/src/components/Settings.tsx | Adds admin-language selector UI (gated) and wraps strings with Lingui macros. |
| packages/admin/src/components/SeoPanel.tsx | Wraps SEO panel labels/descriptions with Lingui macros. |
| packages/admin/src/components/Sections.tsx | Wraps sections UI strings with Lingui macros. |
| packages/admin/src/components/SectionPickerModal.tsx | Wraps section picker modal strings with Lingui macros. |
| packages/admin/src/components/SectionEditor.tsx | Wraps section editor strings with Lingui macros. |
| packages/admin/src/components/SaveButton.tsx | Wraps save button labels with Lingui macros. |
| packages/admin/src/components/SandboxedPluginWidget.tsx | Wraps sandboxed widget empty/error strings with Lingui macros. |
| packages/admin/src/components/SandboxedPluginPage.tsx | Wraps sandboxed page error heading/message with Lingui macros. |
| packages/admin/src/components/RevisionHistory.tsx | Wraps revision history UI strings (incl. conditional plural forms) with Lingui macros. |
| packages/admin/src/components/PluginFieldErrorBoundary.tsx | Wraps plugin widget error fallback strings with Lingui macros. |
| packages/admin/src/components/MenuList.tsx | Wraps menu list UI strings with Lingui macros. |
| packages/admin/src/components/MediaPickerModal.tsx | Wraps media picker strings and pluralizes item count via Lingui macros. |
| packages/admin/src/components/MediaLibrary.tsx | Wraps media library strings and introduces Lingui plural helpers for upload messaging. |
| packages/admin/src/components/MediaDetailPanel.tsx | Wraps media detail panel labels/buttons with Lingui macros. |
| packages/admin/src/components/MarketplaceBrowse.tsx | Wraps plugin marketplace browse strings and plural forms with Lingui macros. |
| packages/admin/src/components/LoginPage.tsx | Wraps login strings with Lingui macros and adds (gated) locale selector. |
| packages/admin/src/components/LocaleSwitcher.tsx | Wraps locale switcher labels/tooltips with Lingui macros. |
| packages/admin/src/components/Header.tsx | Wraps header menu items with Lingui macros. |
| packages/admin/src/components/editor/PluginBlockNode.tsx | Wraps editor plugin block node labels/tooltips with Lingui macros. |
| packages/admin/src/components/editor/ImageNode.tsx | Wraps image node editor labels/tooltips with Lingui macros. |
| packages/admin/src/components/editor/DocumentOutline.tsx | Wraps document outline strings with Lingui macros. |
| packages/admin/src/components/editor/BlockMenu.tsx | Wraps block menu strings with Lingui macros and refactors transform labels. |
| packages/admin/src/components/DeviceAuthorizePage.tsx | Wraps device authorization strings with Lingui macros. |
| packages/admin/src/components/Dashboard.tsx | Wraps dashboard UI strings and count labels with Lingui macros. |
| packages/admin/src/components/ContentTypeList.tsx | Wraps content type list strings with Lingui macros. |
| packages/admin/src/components/ContentPickerModal.tsx | Wraps content picker modal strings with Lingui macros. |
| packages/admin/src/components/ConfirmDialog.tsx | Wraps confirm dialog cancel label with Lingui macros. |
| packages/admin/src/components/comments/CommentDetail.tsx | Wraps comment detail panel strings with Lingui macros. |
| packages/admin/src/components/BlockKitFieldWidget.tsx | Wraps block-kit widget fallback/select labels with Lingui macros. |
| packages/admin/src/components/auth/PasskeyRegistration.tsx | Wraps passkey registration strings with Lingui macros and adds default button text handling. |
| packages/admin/src/components/auth/PasskeyLogin.tsx | Wraps passkey login strings with Lingui macros and adds default button text handling. |
| packages/admin/src/components/AdminCommandPalette.tsx | Wraps command palette strings and navigation labels with Lingui macros. |
| packages/admin/src/App.tsx | Adds I18nProvider and activates Lingui with server-provided locale/messages. |
| packages/admin/package.json | Adds locale exports and Lingui scripts/dependencies. |
| lingui.config.ts | Adds Lingui extraction configuration (English-only catalog). |
| demos/simple/package.json | Adds Lingui tooling dependencies for the demo app. |
| demos/simple/astro.config.mjs | Configures Babel macro plugin + Vite Lingui plugin in the demo Astro pipeline. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
Comments suppressed due to low confidence (1)
packages/admin/tsdown.config.ts:8
- The published admin package won’t be able to load locale catalogs:
useLocale()dynamically imports./${code}/messages.porelative to the builtdist/locales/*files, but the build config only emits JS/DTs and doesn’t copy.pocatalogs intodist. This will make locale switching (and any runtime/SSR catalog import that targetsdist) fail in consumer projects. Consider copyingsrc/locales/**/messages.pointodist/locales/**/messages.poas part of the build (or changing the runtime import strategy) so the catalogs exist alongside the compiled locale modules.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
packages/admin/package.json
Outdated
| "./locales/*": "./src/locales/*" | ||
| }, | ||
| "scripts": { | ||
| "build": "tsdown && npx @tailwindcss/cli -i src/styles.css -o dist/styles.css --minify", |
There was a problem hiding this comment.
exports["./locales/*"] points at ./src/locales/*, but this package’s files list only publishes dist, so consumers won’t get src/locales/** (including messages.po). Additionally, the runtime locale loader in dist/locales/useLocale.js uses relative imports, so catalogs need to be present in dist anyway. Fix by either (a) publishing the .po catalogs (and any needed source) via files, or preferably (b) copying catalogs into dist/locales/** and exporting ./locales/* from dist.
| "./locales/*": "./src/locales/*" | |
| }, | |
| "scripts": { | |
| "build": "tsdown && npx @tailwindcss/cli -i src/styles.css -o dist/styles.css --minify", | |
| "./locales/*": "./dist/locales/*" | |
| }, | |
| "scripts": { | |
| "build": "tsdown && npx @tailwindcss/cli -i src/styles.css -o dist/styles.css --minify && node -e \"const fs=require('fs'); fs.mkdirSync('dist/locales', { recursive: true }); fs.cpSync('src/locales', 'dist/locales', { recursive: true });\"", |
| import { resolveLocale } from "@emdash-cms/admin/locales"; | ||
| const resolvedLocale = resolveLocale(Astro.request); | ||
| const { messages } = await import(`@emdash-cms/admin/locales/${resolvedLocale}/messages.po`); |
There was a problem hiding this comment.
admin.astro now imports .po catalogs directly. That requires @lingui/vite-plugin to be present in the host app’s Vite pipeline for both dev and production builds; otherwise the .po import won’t be understood and the admin route build will fail. Since the EmDash integration’s Vite config currently only adds the virtual-modules plugin, consider either adding the Lingui Vite plugin at the integration level or switching to importing precompiled JS catalogs (so host apps don’t need extra Vite config).
There was a problem hiding this comment.
@ascorbic
The Lingui Vite plugin is currently configured in demos/simple only. For other demos, templates, and published sites, admin.astro's .po import will fail without it. We could inject it from the EmDash integration in astro:config:setup, but lingui() eagerly searches for a lingui.config.ts.
Which direction do you see this resolved? We can ship a default config from core, or resolve the catalog paths programmatically, unless you have other idea in mind?
There was a problem hiding this comment.
Is there any downside to injecting it in the integration? That's generally the approach we'd take
There was a problem hiding this comment.
When someone installs emdash from npm and creates a site, they won't have a lingui.config.ts in their project. The integration would call lingui(), which calls getConfig(), which searches up from cwd and finds nothing.
In the monorepo (all demos, templates, CI), the root lingui.config.ts is always reachable. So it's only a problem for published consumers.
There was a problem hiding this comment.
Will this then cause it to fail? I wonder if we can conditionally add the integration if the config exists, or add the config ourselves. We could at least add it to the default templates. Maybe it should be a config option for the integration.
There was a problem hiding this comment.
Here's a working implementation: ophirbucai/emdash@feat/admin-i18n-lingui...spike/lingui-integration-injection
The lingui.config.ts is distributed through the core build - tsdown.config.ts
There was a problem hiding this comment.
That's looking good in part. However instead of auto-injecting the Vite plugin into the user's site, I'd pre-compile the .po files to JS. I think this could be done with he CLI as part of the admin build?
There was a problem hiding this comment.
That makes absolute sense - I'll see that it works, all ping you once I am confident in the solution
There was a problem hiding this comment.
All done - consuming sites use the pre-built messages.mjs, which runs through lingui compile during build, no Lingui config or Vite plugin needed for consumers.
Dev mode imports .po via the Vite plugin so changes are reflected immediately during development.
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/blocks
@emdash-cms/cloudflare
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
146b661 to
0c53403
Compare
|
@ascorbic Hi Matt, the Lingui macros compile to useLingui() which requires I18nProvider in the React tree. To make component tests work, I've: Created a shared test render utility (tests/utils/render.tsx) that wraps with I18nProvider - import { render } from "vitest-browser-react";
+ import { render } from "../utils/render.js";No test logic changes — just the import path. The render utility initializes Lingui with empty English messages so source strings render as-is. |
|
All 707 admin component tests passing locally, including the 36 test files updated to use the I18nProvider wrapper. Core tests (2,095) also pass. |
Overlapping PRsThis PR modifies files that are also changed by other open PRs:
This may cause merge conflicts or duplicated work. A maintainer will coordinate. |
|
This is looking great so far. I just need to get some clarity on how the setup will need to be handled for sites. Generally I want to avoid the need to install Vite plugins for core features |
Revert regex match back to exact 'text="File uploaded"' now that the source renders the original singular form.
…rement Compile .po → .mjs at admin build time via lingui compile --namespace es. Import pre-compiled JS catalogs instead of raw .po files — no Lingui Vite plugin needed in consuming sites. - Add lingui compile to admin build script - Switch admin.astro and useLocale.ts imports from .po to .mjs - Remove @lingui/vite-plugin from demos/simple and e2e fixture configs - Compiled messages.mjs committed as build artifact alongside .po source
Import .po in dev (Vite plugin compiles on the fly for instant translation feedback) and pre-compiled .mjs in production (no plugin needed). Conditional via import.meta.env.DEV.
Dev mode imports .po directly — the Vite plugin compiles on the fly. Restore the plugin and its dependency in demos/simple.
Dev mode: edit .po, refresh browser — no compile or restart needed. Production: lingui compile before committing generates .mjs. Simplify steps and update common mistakes.
E2E runs astro dev where import.meta.env.DEV is true, importing .po directly. Add the Vite plugin to compile .po on the fly. Also remove messages.mjs from git tracking and gitignore it — it's a build artifact generated by lingui compile, not a source file.
Remove the import.meta.env.DEV conditional and Lingui Vite plugin from all configs. All environments import .mjs (pre-compiled by lingui compile during admin build). No Vite plugin needed anywhere.
Add @lingui/babel-plugin-lingui-macro to react() config in all demos so Lingui t`` macros compile correctly during development.
Add a Vite plugin to the EmDash integration that compiles Lingui macros (t``, <Trans>, <Plural>) when the admin is aliased to source in dev mode. Uses enforce: "pre" to run before the React JSX transform. No Babel config needed in consuming sites.
…ecture Reflects the new automatic macro compilation pipeline: tsdown plugin for build, Vite plugin for dev. No consumer Babel config needed.
cba588b to
72032a9
Compare
|
Hi, Updated the SKILL.md to reflect the changes Key commits:
|
Prevents @babel/types from being bundled into dist — it's a dev-time dependency used only by the emdash-lingui-macro Vite plugin.
… e2e These devDependencies were added when the Babel plugin was configured in each consumer's astro.config. Now that the integration handles macro compilation automatically, consumers don't need it.
Variable dynamic imports fail for newly added locales because Vite can't resolve the package-qualified path at analysis time. Glob imports let Vite watch for new catalog files and serve them in dev without a server restart.
This reverts commit a650af5.
Rename lingui-macro-patterns/skill.md to SKILL.md to match project convention. Note in adding-admin-locale that lingui compile is needed after editing .po files to see changes in dev.
- Add locale:compile and locale:copy scripts - Build now runs compile → tsdown → copy → tailwind - Change ./locales/* export from src/ to dist/ for npm publish - Remove locale:check (CI concern, not ours) - Update SKILL.md to match new script names and correct stale claims
ContentList: View published aria-label MenuList: Primary Navigation placeholder MenuEditor: Home placeholder
|
@ascorbic Ready to review |
What does this PR do?
Adds Lingui i18n infrastructure to the admin UI. Zero user-visible change — this is plumbing for future locale support.
@lingui/core,@lingui/react,@lingui/macroI18nProviderwired into the admin shell viaPluginRegistry.pocatalog extracted (1260 strings across 80+ components)t,<Trans>,<Plural>macros.mjsat build time vialingui compile.podirectly via Vite plugin for instant feedbackRelated
Design decisions
SUPPORTED_LOCALES.length > 1, invisible until a second locale ships.@lingui/*versions managed via pnpm catalog.lingui compileruns at admin build time. Consuming sites import.mjs, no Lingui config or Vite plugin needed.Type of change
Checklist
pnpm typecheckpassespnpm --silent lint:json | jq '.diagnostics | length'returns 0pnpm testpasses (or targeted tests for my change)pnpm formathas been runAI-generated code disclosure
Screenshots / test output
pnpm typecheck— all packages passpnpm --filter @emdash-cms/admin test— 709 component tests passpnpm --filter emdash test— 2114 core tests passpnpm test:e2e— 213 e2e tests pass (all 8 shards)pnpm lint:json— 0 diagnosticslingui extract— catalog in sync