Internationalization (i18n)

This document records the current localization structure for The Pool and the supported workflow for adding languages in a fork.

The immediate shipped secondary locale is Spanish, but the real goal is to make future localization straightforward without custom code for the shared site-owned surfaces.

What Exists Now

The current i18n model covers:

  • structured locale config in _config.yml
  • shared translation catalogs in _data/i18n/
  • locale-aware URL helpers and a shared footer language switcher
  • localized public routes for:
    • /
    • /about/
    • /terms/
    • /campaigns/:slug/
    • /embed/campaign/
    • /pledge-success/
    • /pledge-cancelled/
    • /manage/
    • /community/
    • /admin/
    • /creator-campaign-checklist/
    • supporter community pages
  • localized site-owned runtime copy for cart, checkout, Manage Pledge, admin auth/dashboard/report previews/supporter browsing/content preview, community flows, campaign countdowns (including screen-reader remaining-time status), upcoming-campaign launch reminders, hero-video/loading states and embed titles, supporter-community teaser chrome, diary tabs, production-phase controls, gallery labels, live-stats status text, and the campaign embed builder/widget
  • admin language switching preserves safe view state such as campaign filters and hashes, but strips admin_login magic-link tokens before linking to the alternate language
  • localized campaign-add-on section labels in both cart and Manage Pledge, plus checkout helper copy such as cart-button summaries, tax-location labels, and hosted-checkout next-step copy
  • localized campaign footer switching and localized campaign date formatting for public campaign chrome
  • localized campaign share-link labels plus state-aware share intent text for upcoming, live, funded, and ended campaigns
  • localized Worker supporter emails and localized /manage/ / /community/:slug/ links based on persisted preferredLang
  • localized Worker campaign share-card routes such as /share/campaign/:slug.png?lang=es
  • localized public metadata and structured-data language hints on public pages and localized campaign pages

English remains the default locale. Spanish is the seeded secondary locale.

Config Model

The canonical locale config lives in _config.yml:

i18n:
  default_lang: en
  supported_langs:
    - en
    - es
  language_labels:
    en: English
    es: Español
  pages:
    home:
      en: /
      es: /es/
    about:
      en: /about/
      es: /es/about/
    terms:
      en: /terms/
      es: /es/terms/
    manage:
      en: /manage/
      es: /es/manage/
    admin:
      en: /admin/
      es: /es/admin/
    community_index:
      en: /community/
      es: /es/community/
    creator_campaign_checklist:
      en: /creator-campaign-checklist/
      es: /es/creator-campaign-checklist/
    pledge_success:
      en: /pledge-success/
      es: /es/pledge-success/
    pledge_cancelled:
      en: /pledge-cancelled/
      es: /es/pledge-cancelled/

This model intentionally keeps localization predictable for forks:

  • one default language
  • one supported-language list
  • one display-label map
  • one curated route map for shared locale-aware pages

Campaign pages are the main exception: they are generated from the campaign collection, so their localized routes are derived from campaign slugs and generated localized_paths rather than hand-written i18n.pages entries.

Translation Sources

1. Shared UI and runtime copy

Shared site-owned strings live in one YAML file per locale:

This includes:

  • nav labels
  • buttons
  • status labels
  • progress/meta text
  • cart / checkout / Manage Pledge runtime copy
  • admin dashboard tabs, filters, generated form labels, option labels, help text, media-upload copy, report/supporter/analytics/marketing copy, and content-editor controls
  • campaign add-on section labels and hosted/custom-checkout helper copy
  • community runtime copy
  • campaign countdown / hero-video / supporter-community / diary / production-phase / gallery / live-stats copy
  • upcoming-campaign launch reminder form, status, and email copy
  • campaign share labels and share-intent messages for Bluesky, X, Threads, Facebook, SMS, and email
  • campaign embed builder/widget copy
  • Worker supporter email copy

English is the canonical source file and fallback locale.

2. Long-form authored pages

Long-form page copy should use localized source files rather than trying to force every paragraph into YAML.

Examples:

That same pattern should be used for future content-heavy pages.

Routing Model

The site uses a static locale-prefix model:

  • default language stays on canonical URLs
    • /
    • /about/
    • /terms/
    • /manage/
    • /community/
    • /creator-campaign-checklist/
  • non-default languages live under a locale prefix
    • /es/
    • /es/about/
    • /es/terms/
    • /es/campaigns/{slug}/
    • /es/embed/campaign/
    • /es/manage/
    • /es/community/
    • /es/creator-campaign-checklist/

This keeps the Jekyll/GitHub Pages deployment model simple and predictable.

Campaign collection routes are now generated in both locales, so the footer language switcher can remain available on campaign pages instead of disappearing or linking back to the default-language route.

Helpers and Runtime Plumbing

Shared locale helpers:

Runtime locale payloads:

