Skip to content

feat: v2 translations manager with fallback chains#1294

Open
stevenle wants to merge 10 commits into
mainfrom
claude/v2-translations-manager-4jqzvi
Open

feat: v2 translations manager with fallback chains#1294
stevenle wants to merge 10 commits into
mainfrom
claude/v2-translations-manager-4jqzvi

Conversation

@stevenle

@stevenle stevenle commented Jul 9, 2026

Copy link
Copy Markdown
Member

Implements a complete v2 translations manager system with locale fallback support, replacing the v1 translations system. This feature is gated behind the experiments.v2TranslationsManager flag.

Summary

This PR introduces a new translations management system that stores translations in per-locale Firestore documents with support for locale fallback chains. The system includes:

  • Core translations manager (core/translations-manager.ts): Handles saving, loading, publishing, and unpublishing translations with per-locale document storage
  • Automatic v1→v2 migration (core/translations-migration.ts): Auto-migrates existing v1 translations on dev server startup and before production builds
  • UI pages: New translations manager list and edit pages for managing translations
  • Locale fallback resolution (shared/locale-fallbacks.ts): Implements configurable fallback chains (e.g., en-CAen-GBen)
  • Integration with content publishing: Translations are published atomically with their backing content docs

Key Changes

  • Per-locale document storage: Translations are now stored as individual Firestore documents per locale at Projects/{p}/TranslationsManager/{draft|published}/Translations/{id}:{locale}, replacing the flat v1 structure
  • Locale fallback chains: New i18n.fallbacks config allows defining fallback locales (e.g., regional variants falling back to base language)
  • Atomic publishing: When publishing a content doc, its translations are published in the same batch, ensuring consistency
  • Batch operation management: Handles Firestore's 500-op batch limit by splitting large operations across multiple batches
  • Query chunking: Breaks large in queries into 10-item chunks to respect Firestore limits
  • Migration state tracking: Uses a migration state document to ensure v1→v2 migration runs only once per project
  • Browser-side data layer: New ui/utils/translations-manager.ts mirrors the Firestore paths and operations for the UI
  • Comprehensive test coverage: Includes tests for translations manager, migration, publishing integration, and batch request resolution

Implementation Details

  • Translations are hashed using SHA-1 for efficient storage and lookup
  • The system supports both doc-backed translations (extracted from content fields) and manually-added translations
  • Draft and published translations are kept separate, with publish operations copying draft docs to published
  • Unpublish operations delete published docs and clear published metadata from draft docs
  • The migration preserves v1 data as a backup while creating v2 documents
  • Locale fallback resolution is breadth-first and case-insensitive

https://claude.ai/code/session_01Pw4Wrj1oL9DuVbKoXasBTE

claude and others added 9 commits July 9, 2026 10:04
Adds an `i18n.fallbacks` config option for declaring locale fallback
chains for translations (e.g. en-CA -> en-GB -> en), along with a shared
resolveLocaleFallbacks() helper (server + browser safe) and unit tests.
Also surfaces i18n.defaultLocale/fallbacks and the v2TranslationsManager
experiment flag on the CMS UI's window.__ROOT_CTX types.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pw4Wrj1oL9DuVbKoXasBTE
- Fix the inverted experiments.v2TranslationsManager check in
  getTranslationsManager() and add isV2TranslationsEnabled().
- TranslationsManager.loadTranslations(): use array-contains-any for tag
  filters, chunk id `in` queries (10/chunk, parallel), and merge results
  in ids order for deterministic precedence (doc-specific last/wins).
- saveTranslations(): add {linkedSheet, modifiedBy} options, chunk batch
  commits at 400 ops, and remove the misleading inner update loop.
- Add addPublishTranslationsOps(), publishTranslationsBulk(),
  getTranslationsLocaleDocs() and a public translationsForLocaleV2()
  fallback-chain resolver; loadTranslationsForLocale() now defaults its
  fallback chain from i18n.fallbacks.
- publishDocs()/publishScheduledDocs() (and releases via publishDocs)
  publish each doc's draft translations in the same write batch as the
  doc; unpublishDocs() deletes the published locale docs and clears the
  draft docs' published metadata. All flag-gated.
- Rewrite BatchRequest/BatchResponse for the per-locale schema: fix the
  translations double-fetch, auto-collect translations ids from query
  results when translate is enabled, fetch exact locale-doc refs expanded
  through i18n.fallbacks (chunked getAll) with an id-query fallback when
  locales are unknown, and resolve fallback chains in getTranslations().
- Remove the orphaned incompatible TranslationsDoc schema and the unused
  dbTranslationsPath()/dbTranslationsRef() helpers.
- Add Firestore-emulator tests for the manager, publish integration, and
  BatchRequest translation resolution.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pw4Wrj1oL9DuVbKoXasBTE
Adds migrateV1TranslationsIfNeeded(), which copies v1 translations into
per-locale v2 docs and publishes them (v1 saves were immediately live),
leaving the v1 data untouched as a backup. Migration state is tracked in
a Projects/{p}/TranslationsManager/migration doc: after the first
successful run each boot costs a single read, a Firestore transaction
guards against concurrent runs, and a stale running lock expires after
10 minutes.

The CMS plugin (gated on experiments.v2TranslationsManager) awaits the
migration in the preBuild hook — failing the build with an ADC
credentials hint — and fire-and-forgets it on dev server startup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pw4Wrj1oL9DuVbKoXasBTE
When experiments.v2TranslationsManager is enabled, createRoute's SSR
handler and SSG props load translations from the v2 TranslationsManager
(using the same ids as the v1 tags, honoring the draft/published mode
from ?preview=true) and flatten them per locale through the
i18n.fallbacks chain. The missing-translations check reads the doc's
draft v2 translations the same way. Flag off keeps v1 reads unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pw4Wrj1oL9DuVbKoXasBTE
Adds ui/utils/translations-manager.ts (Firebase Web SDK, mirroring the
core per-locale doc paths): list/load/save/publish/unpublish/delete for
translations docs, batch-op helpers shared with doc publishing, and a
testV2TranslationsEnabled() flag helper. Locale docs are grouped by the
doc data's id field (never parsed from the ':' doc key, which is
ambiguous with '--' slugs), and per-id modified/published timestamps
drive a hasUnpublishedChanges status.

When the flag is enabled, cmsPublishDocs() publishes each doc's draft
translations in the same write batch as the doc (now with op-counted
chunked commits, <=450 ops/batch, each doc + its translations kept in
one chunk, and upfront lock/archive validation so chunked commits can't
partially publish); the releases UI inherits this. cmsUnpublishDoc()
clears the doc's published translations and cmsDeleteDoc() removes the
doc's translations entirely. PublishDocModal notes that translations
publish together with the doc.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pw4Wrj1oL9DuVbKoXasBTE
Adds a TranslationsManagerPage list view (left tree grouping ids by
their prefix, with non-prefixed ids like 'common' under a Global group;
table with locale count, status badge, modified/published timestamps,
search, and an Edit/Publish/Delete row menu) and a
TranslationsManagerEditPage editor (source x translation-language grid
whose rows are the union of the doc's extracted strings and the stored
draft strings — stored-only rows badged 'unused' — with an explicit
Save, a confirm-modal Publish, an 'Add source string' input for non-doc
ids, and a draft/published status badge).

When experiments.v2TranslationsManager is on, /cms/translations routes
to the manager and /cms/translations/:translationsId* (wildcard, so
existing {collection}/{slug} deep links keep working) routes to the
editor; flag off keeps the four v1 translations routes. Conditional
route children are arrays (preact-iso's Router only flattens arrays).
The DocEditor Localization button navigates to the manager editor when
the flag is on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pw4Wrj1oL9DuVbKoXasBTE
Adds a minor changeset for @blinkk/root and @blinkk/root-cms (noting
the TranslationsDoc export removal), runs the admin-SDK emulator tests
against a separate emulator project id so security.test.ts's
clearFirestore() can't race them, and applies lint formatting.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pw4Wrj1oL9DuVbKoXasBTE
@stevenle stevenle force-pushed the claude/v2-translations-manager-4jqzvi branch from ffd8040 to dd713c0 Compare July 9, 2026 17:04
@stevenle

stevenle commented Jul 9, 2026

Copy link
Copy Markdown
Member Author

@jeremydw here's the cms ui for the new translations manager, let me know if you want to review it at all or if i should go ahead and submit. it's gated behind an "experiments" flag.

@stevenle

stevenle commented Jul 9, 2026

Copy link
Copy Markdown
Member Author

Screenshots:

Screenshot 2026-07-09 at 10 35 48 AM Screenshot 2026-07-09 at 10 35 54 AM

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.

3 participants