Skip to content

Updated @tryghost/i18n to support ESM#29144

Merged
9larsons merged 8 commits into
mainfrom
steve/pla-209-i18n-dual-format
Jul 7, 2026
Merged

Updated @tryghost/i18n to support ESM#29144
9larsons merged 8 commits into
mainfrom
steve/pla-209-i18n-dual-format

Conversation

@9larsons

@9larsons 9larsons commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Ref PLA-209.

What & why

@tryghost/i18n was CJS-only with a dynamic require(\../locales/${locale}/${ns}.json`)locale loader. That's thedynamicRequireTargetspattern Rolldown never implemented, so browser bundles built with rolldown-vite **silently drop all non-English locales**. Today's public apps paper over it with a per-appcommonjsOptions.dynamicRequireTargetsshim inapps/_shared/vite-public-app.mjs`.

This makes @tryghost/i18n a dual-format package:

  • require → unchanged CJS Node entry (index.js). Keeps the synchronous, fs-backed theme locale loading that ghost/core's Handlebars {{t}} helper depends on. Zero changes to core.
  • import/browser → static ESM per-namespace registries. Each public app imports @tryghost/i18n/registry/<namespace> and statically bundles ONLY its namespace's 62 locale files — no dynamic require, no shim, works under any bundler.

The dynamic-require loader is isolated in lib/require-loader.js so no ESM/shared path ever imports it.

Per-namespace bundle evidence (critical)

Each public app now bundles only its own namespace's locales. Verified via sourcemap (which registry/*.generated.mjs and how many of each namespace's *.json are included) and by string-presence of non-English translations:

App namespace registry modules bundled locale JSONs bundled non-en locale coverage bundle size
portal portal registry/portal.generated.mjs only 62 × portal.json, 0 others 62/62 2.37 MB
comments-ui comments registry/comments.generated.mjs only 62 × comments.json, 0 others 62/62 0.80 MB
signup-form signup-form registry/signup-form.generated.mjs only 62 × signup-form.json, 0 others 62/62 0.21 MB
sodo-search search registry/search.generated.mjs only 62 × search.json, 0 others 62/62 0.22 MB

