Internationalization
N.E.K.O. supports 5 languages with client-side locale switching.
Supported locales
| Code | Language | File |
|---|---|---|
en | English | static/locales/en.json |
zh-CN | Simplified Chinese | static/locales/zh-CN.json |
zh-TW | Traditional Chinese | static/locales/zh-TW.json |
ja | Japanese | static/locales/ja.json |
ko | Korean | static/locales/ko.json |
How it works
- On page load, the system detects the user's language:
- First checks Steam client language (if available via
/api/config/steam_language) - Falls back to browser language preference
- Respects user override (
/api/config/user_language)
- First checks Steam client language (if available via
- Loads the corresponding locale JSON file
- Replaces text content in the DOM using data attributes or direct JS replacement
Locale file format
Locale files are flat JSON objects with dot-notation keys:
json
{
"nav.home": "Home",
"nav.settings": "Settings",
"chat.placeholder": "Type a message...",
"chat.send": "Send"
}i18n markup patterns
HTML: data-i18n attributes
html
<!-- Element text -->
<span data-i18n="chat.send">发送</span>
<button data-i18n="common.ok">确定</button>
<!-- Placeholder -->
<input placeholder="请输入" data-i18n-placeholder="chat.inputPlaceholder">
<!-- Title attribute -->
<button title="关闭" data-i18n-title="common.close">X</button>
<!-- Alt attribute -->
<img alt="对话" src="chat.png" data-i18n-alt="chat.title">JavaScript: window.t() with fallback
Always provide a Chinese fallback for graceful degradation:
javascript
// Display text
showStatusToast(window.t ? window.t('common.connectionSuccess') : '连接成功', 2000);
// Error messages
showMessage(window.t ? window.t('common.saveFailed') : '保存失败', 'error');
// With parameters
showMessage(window.t ? window.t('files.deleted', { count }) : `删除了 ${count} 个文件`);What NOT to i18n
javascript
// ✅ Skip: console debug messages
console.log('连接成功');
// ✅ Skip: internal logic detection
if (status.includes('已离开')) { ... }
// ✅ Skip: data keys (character field names)
const name = characterData['档案名'];
// ✅ Skip: already wrapped
showMessage(window.t ? window.t('key') : 'fallback');Module-to-file mapping
When checking for untranslated strings, use these module groupings:
| Module | HTML files | JS files |
|---|---|---|
main | index.html | app.js |
live2d | live2d*.html | live2d*.js |
voice | voice*.html | voice*.js, tts*.js |
steam | steam*.html | steam*.js |
settings | settings*.html, config*.html | settings*.js, config*.js |
chat | memory_browser.html, chara_manager.html | memory_browser.js, chara_manager.js |
HTML + icon conflict
When i18next updates text via textContent, it destroys <img> tags inside the element. If your translation contains HTML icons, include the full HTML in the locale JSON so the system uses innerHTML instead. See Developer Notes.
Adding a new language
- Create a new file in
static/locales/(e.g.,fr.json) - Copy the structure from
en.jsonand translate all values - Add the locale option to the language selector in the frontend
- Update
utils/language_utils.pyto recognize the new locale code
Backend translation
The backend also supports translation via TranslationService (utils/translation_service.py):
- Primary:
googletranslibrary - Fallback:
translatepylibrary - Final fallback: LLM-based translation
This is used for translate_if_needed() in the session manager when the user's language differs from the character's language.