Admin dashboard localization:

  • static admin shell copy in /_layouts/admin.html uses the shared Liquid translation helper
  • runtime admin copy is included in the full admin catalog emitted by runtime-messages-json.html
  • generated Settings and Campaigns fields come from Worker JSON, but assets/js/admin-dashboard.js localizes them with deterministic keys:
    • settings_section_* for top-level settings sidebar sections
    • settings_field_*_label, settings_field_*_help, and settings_field_*_placeholder for editable platform settings
    • settings_readonly_*_label and settings_readonly_*_help for platform read-only diagnostics and secret status rows
    • campaign_field_*_label and campaign_field_*_help for campaign settings and campaign-owned collections
    • campaign_readonly_*_label and campaign_readonly_*_help for campaign read-only rows
    • settings_option_*, campaign_option_*, and generic option_* keys for select/checkbox options
  • content editor controls, staged media-upload status/errors, media settings labels/help text, gallery settings, and gallery hover-caption controls are also runtime-localized; update English and Spanish together when adding a new admin media control
  • the Worker should keep returning stable field path values; the client derives i18n keys from those paths so forks can add fields without duplicating render code
  • creator-authored campaign data, diary bodies, add-on names, decision options, and other saved content are displayed as authored; the shared catalog only localizes the surrounding dashboard UI

Important current behavior:

  • the footer language switcher is the shared locale switch surface
  • it preserves the current query string and hash
  • tokenized URLs such as /manage/?t=... can switch to /es/manage/?t=... without dropping pledge access
  • Stripe is initialized with the current locale where supported, so Stripe-owned field labels and validation can localize too
  • cart trigger summaries and tax-location helper copy come from the shared locale catalog, so custom checkout remains translatable without separate hardcoded strings
  • public campaign templates now route shared chrome strings through locale data instead of hardcoded English where practical, including the hero video CTA/loading state, hero-video embed titles, supporter-community teaser copy, launch reminder form/status copy, diary chrome, production-phase labels, gallery accessibility labels, campaign sidebar pledge copy, countdown screen-reader status text, and localized campaign dates
  • campaign pages use localized share labels and state-aware share-intent text while leaving creator-authored campaign titles and blurbs as authored
  • campaign pages now expose localized footer language switching through generated campaign localized_paths
  • the hosted campaign embed builder and widget pull their builder/runtime strings from the shared locale catalog and preserve locale-aware campaign return links
  • public metadata and JSON-LD now also follow the active page language, localized home route, and supported-language set so localized pages do not emit English-only crawl hints by accident
  • localized long-form pages such as About and Terms still use source-file translations, so doc/content sweeps need to keep those locale-specific files in sync manually

Worker Email Behavior

Worker supporter emails reuse the same locale catalog and persisted preferredLang.

Relevant files:

Practical behavior:

  • if no locale preference is captured, emails fall back to English
  • if a supporter pledges or manages from /es/..., the Worker can persist preferredLang=es
  • supporter emails and magic-link URLs then use the Spanish route model, such as /es/manage/?t=...
  • launch reminder emails use the signup’s persisted preferredLang and link back to the localized campaign route when one is available
  • campaign share cards can also be requested in a locale-aware way, such as /share/campaign/sunder.png?lang=es

What a Locale YAML File Does and Does Not Do

Adding a new locale YAML file is enough for:

  • shared site chrome
  • shared runtime/browser messages
  • Worker supporter-email copy

It is not enough for a fully translated site by itself.

Full language support also needs:

  • the language added to i18n.supported_langs
  • its label added to i18n.language_labels
  • localized routes added to i18n.pages
  • localized source pages for any long-form content you actually want translated
  1. Copy /_data/i18n/en.yml to /_data/i18n/{lang}.yml.
  2. Add the language to the i18n block in /_config.yml.
  3. Add localized public-page routes to i18n.pages.
  4. Add localized source pages for long-form content such as /about/, /terms/, /manage/, or curated community index pages where needed.
  5. Verify generated collection routes such as /es/campaigns/{slug}/ and any locale-aware embed routes your deployment exposes.
  6. For admin dashboard changes, add matching admin keys in every locale file before shipping. In particular, generated fields need the deterministic settings_field_*, settings_readonly_*, campaign_field_*, or campaign_readonly_* keys described above.
  7. Run the local stack and verify both the shared UI copy and localized routes, including /admin/ and /es/admin/ when admin UI strings changed:
npm run podman:doctor
./scripts/dev.sh --podman

Current Boundaries

Still intentionally out of scope for this model:

  • automatic translation of creator-authored campaign bodies, diary entries, or community post content
  • automatic translation of saved admin-authored campaign settings, add-on names, decision options, referral labels, or other content stored as campaign/platform data
  • locale-specific tax, shipping, or pricing rules
  • an in-repo machine-translation pipeline