config.js 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. /**
  2. * i18n Configuration
  3. *
  4. * Configures i18next for internationalization support.
  5. * Features:
  6. * - Language detection from localStorage
  7. * - Fallback to English for missing translations
  8. * - Development mode warnings for missing keys
  9. *
  10. * Supported locales: en, zh-CN. Other locales were retired during the
  11. * V1 cleanup; add new locale bundles in `./locales/<lang>/` and register
  12. * them in `resources` + `./languages.js` to bring them back.
  13. */
  14. import i18n from 'i18next';
  15. import { initReactI18next } from 'react-i18next';
  16. // eslint-disable-next-line import-x/order
  17. import LanguageDetector from 'i18next-browser-languagedetector';
  18. import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
  19. import { authenticatedFetch } from '../utils/api';
  20. import enCommon from './locales/en/common.json';
  21. import enSettings from './locales/en/settings.json';
  22. import enAuth from './locales/en/auth.json';
  23. import enSidebar from './locales/en/sidebar.json';
  24. import enChat from './locales/en/chat.json';
  25. import enCodeEditor from './locales/en/codeEditor.json';
  26. import enAlwaysOn from './locales/en/alwaysOn.json';
  27. import enRouting from './locales/en/routing.json';
  28. // eslint-disable-next-line import-x/order
  29. import enTasks from './locales/en/tasks.json';
  30. import zhCommon from './locales/zh-CN/common.json';
  31. import zhSettings from './locales/zh-CN/settings.json';
  32. import zhAuth from './locales/zh-CN/auth.json';
  33. import zhSidebar from './locales/zh-CN/sidebar.json';
  34. import zhChat from './locales/zh-CN/chat.json';
  35. import zhAlwaysOn from './locales/zh-CN/alwaysOn.json';
  36. import zhRouting from './locales/zh-CN/routing.json';
  37. // eslint-disable-next-line import-x/order
  38. import zhCodeEditor from './locales/zh-CN/codeEditor.json';
  39. import { languages } from './languages.js';
  40. const getSavedLanguage = () => {
  41. try {
  42. const saved = localStorage.getItem('userLanguage');
  43. if (saved && languages.some(lang => lang.value === saved)) {
  44. return saved;
  45. }
  46. return 'en';
  47. } catch {
  48. return 'en';
  49. }
  50. };
  51. i18n
  52. .use(LanguageDetector)
  53. .use(initReactI18next)
  54. .init({
  55. resources: {
  56. en: {
  57. common: enCommon,
  58. settings: enSettings,
  59. auth: enAuth,
  60. sidebar: enSidebar,
  61. chat: enChat,
  62. codeEditor: enCodeEditor,
  63. tasks: enTasks,
  64. alwaysOn: enAlwaysOn,
  65. routing: enRouting,
  66. },
  67. 'zh-CN': {
  68. common: zhCommon,
  69. settings: zhSettings,
  70. auth: zhAuth,
  71. sidebar: zhSidebar,
  72. chat: zhChat,
  73. codeEditor: zhCodeEditor,
  74. alwaysOn: zhAlwaysOn,
  75. routing: zhRouting,
  76. },
  77. },
  78. lng: getSavedLanguage(),
  79. fallbackLng: 'en',
  80. debug: import.meta.env.DEV,
  81. ns: ['common', 'settings', 'auth', 'sidebar', 'chat', 'codeEditor', 'tasks', 'alwaysOn', 'routing'],
  82. defaultNS: 'common',
  83. keySeparator: '.',
  84. nsSeparator: ':',
  85. saveMissing: false,
  86. interpolation: {
  87. escapeValue: false,
  88. },
  89. react: {
  90. useSuspense: true,
  91. bindI18n: 'languageChanged',
  92. bindI18nStore: false,
  93. },
  94. detection: {
  95. order: ['localStorage'],
  96. lookupLocalStorage: 'userLanguage',
  97. caches: ['localStorage'],
  98. },
  99. });
  100. i18n.on('languageChanged', (lng) => {
  101. try {
  102. localStorage.setItem('userLanguage', lng);
  103. } catch (error) {
  104. console.error('Failed to save language preference:', error);
  105. }
  106. syncAlwaysOnLanguage(lng);
  107. });
  108. function syncAlwaysOnLanguage(lng) {
  109. const alwaysOnLang = lng === 'zh-CN' ? 'zh-CN' : 'en';
  110. authenticatedFetch('/api/config')
  111. .then((r) => r.json())
  112. .then((data) => {
  113. const raw = typeof data?.raw === 'string' ? data.raw : '';
  114. if (!raw) return;
  115. let parsed;
  116. try { parsed = parseYaml(raw); } catch { return; }
  117. if (!parsed || typeof parsed !== 'object') return;
  118. if (!parsed.alwaysOn || typeof parsed.alwaysOn !== 'object') return;
  119. if (parsed.alwaysOn.language === alwaysOnLang) return;
  120. parsed.alwaysOn.language = alwaysOnLang;
  121. const updated = stringifyYaml(parsed, { lineWidth: 0 });
  122. return authenticatedFetch('/api/config', {
  123. method: 'PUT',
  124. body: JSON.stringify({ raw: updated }),
  125. });
  126. })
  127. .catch(() => {});
  128. }
  129. export default i18n;