Overview

This integration supports two i18n routing strategies:

  1. Domain-based routing - Different domains serve different locales (e.g., www.example.com, www.example.de)
  2. Path-based routing - Locale prefix in the URL path (e.g., www.example.com/en-gb/p/123, www.example.com/de-de/p/123)

Path-based routing is ideal when you want to:

  • Serve multiple languages from a single domain
  • Simplify domain management and SSL certificates
  • Allow users to easily switch between languages
  • Maintain SEO benefits with clear locale-specific URLs

How It Works

Locale Detection

The system can extract locales from URL paths using either standard locale codes or custom prefixes:

  1. Standard locale pattern: [a-z]{2}-[a-z]{2} (e.g., en-gb, de-de, fr-fr)
  2. Custom prefix pattern: User-defined URL-safe strings (e.g., deutsch, row, en-mt)

When a user visits a URL, the system follows this detection order:

  1. Direct locale match: If the path starts with a valid locale code or custom prefix, the system uses that locale
  2. Cookie fallback: If no locale in path, checks for a locale preference cookie
  3. Default fallback: Uses the configured fallbackLocale for the domain

Configuration Options

For full config explanations visit the config page. The key properties needed for path based routing are:

PropertyTypeDescription
pathBasedRoutingbooleanEnables path-based routing for the domain
fallbackLocalestringDefault locale to redirect to (format: xx-xx)
localesobjectMap of locale codes to locale configurations
localeCookiestringName of cookie used to store user’s locale preference
customPrefixstringOptional custom URL prefix for each locale (within locale config)

Custom Prefix Configuration

i18n: {
  domains: {
    'www.example.com': {
      pathBasedRouting: true,
      fallbackLocale: 'en-us',
      locales: {
        'en-us': {
          commerce: { endpoint: 'https://api.example.com/en/graphql' }
        },
        'fr-fr': {
          customPrefix: 'francais',
          commerce: { endpoint: 'https://api.example.com/fr/graphql' }
        },
        'de-de': {
          customPrefix: 'german',
          commerce: { endpoint: 'https://api.example.com/de/graphql' }
        }
      }
    }
  },
  localeCookie: 'locale_V6'
}

Features

Automatic Redirects

The system automatically redirects users to appropriate localized URLs:

With standard locale codes:

  • No locale in path + valid cookie: /page/de-de/page (if cookie has de-de)
  • No locale in path + no cookie: /page/en-gb/page (using fallbackLocale)
  • Invalid locale in path: /invalid/page/en-gb/page (using fallbackLocale)

With custom prefixes:

  • No locale in path + valid cookie: /page/deutsch/page (if cookie has de-de with customPrefix: "deutsch")

  • No locale in path + no cookie: /page/english/page (using fallbackLocale with custom prefix)

  • Invalid prefix in path: /invalid/page/english/page (using fallbackLocale)

  • Consistency: The same locale preference works across different configurations

  • Flexibility: You can change custom prefixes without invalidating user cookies

  • Compatibility: The system can switch between standard and custom prefix URLs seamlessly

Multi-tenant Support

Path-based routing works with both single and multi-tenant configurations:

  • Single tenant: One configuration serves all domains
  • Multi-tenant: Different domains can have different locale sets and routing strategies

Mixed tenant configs supported

One tenant can utilise both path-based and domain based routing. The config setup for this would look like:

// config/site.js
import { fetchIcon } from "@thg-altitude/utils";
import siteConfigFile from "../local/tenants/siteone.json";
import siteLangFile from "../local/lang/siteone.en_gb.properties.json";
import siteConfigDeFile from "../local/tenants/siteoneDe.json";
import siteLangDeFile from "../local/lang/siteone.de_de.properties.json";
import siteConfigAtFile from "../local/tenants/siteoneAt.json";
import siteLangAtFile from "../local/lang/siteone.de_at.properties.json";

export default {
domains: ["www.siteone.com", "www.siteone.de"],
tenantInstance: "siteone",
commerce: {
  endpoint: "https://horizon-api.www.siteone.com/en-gb/graphql",
},
kv: [
  {
    key: "siteone",
    namespace: "config",
    local: siteConfigFile,
  },
  {
    key: "siteone.en_gb.properties",
    namespace: "lang",
    local: siteLangFile,
  },
],
icons: {
  search: await fetchIcon("lucide", "search"),
  left: await fetchIcon("lucide", "chevron-left"),
  right: await fetchIcon("lucide", "chevron-right"),
},
i18n: {
  domains: {
    "www.siteone.com": {
      locales: {
        "en-gb": {
          icons: {
            flag: await fetchIcon("circle-flags", "gb"),
          },
        },
      },
      pathBasedRouting: false,
      fallbackLocale: "de-de",
    },
    "www.siteone.de": {
      locales: {
        "de-de": {
          icons: {
            flag: await fetchIcon("circle-flags", "de"),
          },
          commerce: {
            endpoint: "https://horizon-api.www.siteone.com/de-de/graphql",
          },
          kv: [
            {
              key: "siteone",
              namespace: "config",
              local: siteConfigDeFile,
            },
            {
              key: "siteone.de_de.properties",
              namespace: "lang",
              local: siteLangDeFile,
            },
          ],
        },
        "de-at": {
          icons: {
            flag: await fetchIcon("circle-flags", "at"),
          },
          commerce: {
            endpoint: "https://horizon-api.www.siteone.com/de-at/graphql",
          },
          kv: [
            {
              key: "siteone",
              namespace: "config",
              local: siteConfigAtFile,
            },
            {
              key: "siteone.de_at.properties",
              namespace: "lang",
              local: siteLangAtFile,
            },
          ],
        },
      },
      pathBasedRouting: true,
      fallbackLocale: "de-de",
    },
  },
  localeCookie: "locale_V6",
},
};

Advanced Configuration Examples

Combining Domain and Path-Based Routing

You can use both routing strategies within the same configuration:

export default {
  domains: ["www.example.com", "www.example.de"],
  commerce: {
    endpoint: "https://api.global.com/graphql"
  },
  i18n: {
    domains: {
      // Global domain with path-based routing
      "www.example.com": {
        pathBasedRouting: true,
        fallbackLocale: "en-us",
        locales: {
          "en-us": {
            commerce: { endpoint: ... }
          },
          "es-es": {
            commerce: { endpoint: ... }
          },
          "fr-fr": {
            commerce: { endpoint: ... }
          }
        }
      },
      // German domain with domain-based routing
      "www.example.de": {
        pathBasedRouting: false,
        fallbackLocale: "de-de",
        locales: {
          "de-de": {
            commerce: { endpoint: ... }
          }
        }
      },
    },
    localeCookie: "user_locale_v3"
  }
}

The result of this config would be two different domains supporting 4 locales:

  • www.example.com/en-us/
  • www.example.com/es-es/
  • www.example.com/fr-fr/
  • www.example.de

Schema Validation

The configuration is validated against JSON Schema v3 with these constraints:

  • Domain-based routing: Limited to 1 locale per domain when pathBasedRouting: false
  • Path-based routing: Unlimited locales allowed when pathBasedRouting: true
  • Locale format: Must match ^[a-z]{2}-[a-z]{2}$ pattern
  • Custom prefix format: Must be URL-safe strings when provided
  • Required fields: domains, commerce.endpoint, i18n.localeCookie, pathBasedRouting, fallbackLocale
  • KV namespaces: Only “config”, “lang”, or “session” allowed