Skip to content

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

  1. Backend — add locale to channel.supported_languages:
    channel.supported_languages = ["en", "es", "fr"]
    
  2. Product translations — add via admin or API mutation
  3. Storefront — create $lib/i18n/fr.json, import in index.ts, add to messages map
  4. Admin help — create $lib/help/fr.json with the same structure
  5. Constants — add 'fr' to SUPPORTED_LOCALES in $lib/i18n/index.ts and hooks.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.