Implement Lokalise reference architecture with best-practice project layout.
Use when designing new Lokalise integrations, reviewing project structure,
or establishing architecture standards for Lokalise applications.
Trigger with phrases like "lokalise architecture", "lokalise best practices",
"lokalise project structure", "how to organize lokalise", "lokalise layout".
Implement Lokalise reference architecture with best-practice project layout.
Use when designing new Lokalise integrations, reviewing project structure,
or establishing architecture standards for Lokalise applications.
Trigger with phrases like "lokalise architecture", "lokalise best practices",
"lokalise project structure", "how to organize lokalise", "lokalise layout".
allowed-tools
Read, Grep
version
1.14.0
license
MIT
author
Jeremy Longshore <jeremy@intentsolutions.io>
tags
["saas","lokalise","lokalise-reference"]
compatibility
Designed for Claude Code, also compatible with Codex and OpenClaw
Lokalise Reference Architecture
Overview
A production-ready architecture for integrating Lokalise into web applications. Covers the end-to-end translation flow from source code through CI/CD and Lokalise to deployed translations, recommended project structure for i18n, file organization conventions, multi-app translation sharing, and the tradeoffs between OTA (over-the-air) and build-time translation loading.
Follow these conventions for translation file organization:
Flat keys (recommended for most projects — simpler grep, no nesting ambiguity):
{"homepage.hero.title":"Welcome to MyApp","homepage.hero.subtitle":"The best app ever","settings.profile.name_label":"Full Name","errors.not_found":"Page not found"}
Nested keys work better for large projects with clear module boundaries, where each top-level key maps to a feature area. Both formats are supported by Lokalise and i18next.
Key naming conventions:
Use dot notation: module.section.element
Use snake_case for key segments: user_profile, not userProfile
Prefix by feature area: checkout.payment.card_label
Use consistent suffixes: _title, _label, _button, _error, _placeholder
Keep keys under 100 characters (Lokalise hard limit is 1024 chars per key name)
File naming:
One file per locale: en.json, de.json, fr.json
For large apps, split by namespace: common.json, auth.json, dashboard.json
Namespace files go in subdirectories: locales/en/common.json, locales/en/auth.json
Step 6: Multi-App Translation Sharing
When multiple applications share translations (e.g., web app + mobile app + marketing site):
Alternative: Separate projects with key linking. Lokalise does not natively share keys across projects, so tag-based filtering within a single project is the recommended approach for shared translations.
Step 7: OTA vs Build-Time Translation Loading
Choose the right delivery strategy based on your requirements:
Factor
Build-Time
OTA
Latency
Zero (bundled)
Network request on first load
Update speed
Requires deployment
Instant (CDN cache)
Offline support
Full
Needs initial fetch + local cache
Bundle size
Increases with locales
Minimal (loaded on demand)
Reliability
No external dependency
Depends on Lokalise CDN
Best for
Server-rendered apps, SPAs with CI/CD
Mobile apps, rapid copy changes
Build-time implementation (recommended for most web apps):
// src/i18n/loader-buildtime.ts// Translations are imported statically — bundled at build timeimport en from'../locales/en.json';
import de from'../locales/de.json';
import fr from'../locales/fr.json';
consttranslations: Record<string, Record<string, unknown>> = { en, de, fr };
exportfunctionloadTranslation(locale: string): Record<string, unknown> {
return translations[locale] ?? translations['en'];
}
OTA implementation (for instant translation updates without redeployment):
// src/i18n/loader-ota.tsimport i18next from'i18next';
importLocizeBackendfrom'i18next-locize-backend'; // or i18next-http-backend// Lokalise OTA requires the @lokalise/i18next-ota-plugin or a custom backend// pointing at the Lokalise OTA endpoint.
i18next
.use(LocizeBackend)
.init({
backend: {
// Lokalise OTA SDK endpoint// See:loadPath: `https://ota.lokalise.com/v3/public/${process.env.LOKALISE_OTA_TOKEN}/{{lng}}/{{ns}}`,
},
fallbackLng: 'en',
ns: ['translation'],
defaultNS: 'translation',
});
Hybrid approach (recommended for production):
// src/i18n/loader-hybrid.tsimport bundledEn from'../locales/en.json';
/**
* Load bundled translations immediately, then attempt OTA update.
* User sees bundled content instantly; OTA updates appear on next render.
*/exportasyncfunctionloadWithOtaFallback(locale: string): Promise<Record<string, unknown>> {
// 1. Start with bundled translations (instant)const bundled = awaitimport(`../locales/${locale}.json`)
.then(m => m.default)
.catch(() => bundledEn);
// 2. Attempt OTA fetch in background (non-blocking)fetchOtaTranslations(locale)
.then(ota => {
if (ota) {
// Merge OTA translations over bundled (OTA wins on conflicts)Object.assign(i18next.store.data[locale].translation, ota);
i18next.emit('loaded');
}
})
.catch(() => { /* OTA failed, bundled translations are sufficient */ });
return bundled;
}
asyncfunctionfetchOtaTranslations(locale: string): Promise<Record<string, unknown> | null> {
const otaToken = process.env.LOKALISE_OTA_TOKEN;
if (!otaToken) returnnull;
const response = awaitfetch(`https://ota.lokalise.com/v3/public/${otaToken}/${locale}/translation`, {
signal: AbortSignal.timeout(5000),
});
if (!response.ok) returnnull;
return response.json();
}
Step 8: TypeScript Type Safety
Generate types from your source locale to get compile-time checks on translation keys:
Enterprise adds: client.ts (Step 4), loader-hybrid.ts (Step 7), fallback.ts, middleware.ts, types.ts (Step 8), locales-fallback/ directory, validate-translations.ts, generate-types.ts, and CI workflows. See the full tree in Step 2.