Locale Resolution and i18n
Relevant source files
The following files were used as context for generating this wiki page:
Locale resolution and internationalization (i18n) within this platform are designed for a multi-tenant environment where every shop (tenant) manages its own set of supported languages and regional settings. Rather than a global platform-wide list, locales are resolved dynamically at request time based on the specific shop's configuration and commerce provider data. This ensures that the storefront remains consistent with the merchant's backend settings without requiring manual synchronization or redeploys when locales change.
Sources: CONTEXT.md:21-25, README.md:19-20, apps/storefront/README.md:143-147
Architecture and Core Concepts
The system centers around a Locale (often formatted as xx-XX), which is a language/region tag carried in the URL path. In the storefront, a Locale class utility provides helpers for managing these tags, while the underlying value is passed into every data-fetching helper to ensure tenant-aware responses.
Locale Fallback Chain
When a request is made, the middleware resolves the locale by consulting a specific hierarchy. If a locale is not explicitly provided in the URL path, the system follows a fallback chain:
- Request Locale: The locale identified in the inbound request path.
- Shop Default: The fallback defined on the individual
Shoprecord (stored inshop.i18n.defaultLocale). - Platform Last-Resort: A hardcoded safety net (typically
en-US) used only if the shop record is malformed.
Sources: CONTEXT.md:27-38, CONTEXT.md:329-335, apps/storefront/README.md:143-148
Logic Flow for Locale Resolution
The following diagram illustrates how a request locale is determined during the middleware processing phase.
This flowchart describes the logic used to ensure a valid locale is assigned to every request, terminating at the tenant's default if no specific locale is valid.
Sources: CONTEXT.md:30-38, apps/storefront/src/app/[domain]/[locale]/countries/page.tsx:75-92
CMS Localization and Shredding
The CMS layer uses a "shredding" approach for localized content. Instead of duplicating entire documents for different languages, individual fields are wrapped in a LocalizedBucket.
Data Structures
- LocalizedBucket: A dictionary where keys are locale codes and values are the content for that locale (e.g.,
{"en-US": "Hello", "de-DE": "Hallo"}). - Localized Patterns: Wildcarded paths (e.g.,
blocks.*.body) used by the Convex backend to identify which fields in a document require locale-based resolution.
Sources: packages/convex/convex/cms/localization.test.ts:74-78, packages/cms/scripts/cms-gen.test.ts:46-55
Reassembly and Fallbacks
When the storefront reads a document from Convex, it "reassembles" the localized fields into a flat object for the active locale. If the requested locale's bucket is empty, it walks the fallback chain (request → shop → platform) to find the first available value.
Sources: packages/convex/convex/cms/localization.test.ts:167-175, packages/convex/convex/cms/localization.test.ts:205-215
Key Components and Functions
The implementation relies on several critical utilities distributed across the storefront and packages.
| Component / Function | Location | Description |
|---|---|---|
Locale (Class) | apps/storefront/src/utils/locale/ | Utility class wrapping xx-XX tags with helpers like .from() and .default. |
buildLocaleFallbackChain | packages/convex/convex/cms/localization.ts | Orders locales for content resolution (Request → Shop → Platform). |
reassembleLocalizedDocument | packages/convex/convex/cms/localization.ts | Flattens a CMS document for a specific locale by resolving all buckets. |
getDictionary | apps/storefront/src/utils/dictionary.ts | Loads UI translation strings from JSON files (e.g., en.json, sv.json). |
ShopifyApolloApiClient | apps/storefront/src/api/shopify/ | Injects @inContext(language, country) into GraphQL queries automatically. |
Sources: CONTEXT.md:337-343, apps/storefront/README.md:143-149, packages/convex/convex/cms/localization.test.ts:44-50, apps/storefront/src/app/[domain]/[locale]/countries/page.tsx:24-30
Multi-Tenant Integration
Localization is deeply integrated with the shop's commerce provider. Supported locales are not hardcoded but are derived from the commerce backend (e.g., Shopify) at request time. This prevents data drift between the platform and the merchant's external settings.
Localization Cookie Management
Users can manually change their locale via the /countries page. This updates the localization and NEXT_LOCALE cookies, which the middleware prioritizes in subsequent requests.
// Example from the Countries page action
const cookieStore = await cookies();
cookieStore.set('localization', code);
cookieStore.set('NEXT_LOCALE', code);
redirect(`/${code}/`, RedirectType.push);
Sources: apps/storefront/src/app/[domain]/[locale]/countries/page.tsx:96-102, CONTEXT.md:330-335
Localization Relationship Diagram
The following diagram shows the relationship between a Shop, its Locales, and how they interact with both the CMS and the Commerce Provider.
This diagram illustrates that the Shop record holds the default locale, while the actual supported list is dynamic from the provider, and CMS content is stored in buckets within documents.
Sources: CONTEXT.md:21-25, CONTEXT.md:330-335, packages/convex/convex/cms/localization.test.ts:167-171
Conclusion
The Locale Resolution and i18n system provides a robust framework for managing diverse language requirements across multiple tenants. By decoupling supported locales from the database and using a shredded content model in the CMS, the platform ensures high performance and scalability while maintaining strict tenant isolation and backend synchronization.