Internationalisation
Coil resolves locale on the server, but the public apps currently demonstrate two different copy-delivery patterns on top of that runtime model.
Use this page to keep those patterns straight:
- Shoppr shows server-resolved locale, host-aware sites, and locale-aware links
- Gitly shows customer-owned frontend dictionaries on top of localized routes
The Runtime Part Is Already Real
Shoppr’s manifest is the shortest concrete example:
[i18n]
default_locale = "en-GB"
supported_locales = ["en-GB", "fr-FR", "pl-PL"]
localized_routes = true
[[sites]]
id = "shoppr-fr"
canonical_domain = "fr.localhost"
default_locale = "fr-FR"
supported_locales = ["en-GB", "fr-FR", "pl-PL"]
That contract means the runtime already resolves:
- site from the host
- locale inside that site
- route and links under that site-and-locale context
So locale is not a browser-only afterthought.
Pattern 1: Server-Resolved Locale Values
This is the base pattern templates should prefer for request-critical and SEO-relevant output:
<html xmlns:coil="https://coil.rs" coil:attr="lang=${locale}">
<a coil:attr="href=${links.home}">
<span coil:text="${site.brand_name}">Brand</span>
</a>
</html>
That is the Shoppr-style boundary:
- the runtime resolves
locale - the runtime shapes
links.* - the template consumes already-localized routing context
Use this pattern for:
- alternate locale links
- page shells
- account and admin surfaces
- checkout and confirmation pages
Pattern 2: Customer-Owned Frontend Dictionaries
Gitly intentionally uses a narrower, app-owned pattern for copy:
<h1 data-i18n="actions.title">Workflow runs</h1>
<p data-i18n="actions.mockBody">
This browser-side loop simulates a scheduled refresh so the Actions demo shows visible cadence.
</p>
And its frontend script applies the dictionary after the page is rendered:
function applyCopy(locale) {
const messages = translations[locale] || translations["en-GB"];
document.querySelectorAll("[data-i18n]").forEach((node) => {
const key = node.getAttribute("data-i18n");
const value = messages.copy[key] || messages[key];
if (value) node.textContent = value;
});
}
That demonstrates a real customer choice, not a platform limit.
Use this pattern when:
- the strings are product-shell or demo copy
- the app wants to own the dictionary format entirely
- client-side hydration is acceptable
Do not mistake it for “the Coil i18n API.” It is Gitly’s chosen implementation.
What Coil Does Not Yet Ship As A Customer API
Current honest state:
- there is no built-in customer translation file convention
- there is no template-native
t("key")helper - the public demos do not yet wire a checked-in customer translation catalog into server-rendered page copy
So if you need translation dictionaries today, define them in customer code and document that choice clearly.
What To Copy Right Now
If you need server-first locale behavior
Copy the Shoppr pattern:
- declare locales and sites in
app.toml - declare runtime locale policy in
platform.dev.toml - consume
locale,site.*, andlinks.*in templates
If you need app-owned UI copy dictionaries
Copy the Gitly pattern:
- keep localized routes in app/runtime config
- keep dictionary keys in app-owned assets
- apply those strings in frontend code
- document clearly that this is a customer convention
Common Mistakes
Claiming Gitly is “server-rendered translated copy”
It is not. Gitly’s route/locale resolution is runtime-backed, but the visible translated copy is currently applied in frontend JS.
Hardcoding locale-prefixed paths in templates
Use runtime-generated links instead.
Treating locale as only a text problem
Locale also affects routes, canonical URLs, and alternate links.
Confusing site with locale
Site is the host/brand/public-surface boundary. Locale is the language/formatting layer inside that surface.