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_loginmagic-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 persistedpreferredLang - 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:
- about.md
- es/about.md
- terms.md
- es/terms.md
- creator-campaign-checklist.md
- es/creator-campaign-checklist.md
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:
- /_includes/t.html
- /_includes/localized-url.html
- /_includes/language-switcher.html
- /_includes/localized-date.html
- /_includes/localized-datetime.html
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 sectionssettings_field_*_label,settings_field_*_help, andsettings_field_*_placeholderfor editable platform settingssettings_readonly_*_labelandsettings_readonly_*_helpfor platform read-only diagnostics and secret status rowscampaign_field_*_labelandcampaign_field_*_helpfor campaign settings and campaign-owned collectionscampaign_readonly_*_labelandcampaign_readonly_*_helpfor campaign read-only rowssettings_option_*,campaign_option_*, and genericoption_*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
pathvalues; 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 persistpreferredLang=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
preferredLangand 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
Recommended Fork Workflow
- Copy /_data/i18n/en.yml to
/_data/i18n/{lang}.yml. - Add the language to the
i18nblock in /_config.yml. - Add localized public-page routes to
i18n.pages. - Add localized source pages for long-form content such as
/about/,/terms/,/manage/, or curated community index pages where needed. - Verify generated collection routes such as
/es/campaigns/{slug}/and any locale-aware embed routes your deployment exposes. - For admin dashboard changes, add matching
adminkeys in every locale file before shipping. In particular, generated fields need the deterministicsettings_field_*,settings_readonly_*,campaign_field_*, orcampaign_readonly_*keys described above. - 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