Skip to content

Internationalization

N.E.K.O. supports 5 languages with client-side locale switching.

Supported locales

CodeLanguageFile
enEnglishstatic/locales/en.json
zh-CNSimplified Chinesestatic/locales/zh-CN.json
zh-TWTraditional Chinesestatic/locales/zh-TW.json
jaJapanesestatic/locales/ja.json
koKoreanstatic/locales/ko.json

How it works

  1. 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)
  2. Loads the corresponding locale JSON file
  3. 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:

ModuleHTML filesJS files
mainindex.htmlapp.js
live2dlive2d*.htmllive2d*.js
voicevoice*.htmlvoice*.js, tts*.js
steamsteam*.htmlsteam*.js
settingssettings*.html, config*.htmlsettings*.js, config*.js
chatmemory_browser.html, chara_manager.htmlmemory_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

  1. Create a new file in static/locales/ (e.g., fr.json)
  2. Copy the structure from en.json and translate all values
  3. Add the locale option to the language selector in the frontend
  4. Update utils/language_utils.py to recognize the new locale code

Backend translation

The backend also supports translation via TranslationService (utils/translation_service.py):

  • Primary: googletrans library
  • Fallback: translatepy library
  • 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.

Released under the MIT License.