Raw per-namespace locale payloads: portal 1.55 MB, ghost 0.50, comments 0.24, signup-form 0.04, search 0.02 (all-5 = 2.35 MB). Portal carries only its 1.55 MB — not the 3.2/2.35 MB of all namespaces. Portal bundle vs main: 2.42 MB → 2.37 MB (equivalent; main's old shim was also single-namespace, but broke under Rolldown — see below).

Both bundlers verified

Isolated harness importing @tryghost/i18n/registry/portal:

Bundler old dynamic-require path new static registry
stock Vite (Rollup 4) works (via shim) works — 62/62 locales
rolldown-vite@7.3.1 2/62 locales — DROPPED (5 modules, 91 KB) works — 62/62 locales (69 modules, 1.71 MB)

This is the whole point: under rolldown-vite the old approach ships a bundle with essentially no translations; the static registry ships all 62.

Core untouched — test proof

  • require('@tryghost/i18n') still resolves to index.js; nl→"Back" verified.
  • core e2e-frontend theme-i18n: 9/9 (boots Ghost, changes locale via Admin API, renders {{t}} against real on-disk theme locale files — the fs-backed path).
  • theme-engine units: theme-i18n 1/1, i18n 10/10, i18next 8/8.
  • email consumers: email-renderer 186/186, email-helpers 15/15, comments-service-emails-renderer 3/3, member-welcome-emails-snapshot (integration) 4/4.
  • @tryghost/i18n own suite: 37/37.
  • App suites with new imports: portal 583, comments-ui 252, sodo-search 11 — all pass under Vitest (which also validates the with {type:'json'} import attribute resolves).

9larsons added 3 commits July 6, 2026 15:51
Option A prototype: require->CJS Node entry (unchanged fs theme loading),
import/browser->static ESM locale registry (no dynamicRequireTargets).

- lib/i18n-core.js: inject generateResources via createGenerateResources(loader)
- lib/require-loader.js: CJS-only dynamic require (isolated from core)
- lib/locale-registry.generated.mjs: 310 static locale imports (codegen)
- scripts/generate-locale-registry.js: codegen
- index.esm.mjs: ESM entry wiring static registry
- package.json: dual exports map (require/import/browser conditions)

Core impact: zero. All ghost/core theme-i18n + i18n tests green.
Ref PLA-209. CJS require path unchanged (fs-backed theme loading); ESM/browser
import path uses static per-namespace locale registries so apps tree-shake to a
single namespace and no bundler needs the dynamicRequireTargets shim.

- lib/i18n-core.js: injectable generateResources loader (no dynamic require)
- lib/require-loader.js: CJS-only dynamic require, isolated from ESM path
- lib/registry/<ns>.generated.mjs: per-namespace static locale registries (codegen)
- lib/registry/<ns>.mjs + ./registry/* export: per-namespace public entries
- index.esm.mjs: default ESM entry (all namespaces, classic signature)
- scripts/: registry codegen, freshness CI gate, types gen, bundle measurement
- build/*.d.ts: real type declarations
- apps: import @tryghost/i18n/registry/<ns>; drop dynamicRequireTargets shim
  from apps/_shared/vite-public-app.mjs and per-app i18nNamespace wiring

Per-app bundles verified to contain only their namespace's 62 locales.
ref PLA-209. Portal errors.test vi.mock target + test-utils and comments
pagination test now import @tryghost/i18n/registry/<ns> to match source.
@cursor

cursor Bot commented Jul 6, 2026

Copy link
Copy Markdown

Bugbot is not enabled for this team, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@nx-cloud

nx-cloud Bot commented Jul 6, 2026

Copy link
Copy Markdown

🤖 Nx Cloud AI Fix

Ensure the fix-ci command is configured to always run in your CI pipeline to get automatic fixes in future runs. For more information, please see https://nx.dev/ci/features/self-healing-ci


View your CI Pipeline Execution ↗ for commit 6ce110d

Command Status Duration Result
nx run @tryghost/admin-x-settings:test:acceptance ✅ Succeeded 10m 4s View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-07-07 14:26:37 UTC

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR migrates Ghost's i18n system to static ESM per-namespace registries. Shared i18n-core logic is refactored into injectable resource-loading factories (createGenerateResources, mergeDefaultExport) used by both CJS (i18n-core.js, require-loader.js) and new ESM (i18n-core.mjs, esm-factory.mjs) implementations. New registry entry modules (comments.mjs, portal.mjs, search.mjs, signup-form.mjs, ghost.mjs) eagerly glob-import locale JSON at build time. Package exports, type declarations (index.d.ts, registry.d.ts), and eslint/vitest configs are updated accordingly. Consuming apps (comments-ui, portal, signup-form, sodo-search) switch to @tryghost/i18n/registry/<namespace> imports and drop the i18nNamespace Vite config option, which is also removed from the shared Vite config helper.

Possibly related PRs

  • TryGhost/Ghost#28777: Both PRs modify the i18n test suite in ghost/i18n/test/i18n.test.js during the Mocha→Vitest transition.
  • TryGhost/Ghost#28976: Introduced the i18nNamespace-based commonjsOptions.dynamicRequireTargets handling in apps/_shared/vite-public-app.mjs that this PR removes and replaces with registry imports.
  • TryGhost/Ghost#29003: Directly related prior changes to ghost/i18n core/browser entrypoints (index.browser.js, i18n-core.js, i18n.browser.js) that this PR builds upon.

Suggested reviewers: EvanHahn

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title accurately summarizes the main change: adding ESM support to @tryghost/i18n.
Description check ✅ Passed The description clearly matches the changeset and explains the dual-format i18n update.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch steve/pla-209-i18n-dual-format

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

E2E Tests Failed

To view the Playwright test report locally, run:

REPORT_DIR=$(mktemp -d) && gh run download 28824390653 -n playwright-report -D "$REPORT_DIR" && npx playwright show-report "$REPORT_DIR"

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/signup-form/src/i18n.d.ts (1)

1-3: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the shorthand declare module '@tryghost/i18n/registry/signup-form'

This ambient declaration resolves that import to any and masks the generated @tryghost/i18n subpath typings, so the signup-form app loses type checking on @tryghost/i18n/registry/signup-form.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/signup-form/src/i18n.d.ts` around lines 1 - 3, Remove the ambient
shorthand declaration for `@tryghost/i18n/registry/signup-form` from the
signup-form i18n typings so it no longer forces that import to any. Update the
i18n.d.ts file to keep only the broader `@tryghost/i18n` declaration, and let the
generated subpath typings provide the registry/signup-form types for proper type
checking.
🧹 Nitpick comments (2)
ghost/i18n/lib/require-loader.js (1)

5-11: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Broad catch masks real JSON errors as "missing locale".

The catch here treats every error the same way — a missing file (MODULE_NOT_FOUND) and a malformed/invalid JSON file (SyntaxError) both silently fall back to English. A corrupted locale file would ship broken translations without any signal. Consider narrowing the catch to only fall back for missing-module errors and rethrowing (or logging) anything else.

♻️ Proposed refactor
 function requireLoader(locale, ns) {
     try {
         return require(`../locales/${locale}/${ns}.json`);
     } catch (err) {
+        if (err.code !== 'MODULE_NOT_FOUND') {
+            throw err;
+        }
         return require(`../locales/en/${ns}.json`);
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ghost/i18n/lib/require-loader.js` around lines 5 - 11, The requireLoader
locale fallback is too broad and hides real parsing failures. Update
requireLoader so the fallback to `../locales/en/${ns}.json` only happens for
missing-module cases from the `require()` of the locale JSON, and do not treat
`SyntaxError` or other non-MODULE_NOT_FOUND errors as a missing translation. Use
the existing `requireLoader(locale, ns)` function and its `catch (err)` path to
either rethrow or log unexpected errors while preserving the English fallback
only for absent locale files.
ghost/i18n/index.esm.mjs (1)

14-29: 🚀 Performance & Scalability | 🔵 Trivial

Keep browser consumers on the subpath entries.

This root entry still statically imports every namespace registry, so any browser consumer that remains on @tryghost/i18n will keep all locale data in the bundle. Please confirm the app migrations and exports leave no browser path on this entry.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ghost/i18n/index.esm.mjs` around lines 14 - 29, The root `@tryghost/i18n` entry
in index.esm.mjs still statically imports all generated registries, which keeps
every locale bundle reachable for browser consumers. Update the package exports
and app migration points so browser usage resolves to the specific subpath
entries instead of this root entry, and keep the registry aggregation confined
to non-browser/internal paths via the existing createI18n and REGISTRIES setup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ghost/i18n/lib/i18n-core.js`:
- Around line 12-33: `createGenerateResources` can still crash when both the
requested locale and the English fallback are missing because
`mergeDefaultExport` is called with `undefined`. Update the `generateResources`
logic to defensively handle a double miss in the `loadResource(locale, ns)` /
`loadResource('en', ns)` path, and ensure `mergeDefaultExport` is only called
with a defined resource (or given a safe empty fallback) so missing namespace
entries degrade gracefully instead of throwing.

In `@ghost/i18n/scripts/generate-types.js`:
- Line 23: The Namespace union in generate-types.js still includes theme even
though the locale registry generator does not emit a backing module for it, so
remove theme from the Namespace type and keep the existing locale factory
signature unchanged. Update the type definition near Namespace so it matches the
namespaces actually produced by generate-locale-registry.js, and ensure any
references to `@tryghost/i18n/registry/theme` are no longer allowed through the
generated types.

---

Outside diff comments:
In `@apps/signup-form/src/i18n.d.ts`:
- Around line 1-3: Remove the ambient shorthand declaration for
`@tryghost/i18n/registry/signup-form` from the signup-form i18n typings so it no
longer forces that import to any. Update the i18n.d.ts file to keep only the
broader `@tryghost/i18n` declaration, and let the generated subpath typings
provide the registry/signup-form types for proper type checking.

---

Nitpick comments:
In `@ghost/i18n/index.esm.mjs`:
- Around line 14-29: The root `@tryghost/i18n` entry in index.esm.mjs still
statically imports all generated registries, which keeps every locale bundle
reachable for browser consumers. Update the package exports and app migration
points so browser usage resolves to the specific subpath entries instead of this
root entry, and keep the registry aggregation confined to non-browser/internal
paths via the existing createI18n and REGISTRIES setup.

In `@ghost/i18n/lib/require-loader.js`:
- Around line 5-11: The requireLoader locale fallback is too broad and hides
real parsing failures. Update requireLoader so the fallback to
`../locales/en/${ns}.json` only happens for missing-module cases from the
`require()` of the locale JSON, and do not treat `SyntaxError` or other
non-MODULE_NOT_FOUND errors as a missing translation. Use the existing
`requireLoader(locale, ns)` function and its `catch (err)` path to either
rethrow or log unexpected errors while preserving the English fallback only for
absent locale files.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ebdcd6e4-fb6d-4b1b-a9ed-f3ce41cf8fd7

📥 Commits

Reviewing files that changed from the base of the PR and between 13c561f and 3e9429a.

📒 Files selected for processing (38)
  • apps/_shared/vite-public-app.mjs
  • apps/comments-ui/src/app.tsx
  • apps/comments-ui/test/unit/components/content/pagination.test.jsx
  • apps/comments-ui/vite.config.mts
  • apps/portal/src/utils/i18n.js
  • apps/portal/test/errors.test.js
  • apps/portal/test/utils/test-utils.jsx
  • apps/portal/vite.config.mjs
  • apps/signup-form/.storybook/preview.tsx
  • apps/signup-form/src/app.tsx
  • apps/signup-form/src/i18n.d.ts
  • apps/signup-form/src/preview.stories.tsx
  • apps/signup-form/vite.config.mts
  • apps/sodo-search/src/app.jsx
  • apps/sodo-search/vite.config.mjs
  • ghost/i18n/eslint.config.mjs
  • ghost/i18n/index.esm.mjs
  • ghost/i18n/lib/esm-factory.mjs
  • ghost/i18n/lib/i18n-core.js
  • ghost/i18n/lib/i18n.browser.js
  • ghost/i18n/lib/i18n.js
  • ghost/i18n/lib/registry/comments.generated.mjs
  • ghost/i18n/lib/registry/comments.mjs
  • ghost/i18n/lib/registry/ghost.generated.mjs
  • ghost/i18n/lib/registry/ghost.mjs
  • ghost/i18n/lib/registry/index.generated.mjs
  • ghost/i18n/lib/registry/portal.generated.mjs
  • ghost/i18n/lib/registry/portal.mjs
  • ghost/i18n/lib/registry/search.generated.mjs
  • ghost/i18n/lib/registry/search.mjs
  • ghost/i18n/lib/registry/signup-form.generated.mjs
  • ghost/i18n/lib/registry/signup-form.mjs
  • ghost/i18n/lib/require-loader.js
  • ghost/i18n/package.json
  • ghost/i18n/scripts/check-registry-fresh.js
  • ghost/i18n/scripts/generate-locale-registry.js
  • ghost/i18n/scripts/generate-types.js
  • ghost/i18n/scripts/measure-bundle-locales.js
💤 Files with no reviewable changes (4)
  • apps/portal/vite.config.mjs
  • apps/signup-form/vite.config.mts
  • apps/sodo-search/vite.config.mjs
  • apps/comments-ui/vite.config.mts

Comment thread ghost/i18n/lib/i18n-core.js
Comment thread ghost/i18n/scripts/generate-types.js Outdated
ref PLA-209. Non-blocking cleanups from adversarial review:

- MINOR-1: nx build target outputs include {projectRoot}/build so generated
  .d.ts types are cached/restored on Nx cache hits (via package.json nx.targets)
- MINOR-2: check-registry-fresh.js regenerates into a temp dir and diffs,
  never mutating tracked lib/registry; fails hard on drift
- MINOR-3: dropped the root '.' ESM entry (all-namespaces footgun) — removed
  import/browser conditions + deleted index.esm.mjs; apps must use /registry/<ns>
- MINOR-4: removed unreachable index.browser.js + lib/i18n.browser.js and their
  test block; retargeted ESM behaviour tests onto per-namespace registry entries
- NIT-2: English floor — mergeDefaultExport(res || {}) so a missing en namespace
  never throws; covered by direct createGenerateResources unit tests
- NIT-3: lint:code globs *.mjs and eslint config lints lib/esm-factory.mjs as ESM

i18n suite 41 tests, 100% coverage. Core i18n (theme-i18n e2e 9/9, theme-engine
units, email tests) green. All 4 public apps build with per-namespace tree-shaking.
@cursor

cursor Bot commented Jul 6, 2026

Copy link
Copy Markdown

Bugbot is not enabled for this team, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

E2E Tests Failed

To view the Playwright test report locally, run:

REPORT_DIR=$(mktemp -d) && gh run download 28826200367 -n playwright-report -D "$REPORT_DIR" && npx playwright show-report "$REPORT_DIR"

ref PLA-209

- comments-ui and signup-form acceptance tests were failing: both apps never
  rendered because the built UMD bundle threw "require is not defined" at load
- the per-namespace registry path (registry/<ns> -> esm-factory.mjs) imported the
  CJS lib/i18n-core.js, whose require('i18next') / require('./locale-data.json')
  leaked verbatim into the browser bundle. This PR had removed the vite
  commonjsOptions that previously transformed ghost/i18n, so nothing rewrote them
- fix keeps the browser graph pure ESM: added lib/i18n-core.mjs (static i18next +
  JSON imports) as the twin of the CJS lib/i18n-core.js, and pointed esm-factory
  at it. No package.json "browser" mapping reintroduced
- the two cores are deliberate twins (Node needs sync CJS, browser needs pure
  ESM); guarded by a CJS/ESM parity test. CJS core logic is byte-identical to
  before, so ghost core's Node path is unchanged
- verified: all 4 public apps build with 0 leaked require/module.exports; i18n
  suite 45 tests / 100% coverage; lint + registry freshness green
@cursor

cursor Bot commented Jul 6, 2026

Copy link
Copy Markdown

Bugbot is not enabled for this team, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

ref PLA-209

- ship the two .d.ts as committed static files (index.d.ts, registry.d.ts) and
  drop scripts/generate-types.js + the build:types step. The "generator" only
  ever wrote two constant strings, so it was a build step producing fixed output
  — the files describe the surface directly now (typechecked clean for consumers)
- build/ is no longer produced; removed it from files + nx build outputs. build
  now just runs build:registry
- removed scripts/measure-bundle-locales.js — an ad-hoc bundle-analysis helper
  that didn't belong in the shipped package
- trimmed the vite-public-app.mjs i18n comment to describe the current registry
  import, without the historical dynamicRequireTargets narrative
@cursor

cursor Bot commented Jul 7, 2026

Copy link
Copy Markdown

Bugbot is not enabled for this team, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
ghost/i18n/registry.d.ts (1)

1-13: 🎯 Functional Correctness | 🔵 Trivial

Generic type doesn't narrow to the module's own namespace.

This declaration file appears to be shared across all @tryghost/i18n/registry/<namespace> subpaths. Since i18n: I18nFactory inherits the call signature (locale?, ns?: Namespace | string, options?), a consumer importing from e.g. @tryghost/i18n/registry/portal can still type-check a call like i18n('en', 'search') even though that namespace's registry module presumably only bundles its own namespace's locale data. Same applies to the standalone generateResources(locales, ns) export — nothing constrains ns to the namespace this specific module was generated for.

Consider whether a lightweight per-namespace parameterization (e.g. templating this .d.ts with the concrete namespace literal during generation) would give callers real type safety here, since the current typing can silently accept mismatched namespace arguments.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ghost/i18n/registry.d.ts` around lines 1 - 13, The registry declaration is
too generic and does not restrict calls to the module’s own namespace. Update
the generated per-namespace typings in i18n/registry.d.ts so the exported i18n
and generateResources signatures are parameterized with the concrete namespace
literal for each subpath (for example, the namespace used by that registry
module), rather than allowing any string. Use the existing i18n/I18nFactory and
generateResources symbols as the insertion points, and ensure the namespace
argument type is narrowed during generation so mismatched namespaces fail
type-checking.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@ghost/i18n/registry.d.ts`:
- Around line 1-13: The registry declaration is too generic and does not
restrict calls to the module’s own namespace. Update the generated per-namespace
typings in i18n/registry.d.ts so the exported i18n and generateResources
signatures are parameterized with the concrete namespace literal for each
subpath (for example, the namespace used by that registry module), rather than
allowing any string. Use the existing i18n/I18nFactory and generateResources
symbols as the insertion points, and ensure the namespace argument type is
narrowed during generation so mismatched namespaces fail type-checking.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f869c793-936a-421c-81dc-febf225cd056

📥 Commits

Reviewing files that changed from the base of the PR and between 1851928 and 565d8a6.

📒 Files selected for processing (4)
  • apps/_shared/vite-public-app.mjs
  • ghost/i18n/index.d.ts
  • ghost/i18n/package.json
  • ghost/i18n/registry.d.ts
✅ Files skipped from review due to trivial changes (1)
  • ghost/i18n/index.d.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/_shared/vite-public-app.mjs
  • ghost/i18n/package.json

ref PLA-209

- the per-namespace registry entries now collect locales via Vite's
  `import.meta.glob('../../locales/*/<ns>.json', {eager, import: 'default'})`
  instead of a committed, generated list of 62 static imports per namespace
- deletes the generator (generate-locale-registry.js), the freshness guard
  (check-registry-fresh.js), all 5 *.generated.mjs + index.generated.mjs, and the
  build:registry / prebuild:check / build scripts + nx build target. i18n is now
  source-only (no build step)
- Vite-coupled by design: every consumer of this browser path builds with Vite
  (rolldown-vite). This is the bundler we're standardising on, so coupling the
  locale collection to it is fine — and import.meta.glob's lazy mode is the
  natural basis for per-locale lazy loading later (PLA-210)
- registry entries must now be Vitest-transformed (not externalized) for the glob
  to resolve; dropped the stale externalization (its CJS i18n-core dedup reason no
  longer applies now the ESM path uses the i18n-core.mjs twin)
- verified: byte-identical output to the generated approach (portal 2363 vs
  2361 KB), per-namespace scoping intact (presence matrix clean), all 4 apps build
  with no require/module/glob leaks, i18n suite 45 tests / 100% coverage
@cursor

cursor Bot commented Jul 7, 2026

Copy link
Copy Markdown

Bugbot is not enabled for this team, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

♻️ Duplicate comments (3)
ghost/i18n/lib/registry/signup-form.mjs (1)

6-15: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Same duplicated registry-building logic as ghost.mjs.

See the extraction suggestion in ghost.mjs — applies identically here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ghost/i18n/lib/registry/signup-form.mjs` around lines 6 - 15, The
registry-building logic in signup-form.mjs duplicates the same locale
registration flow already found in ghost.mjs. Extract the shared
`import.meta.glob` plus `REGISTRY` construction into a common helper and have
`createNamespacedI18n` callers reuse it, so `signup-form.mjs` no longer manually
loops over `modules` and parses locale keys itself.
ghost/i18n/lib/registry/portal.mjs (1)

6-15: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Same duplicated registry-building logic as ghost.mjs.

See the extraction suggestion in ghost.mjs — applies identically here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ghost/i18n/lib/registry/portal.mjs` around lines 6 - 15, The
registry-building logic in portal.mjs is duplicated from ghost.mjs, so extract
the shared locale registry creation into the same reusable helper used there and
have this module reuse it instead of rebuilding REGISTRY inline. Update the
portal registry setup around createNamespacedI18n, import.meta.glob, and the
REGISTRY population loop to delegate to the shared extraction so both modules
stay consistent.
ghost/i18n/lib/registry/search.mjs (1)

6-15: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Same duplicated registry-building logic as ghost.mjs.

See the extraction suggestion in ghost.mjs — applies identically here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ghost/i18n/lib/registry/search.mjs` around lines 6 - 15, The
registry-building logic in the search namespace is duplicated from ghost.mjs;
extract the shared module registry construction into a reusable helper and have
both createNamespacedI18n callers use it. Refactor the REGISTRY initialization
loop that iterates over import.meta.glob results so the locale-to-resource
mapping is centralized, keeping the search.mjs setup aligned with the same
shared implementation used elsewhere.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ghost/i18n/lib/registry/ghost.mjs`:
- Around line 6-15: The REGISTRY-building loop in ghost.mjs is duplicated across
multiple namespace modules and should be centralized in esm-factory.mjs. Add a
shared helper there (next to createNamespacedI18n) that takes the glob modules
object, parses the locale from each filePath, and returns the registry, then
update ghost.mjs to call it instead of constructing REGISTRY inline. Apply the
same helper from portal.mjs, search.mjs, and signup-form.mjs so the locale
parsing and registry creation live in one place.

---

Duplicate comments:
In `@ghost/i18n/lib/registry/portal.mjs`:
- Around line 6-15: The registry-building logic in portal.mjs is duplicated from
ghost.mjs, so extract the shared locale registry creation into the same reusable
helper used there and have this module reuse it instead of rebuilding REGISTRY
inline. Update the portal registry setup around createNamespacedI18n,
import.meta.glob, and the REGISTRY population loop to delegate to the shared
extraction so both modules stay consistent.

In `@ghost/i18n/lib/registry/search.mjs`:
- Around line 6-15: The registry-building logic in the search namespace is
duplicated from ghost.mjs; extract the shared module registry construction into
a reusable helper and have both createNamespacedI18n callers use it. Refactor
the REGISTRY initialization loop that iterates over import.meta.glob results so
the locale-to-resource mapping is centralized, keeping the search.mjs setup
aligned with the same shared implementation used elsewhere.

In `@ghost/i18n/lib/registry/signup-form.mjs`:
- Around line 6-15: The registry-building logic in signup-form.mjs duplicates
the same locale registration flow already found in ghost.mjs. Extract the shared
`import.meta.glob` plus `REGISTRY` construction into a common helper and have
`createNamespacedI18n` callers reuse it, so `signup-form.mjs` no longer manually
loops over `modules` and parses locale keys itself.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0c91a0ac-a023-40ae-88a8-eb3bd08a810f

📥 Commits

Reviewing files that changed from the base of the PR and between 565d8a6 and 90734c5.

📒 Files selected for processing (7)
  • ghost/i18n/lib/registry/comments.mjs
  • ghost/i18n/lib/registry/ghost.mjs
  • ghost/i18n/lib/registry/portal.mjs
  • ghost/i18n/lib/registry/search.mjs
  • ghost/i18n/lib/registry/signup-form.mjs
  • ghost/i18n/package.json
  • ghost/i18n/vitest.config.mjs
🚧 Files skipped from review as they are similar to previous changes (1)
  • ghost/i18n/lib/registry/comments.mjs

Comment thread ghost/i18n/lib/registry/ghost.mjs Outdated
ref PLA-209

- the per-namespace browser entries were copy-pasting the glob->registry loop;
  moved it into esm-factory as i18nFromGlob(globModules, namespace)
- each entry is now just the literal glob pattern + namespace name (the only bits
  Vite requires to be per-file, since import.meta.glob patterns must be literals)
  plus the standard re-exports — everything else is shared
@cursor

cursor Bot commented Jul 7, 2026

Copy link
Copy Markdown

Bugbot is not enabled for this team, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@9larsons 9larsons changed the title Added dual-format @tryghost/i18n with per-namespace ESM locale registries Updated @tryghost/i18n to support ESM Jul 7, 2026
@9larsons 9larsons enabled auto-merge (squash) July 7, 2026 14:26
@9larsons 9larsons merged commit b6889fa into main Jul 7, 2026
84 of 86 checks passed
@9larsons 9larsons deleted the steve/pla-209-i18n-dual-format branch July 7, 2026 14:26
9larsons added a commit that referenced this pull request Jul 7, 2026
ref https://linear.app/ghost/issue/PLA-96

- adds a `rolldown` named catalog (vite: npm:rolldown-vite@7.3.1) and points the 8
  admin-surface apps (posts, stats, admin, admin-x-framework, admin-x-settings,
  admin-x-design-system, shade, activitypub) at catalog:rolldown
- public UMD apps stay on stock vite until their own Rolldown config lands (#29141)
- the known Rolldown interop breakers are already fixed on main: shade Flag ESM
  swap (#29139), @svg-maps/world shim (#29143), @tryghost/i18n dual-format (#29144)
- verified: all 8 apps + the admin shell build clean under Rolldown; react/react-dom
  stay external (no dual-instance regression); only benign bundler-agnostic warnings
9larsons added a commit that referenced this pull request Jul 7, 2026
ref https://linear.app/ghost/issue/PLA-96

- points the 5 public apps (portal, comments-ui, sodo-search, signup-form,
  announcement-bar) at catalog:rolldown, putting the whole repo on Rolldown —
  the prerequisite for moving to Vite 8 (which ships Rolldown as default)
- removes portal's rollupOptions.output.manualChunks:false — Rolldown rejects it
  and it was redundant (UMD lib mode is single-file anyway)
- no i18n locale-bundling plugin needed: the merged @tryghost/i18n dual-format
  work (#29144) already handles per-namespace locale bundling via import.meta.glob,
  superseding this branch's original readdir-based plugin
- verified: all 5 apps build clean under Rolldown, 0 require/module leaks,
  per-namespace locale scoping intact, UMD sizes match the Rollup builds
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant