Internationalization (i18n)¶
Vectis supports multi-language content at both the backend (product data) and frontend (UI strings). English is the base language; translations are additive overlays.
Backend: Translatable Content¶
Products, categories, brands, and CMS pages store translations in a translations JSONB column:
product.name = "Disposable Vaporizer"
product.translations = {
"es": {"name": "Vaporizador Desechable", "description": "..."}
}
Translatable fields per entity (vectis/core/i18n.py): Product has name, description, seo_title, seo_description; Category/Brand have name, description; Page has title, blocks, seo_title, seo_description.
GraphQL resolvers call resolve_field() to merge base values with locale overrides:
from vectis.core.i18n import resolve_field
def resolve_product_name(product, info):
locale = info.context.get("locale", "en")
return resolve_field(product, "name", locale, product.translations)
The backend resolves locale from: X-Locale header → Accept-Language → "en".
Storefront: UI Strings¶
JSON message files live at $lib/i18n/{en,es}.json. Import t() with dot-notation keys:
<script lang="ts">
import { t } from '$lib/i18n';
</script>
<button>{t('product.add_to_cart')}</button>
<!-- English: "Add to Cart" | Spanish: "Agregar al Carrito" -->
<p>{t('cart.item_count', { count: 3 })}</p>
<!-- Parameter substitution via {placeholder} syntax -->
Missing keys fall back to English, then to the key string itself.
Setting the Locale¶
The root +layout.svelte calls setLocale() with server data:
<script lang="ts">
import { setLocale } from '$lib/i18n';
let { data, children } = $props();
setLocale(data.locale);
</script>
LanguageSwitcher¶
The LanguageSwitcher component sets the vectis_locale cookie and reloads:
<script lang="ts">
import { SUPPORTED_LOCALES, LOCALE_LABELS } from '$lib/i18n';
import { Globe } from '@lucide/svelte';
function switchLocale(locale: string) {
document.cookie = `vectis_locale=${locale};path=/;max-age=${365 * 86400}`;
location.reload();
}
</script>
{#each SUPPORTED_LOCALES as loc}
<button onclick={() => switchLocale(loc)}>{LOCALE_LABELS[loc]}</button>
{/each}
Admin: Help Content¶
The admin uses a parallel JSON structure in $lib/help/{en,es}.json for in-app contextual help. Both files must be kept in sync.
Adding a New Language¶
- Backend — add locale to
channel.supported_languages: - Product translations — add via admin or API mutation
- Storefront — create
$lib/i18n/fr.json, import inindex.ts, add tomessagesmap - Admin help — create
$lib/help/fr.jsonwith the same structure - Constants — add
'fr'toSUPPORTED_LOCALESin$lib/i18n/index.tsandhooks.server.ts
Note
hooks.server.ts only recognizes locales in its SUPPORTED_LOCALES array. Missing locales fall back to English.
Tip
Run grep -r "t('" src/ to find all i18n keys in use, then verify they exist in every locale JSON file.
CMS Block Translations (Decided #145, #146, #156)¶
CMS pages and reusable blocks add a translation tier on top of the channel locale. Each BlockInstance carries a base payload plus a ProductTranslation-style overlay table keyed by locale. The block cache contract (backend/vectis/modules/cms/block_cache.py) defines:
tag_key(block_id, locale)— cache key for the rendered blockoverlay_key(page_id, locale)— cache key for the resolved page → blocks overlayttl_for_block(block_type)— 30 minutes default, 60 seconds for price-bearing blocksinvalidation_tag(block_id, locale?)— Redpanda invalidation payload
When a translation changes the cache invalidates only the affected locale shards, so other locales stay warm. The page-level overlay merges block translations at render time; missing translations fall back to base content.