Add Lunaria translation tracking and i18n dashboard#461
Conversation
Configure Lunaria to track PO files directly using dictionary mode (via lunariajs/lunaria#178). Add Spanish as first translation locale. - Add lunaria.config.json with PO dictionary tracking - Add Spanish locale to Lingui config and extract empty catalog - Add locale:extract and locale:compile scripts to root
Static HTML dashboard generated from Lunaria, deployed as Cloudflare Workers static assets. Shows per-locale completion with progress bars, missing keys, and GitHub edit links.
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| 🔵 In progress View logs |
emdash-playground | 8f324d1 | Apr 11 2026, 07:24 PM |
Scope checkThis PR changes 688 lines across 11 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. |
|
@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: |
There was a problem hiding this comment.
Pull request overview
Adds translation tracking for the admin UI using Lunaria (PO “dictionary” mode) and introduces a deployable static i18n status dashboard, plus supporting workspace/scripts and a PR-commenting GitHub Action.
Changes:
- Add Lunaria configuration + GitHub Action workflow for translation-overview reporting.
- Add Spanish (
es) PO catalog support and expand Lingui locale configuration. - Add a new
i18n/workspace package that generates & deploys a static translation-progress dashboard.
Reviewed changes
Copilot reviewed 10 out of 11 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-workspace.yaml | Adds i18n/ as a workspace package. |
| pnpm-lock.yaml | Locks new Lunaria dependency graph and related resolution changes. |
| package.json | Adds Lunaria core dependency + root locale scripts delegating to admin. |
| lunaria.config.ts | Defines Lunaria repo/source locale/locales and tracked PO file patterns. |
| lingui.config.ts | Adds es to supported locales for PO extraction/compilation. |
| packages/admin/src/locales/es/messages.po | Adds initial Spanish PO catalog (currently untranslated entries). |
| i18n/package.json | Defines dashboard build/deploy scripts. |
| i18n/build.ts | Generates i18n/dist/index.html + status.json from Lunaria status. |
| i18n/wrangler.jsonc | Wrangler config to deploy the dashboard to i18n.emdashcms.com. |
| i18n/.gitignore | Ignores generated dist/ artifacts. |
| .github/workflows/lunaria.yml | Adds PR-target workflow to run Lunaria overview action. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| function localeCard(s: LocaleStatus): string { | ||
| return ` | ||
| <details class="locale"> | ||
| <summary> | ||
| <strong>${s.label} <span class="lang">${s.lang}</span></strong> | ||
| <span class="stats">${s.completedKeys}/${s.totalKeys} · ${s.percentComplete}%</span> | ||
| <div class="bar"><div class="fill ${barClass(s.percentComplete)}" style="width:${s.percentComplete}%"></div></div> | ||
| </summary> | ||
| <div class="links"> | ||
| <a href="${s.editUrl}">Edit translation</a> | ||
| ${s.historyUrl ? `· <a href="${s.historyUrl}">History</a>` : ""} |
There was a problem hiding this comment.
The generated HTML interpolates dynamic values into markup/attributes without escaping (e.g. locale label/lang in text and editUrl/historyUrl inside href). If any of these contain characters like <, &, or ", the dashboard HTML can break and could become an injection vector. Use HTML escaping for all inserted text, and an attribute-safe escape for href values (or build DOM with URL objects + encodeURIComponent for paths) before writing index.html.
| function localeCard(s: LocaleStatus): string { | |
| return ` | |
| <details class="locale"> | |
| <summary> | |
| <strong>${s.label} <span class="lang">${s.lang}</span></strong> | |
| <span class="stats">${s.completedKeys}/${s.totalKeys} · ${s.percentComplete}%</span> | |
| <div class="bar"><div class="fill ${barClass(s.percentComplete)}" style="width:${s.percentComplete}%"></div></div> | |
| </summary> | |
| <div class="links"> | |
| <a href="${s.editUrl}">Edit translation</a> | |
| ${s.historyUrl ? `· <a href="${s.historyUrl}">History</a>` : ""} | |
| function escapeHtmlAttribute(s: string): string { | |
| return escapeHtml(s); | |
| } | |
| function localeCard(s: LocaleStatus): string { | |
| return ` | |
| <details class="locale"> | |
| <summary> | |
| <strong>${escapeHtml(s.label)} <span class="lang">${escapeHtml(s.lang)}</span></strong> | |
| <span class="stats">${s.completedKeys}/${s.totalKeys} · ${s.percentComplete}%</span> | |
| <div class="bar"><div class="fill ${barClass(s.percentComplete)}" style="width:${s.percentComplete}%"></div></div> | |
| </summary> | |
| <div class="links"> | |
| <a href="${escapeHtmlAttribute(s.editUrl)}">Edit translation</a> | |
| ${s.historyUrl ? `· <a href="${escapeHtmlAttribute(s.historyUrl)}">History</a>` : ""} |
| "build": "cd .. && node --experimental-strip-types i18n/build.ts", | ||
| "deploy": "node --run build && wrangler deploy" | ||
| }, | ||
| "dependencies": { |
There was a problem hiding this comment.
This package lists @types/node under dependencies, but it's a types-only dev-time dependency. Keeping it in dependencies can unnecessarily bloat installs and is inconsistent with other workspace packages (which place @types/node in devDependencies). Move @types/node to devDependencies here.
| "dependencies": { | |
| "devDependencies": { |
| "@changesets/changelog-github": "^0.5.2", | ||
| "@changesets/cli": "^2.29.8", | ||
| "@e18e/eslint-plugin": "^0.2.0", | ||
| "@lunariajs/core": "https://pkg.pr.new/lunariajs/lunaria/@lunariajs/core@83617cc", |
There was a problem hiding this comment.
@lunariajs/core is being pulled from pkg.pr.new (a PR tarball URL). This can reduce reproducibility and can break installs if the tarball is GC’d or the service is unavailable. Prefer a published npm version (or a pinned git URL/commit in a registry you control) once the upstream change is released, and document the rationale if this must stay temporary.
| "@lunariajs/core": "https://pkg.pr.new/lunariajs/lunaria/@lunariajs/core@83617cc", | |
| "@lunariajs/core": "REPLACE_WITH_EXACT_PUBLISHED_NPM_VERSION", |
| vitest: | ||
| specifier: 'catalog:' | ||
| version: 4.0.18(@types/node@24.10.13)(@vitest/browser-playwright@4.0.18)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) | ||
| version: 4.0.18(@types/node@24.10.13)(@vitest/browser-playwright@4.0.18)(@vitest/ui@4.0.17)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) |
There was a problem hiding this comment.
The lockfile now records vitest@4.0.18 being resolved with @vitest/ui@4.0.17. Since Vitest typically expects the UI package to match its own patch version, this can cause pnpm peer-dependency warnings and potentially break vitest --ui. Consider aligning @vitest/ui to the same version as vitest (or removing it if it's not needed) to keep the dependency graph consistent.
| version: 4.0.18(@types/node@24.10.13)(@vitest/browser-playwright@4.0.18)(@vitest/ui@4.0.17)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) | |
| version: 4.0.18(@types/node@24.10.13)(@vitest/browser-playwright@4.0.18)(@vitest/ui@4.0.18)(jiti@2.6.1)(jsdom@26.1.0)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) |
| "deploy": "node --run build && wrangler deploy" | ||
| }, | ||
| "dependencies": { | ||
| "@types/node": "catalog:" |
There was a problem hiding this comment.
The deploy script calls wrangler deploy, but this package doesn’t declare wrangler in dependencies/devDependencies. In other workspace packages, wrangler is explicitly listed so scripts work in isolation; consider adding it here (likely as a devDependency) to avoid relying on incidental hoisting from other workspaces.
| "@types/node": "catalog:" | |
| "@types/node": "catalog:" | |
| }, | |
| "devDependencies": { | |
| "wrangler": "^4.11.1" |
* Add Lunaria translation tracking with PO dictionary support Configure Lunaria to track PO files directly using dictionary mode (via lunariajs/lunaria#178). Add Spanish as first translation locale. - Add lunaria.config.json with PO dictionary tracking - Add Spanish locale to Lingui config and extract empty catalog - Add locale:extract and locale:compile scripts to root * Switch Lunaria config to TypeScript with defineConfig * Fix Lunaria config: include only source file, not all PO files * Add translation status dashboard deployable to i18n.emdashcms.com Static HTML dashboard generated from Lunaria, deployed as Cloudflare Workers static assets. Shows per-locale completion with progress bars, missing keys, and GitHub edit links. * Move Lunaria build/deploy scripts into i18n/ package * Add Lunaria GitHub Action for PR translation impact comments * Add i18n workspace package, update lockfile * style: format * Replace Spanish with German as first translation locale * fix ts * style: format * Add wrangler --------- Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com>
What does this PR do?
Adds translation infrastructure for the admin UI using Lunaria with PO dictionary support (via lunariajs/lunaria#178).
lunaria.config.ts) — tracks PO catalogs with dictionary mode, detecting per-key completioni18n/) — static HTML dashboard showing per-locale progress bars, missing keys, and GitHub edit links. Deployable to i18n.emdashcms.com via Cloudflare Workerspull_request_targetfor fork PRs)locale:extractandlocale:compiledelegate to the admin packageType of change
Checklist
pnpm typecheckpassespnpm --silent lint:json | jq '.diagnostics | length'returns 0pnpm formathas been runAI-generated code disclosure