Internationalization and localization for the boilerplate monorepo. Covers backend (i18next), frontend (next-intl), and mobile (react-i18next) with shared types.
Complete i18n toolkit for the monorepo boilerplate - backend, frontend, and mobile.
Default: Türkçe (tr) | Supported: tr, en
| Layer | Library | Locale Detection | Storage |
|---|---|---|---|
| Backend | i18next + i18next-fs-backend | Accept-Language header | In-memory (JSON files) |
| Frontend | next-intl | URL segment (/tr/..., /en/...) | Cookie + URL |
| Mobile | react-i18next | Device locale + AsyncStorage | AsyncStorage |
All i18n types are centralized in @boilerplate/types:
import {
SupportedLocale, // 'tr' | 'en'
SUPPORTED_LOCALES, // ['tr', 'en'] as const
DEFAULT_LOCALE, // 'tr'
FALLBACK_LOCALE, // 'tr'
LOCALE_NAMES, // { tr: 'Turkish', en: 'English' }
LOCALE_NATIVE_NAMES, // { tr: 'Türkçe', en: 'English' }
ErrorCode, // API error codes
LocalizedApiError, // Localized API error response type
isLocaleSupported, // Type guard function
} from '@boilerplate/types';
packages/
├── shared/
│ ├── types/src/i18n.ts # Shared type definitions
│ ├── backend/src/i18n/
│ │ ├── index.ts # Exports
│ │ ├── i18n.service.ts # Singleton service
│ │ ├── i18n.middleware.ts # Express middleware
│ │ └── locales/
│ │ ├── tr/
│ │ │ ├── common.json
│ │ │ ├── errors.json
│ │ │ └── validation.json
│ │ └── en/
│ │ ├── common.json
│ │ ├── errors.json
│ │ └── validation.json
│ └── mobile/src/lib/
│ ├── i18n.ts # Mobile i18n config
│ └── locales/
│ ├── tr.json
│ └── en.json
├── frontend/nextjs-template/
│ ├── src/i18n/
│ │ ├── config.ts # Locale config
│ │ ├── request.ts # Server request config
│ │ └── navigation.ts # Locale-aware navigation
│ ├── src/messages/
│ │ ├── tr.json
│ │ └── en.json
│ ├── src/middleware.ts # Locale routing
│ └── src/app/[locale]/ # Locale-aware routes
└── mobile/expo-template/
└── src/components/LanguageSwitcher.tsx # Mobile language picker
// app.ts
import { i18nService, createI18nMiddleware } from '@boilerplate/shared-backend';
export async function createApp() {
const app = express();
// Initialize i18n
await i18nService.init({
debug: process.env.NODE_ENV === 'development',
localesPath: path.join(__dirname, '../src/i18n/locales'),
});
// Add middleware
app.use(cookieParser());
app.use(createI18nMiddleware());
return app;
}
// Request object has t() function attached by middleware
app.get('/api/greeting', (req, res) => {
// req.locale - detected locale ('tr' | 'en')
// req.localeSource - how locale was detected ('header' | 'query' | 'cookie' | 'default')
// req.t() - translation function
res.json({
message: req.t('common.welcome'),
greeting: req.t('common.hello', { name: 'Atakan' }),
});
});
import { ApiError } from '@boilerplate/shared-backend';
// Throw error with error code for localized message
throw ApiError.notFoundWithCode('USER_NOT_FOUND', { resource: 'User' });
// Error middleware returns localized response:
// {
// "success": false,
// "error": {
// "message": "Kullanıcı bulunamadı", // Localized message
// "messageKey": "USER_NOT_FOUND", // Key for client override
// "statusCode": 404,
// "status": "Not Found"
// }
// }
import { useTranslations } from 'next-intl';
export default function HomePage() {
const t = useTranslations('common');
return (
<div>
<h1>{t('welcome')}</h1>
<p>{t('greeting', { name: 'Atakan' })}</p>
</div>
);
}
'use client';
import { useTranslations, useLocale } from 'next-intl';
export function ClientComponent() {
const t = useTranslations('auth');
const locale = useLocale();
return (
<button>{t('login')}</button>
);
}
// Use locale-aware Link from navigation.ts
import { Link, useRouter, usePathname, redirect } from '@/i18n/navigation';
export function Navigation() {
const router = useRouter();
const pathname = usePathname();
return (
<nav>
<Link href="/dashboard">{t('dashboard')}</Link>
<Link href="/settings">{t('settings')}</Link>
</nav>
);
}
'use client';
import { useLocale } from 'next-intl';
import { usePathname, useRouter } from '@/i18n/navigation';
import { SUPPORTED_LOCALES, LOCALE_NATIVE_NAMES, type SupportedLocale } from '@boilerplate/types';
export function LocaleSwitcher() {
const locale = useLocale();
const pathname = usePathname();
const router = useRouter();
const switchLocale = (newLocale: SupportedLocale) => {
router.replace(pathname, { locale: newLocale });
};
return (
<select value={locale} onChange={(e) => switchLocale(e.target.value as SupportedLocale)}>
{SUPPORTED_LOCALES.map((loc) => (
<option key={loc} value={loc}>{LOCALE_NATIVE_NAMES[loc]}</option>
))}
</select>
);
}
// app/_layout.tsx
import { initI18n } from '@boilerplate/shared-mobile';
export default function RootLayout() {
const [isI18nReady, setIsI18nReady] = useState(false);
useEffect(() => {
initI18n()
.then(() => setIsI18nReady(true))
.catch((error) => {
console.error('i18n init failed:', error);
setIsI18nReady(true); // Continue with defaults
});
}, []);
if (!isI18nReady) {
return <LoadingScreen />;
}
return <Stack />;
}
import { useTranslation } from 'react-i18next';
export function MyScreen() {
const { t } = useTranslation();
return (
<View>
<Text>{t('common.welcome')}</Text>
<Text>{t('auth.login')}</Text>
</View>
);
}
import { changeLanguage, getCurrentLanguage } from '@boilerplate/shared-mobile';
import { type SupportedLocale } from '@boilerplate/types';
// Get current language
const current = getCurrentLanguage(); // 'tr' | 'en'
// Change language (persists to AsyncStorage)
await changeLanguage('en');
// packages/shared/types/src/i18n.ts
export type SupportedLocale = 'tr' | 'en' | 'de'; // Add new locale
export const SUPPORTED_LOCALES: readonly SupportedLocale[] = ['tr', 'en', 'de'] as const;
export const LOCALE_NAMES: Record<SupportedLocale, string> = {
tr: 'Turkish',
en: 'English',
de: 'German', // Add name
};
export const LOCALE_NATIVE_NAMES: Record<SupportedLocale, string> = {
tr: 'Türkçe',
en: 'English',
de: 'Deutsch', // Add native name
};
Create packages/shared/backend/src/i18n/locales/de/ directory with:
common.jsonerrors.jsonvalidation.jsonCreate packages/frontend/nextjs-template/src/messages/de.json
Create packages/shared/mobile/src/lib/locales/de.json
Then update packages/shared/mobile/src/lib/i18n.ts:
import de from './locales/de.json';
const resources = {
tr: { translation: tr },
en: { translation: en },
de: { translation: de }, // Add new locale
};
// packages/frontend/nextjs-template/src/i18n/config.ts
export const locales = ['tr', 'en', 'de'] as const;
pnpm --filter @boilerplate/types build
Minimal boilerplate keys - projects add their own:
{
"common": {
"loading": "Yükleniyor...",
"error": "Bir hata oluştu",
"save": "Kaydet",
"cancel": "İptal",
"delete": "Sil",
"edit": "Düzenle",
"search": "Ara",
"noResults": "Sonuç bulunamadı",
"welcome": "Hoş geldiniz",
"getStarted": "Başla"
},
"auth": {
"login": "Giriş Yap",
"logout": "Çıkış Yap",
"register": "Kayıt Ol",
"email": "E-posta",
"password": "Şifre",
"createAccount": "Hesap Oluştur"
},
"errors": {
"unauthorized": "Yetkilendirme gerekli",
"forbidden": "Bu işlemi yapmaya yetkiniz yok",
"notFound": "{{resource}} bulunamadı",
"serverError": "Sunucu hatası oluştu"
},
"validation": {
"required": "Bu alan zorunludur",
"invalidEmail": "Geçerli bir e-posta adresi girin",
"minLength": "En az {{min}} karakter olmalıdır"
}
}
auth.login.button@boilerplate/typesSupportedLocale type, not stringisLocaleSupported() for runtime checksmessageKey for client-side override?lang=en for testing# Python script to find missing keys
python scripts/check-translations.py packages/frontend/nextjs-template/src/messages
# Sync missing keys from base locale (tr) to others
python scripts/sync-translations.py packages/frontend/nextjs-template/src/messages tr
@boilerplate/types