Nightwatch supports 14 languages using next-intl with cookie-based locale detection, 8 translation namespaces, RTL support for Arabic, and a server-side provider that pre-loads all messages before rendering.
| Code | Language | Direction |
|---|---|---|
en |
English | LTR |
hi |
Hindi | LTR |
es |
Spanish | LTR |
fr |
French | LTR |
ja |
Japanese | LTR |
ko |
Korean | LTR |
de |
German | LTR |
pt |
Portuguese | LTR |
ar |
Arabic | RTL |
ru |
Russian | LTR |
zh |
Chinese | LTR |
it |
Italian | LTR |
tr |
Turkish | LTR |
th |
Thai | LTR |
Default locale: en
Each locale has 8 JSON files, one per namespace:
| Namespace | File | Domain |
|---|---|---|
common |
common.json |
Shared UI strings (navigation, buttons, errors, friends, settings) |
auth |
auth.json |
Login, signup, password reset, verification |
profile |
profile.json |
User profile, preferences, security settings |
search |
search.json |
Search page, filters, content cards |
watch |
watch.json |
VOD player, watchlist, content details |
live |
live.json |
Livestream player, clips, chat |
party |
party.json |
Watch party, room creation, invitations |
music |
music.json |
Music player, playlists, lyrics |
src/i18n/
├── config.ts # Locale list, types, cookie name
├── request.ts # Server-side message loader (getRequestConfig)
└── messages/
├── en/ # English (source language)
│ ├── common.json
│ ├── auth.json
│ ├── profile.json
│ ├── search.json
│ ├── watch.json
│ ├── live.json
│ ├── party.json
│ └── music.json
├── hi/ # Hindi
│ └── ... (same 8 files)
├── ar/ # Arabic (RTL)
│ └── ...
└── ... (11 more locales)
export const locales = [
'en', 'hi', 'es', 'fr', 'ja', 'ko', 'de', 'pt', 'ar', 'ru', 'zh', 'it', 'tr', 'th',
] as const;
export type Locale = (typeof locales)[number];
export const defaultLocale: Locale = 'en';
export const COOKIE_NAME = 'NEXT_LOCALE';The active locale is stored in a cookie named NEXT_LOCALE. The server reads this cookie on every request:
// src/i18n/request.ts
const raw = cookieStore.get(COOKIE_NAME)?.value;
const locale = locales.includes(raw as Locale) ? raw : defaultLocale;If the cookie is missing or contains an invalid value, the app falls back to en.
src/i18n/request.ts uses getRequestConfig from next-intl/server with a static import map. Dynamic import() with template literals fails on Vercel Edge/serverless because the bundler can't resolve paths at build time.
const messageImports: Record<string, () => Promise[]> = {
en: () => [
import('./messages/en/common.json'),
import('./messages/en/auth.json'),
// ... all 8 namespaces
],
// ... all 14 locales
};All 8 namespace files are loaded in parallel via Promise.all() and merged into a single messages object:
const [common, auth, profile, search, watch, live, party, music] =
await Promise.all(loader());
return {
locale,
messages: {
common: common.default,
auth: auth.default,
// ...
},
};src/providers/intl-provider.tsx — the top-level server-side provider:
- Calls
getLocale()andgetMessages()fromnext-intl/server - Falls back to English if loading fails
- Wraps children with
IntlClientWrapperandHtmlLangSetter
export async function IntlProvider({ children }) {
const locale = await getLocale();
const messages = await getMessages();
return (
<IntlClientWrapper locale={locale} messages={messages}>
<HtmlLangSetter locale={locale} />
{children}
</IntlClientWrapper>
);
}src/providers/intl-client-wrapper.tsx — wraps NextIntlClientProvider with:
- Error handler: dev-only
console.warnfor missing translations - Fallback strategy: renders the leaf key name when a message is missing (e.g.,
common.title→title)
src/providers/html-lang-setter.tsx — headless component that synchronizes <html> attributes:
document.documentElement.lang = locale; // e.g. "ar"
document.documentElement.dir = RTL_LOCALES.includes(locale) ? 'rtl' : 'ltr';RTL locales: ['ar']
Arabic (ar) is the only RTL locale. When active:
HtmlLangSettersetsdir="rtl"on<html>- Tailwind's
rtl:variant classes activate automatically - Layout mirrors (sidebars, text alignment, icons) via CSS logical properties
'use client';
import { useTranslations } from 'next-intl';
function MyComponent() {
const t = useTranslations('common');
return <h1>{t('title')}</h1>;
}const t = useTranslations('common');
t('friends.online'); // common.json → { "friends": { "online": "Online" } }- Add the locale code to
src/i18n/config.ts:
export const locales = [
'en', 'hi', 'es', 'fr', 'ja', 'ko', 'de', 'pt', 'ar', 'ru', 'zh', 'it', 'tr', 'th',
'vi', // ← new locale
] as const;- Create translation files — copy the
en/directory:
cp -r src/i18n/messages/en src/i18n/messages/vi-
Translate all 8 JSON files in the new directory.
-
Add the static import entry in
src/i18n/request.ts:
vi: () => [
import('./messages/vi/common.json'),
import('./messages/vi/auth.json'),
import('./messages/vi/profile.json'),
import('./messages/vi/search.json'),
import('./messages/vi/watch.json'),
import('./messages/vi/live.json'),
import('./messages/vi/party.json'),
import('./messages/vi/music.json'),
],- If RTL, add the locale code to
RTL_LOCALESinsrc/providers/html-lang-setter.tsx:
const RTL_LOCALES = ['ar', 'he']; // example- Add a language option in the language selector UI (typically in profile/settings).
- Keys use camelCase:
pageTitle,noResults,searchPlaceholder - Nested objects group related strings:
friends.online,errors.sessionExpired - Pluralization uses ICU message format where needed
- Keep keys descriptive but concise