File size: 6,525 Bytes
8ede856
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
import { ref, computed } from 'vue';
import { translations as staticTranslations } from './translations';
import type { Locale } from './types';

// 全局状态
const currentLocale = ref<Locale>('zh-CN');
const translations = ref<Record<string, any>>({});

/**
 * 初始化i18n系统
 */
export async function initI18n(locale: Locale = 'zh-CN') {
  currentLocale.value = locale;

  // 加载静态翻译数据
  loadTranslations(locale);
}

/**
 * 加载翻译数据(现在从静态导入获取)
 */
function loadTranslations(locale: Locale) {
  try {
    const data = staticTranslations[locale];
    if (data) {
      translations.value = data;
    } else {
      console.warn(`Translations not found for locale: ${locale}`);
      // 回退到中文
      if (locale !== 'zh-CN') {
        console.log('Falling back to zh-CN');
        translations.value = staticTranslations['zh-CN'];
      }
    }
  } catch (error) {
    console.error(`Failed to load translations for ${locale}:`, error);
    // 回退到中文
    if (locale !== 'zh-CN') {
      console.log('Falling back to zh-CN');
      translations.value = staticTranslations['zh-CN'];
    }
  }
}

/**
 * 主要的翻译函数组合
 */
export function useI18n() {
  // 翻译函数
  const t = (key: string, params?: Record<string, string | number>): string => {
    const keys = key.split('.');
    let value: any = translations.value;

    // 遍历键路径
    for (const k of keys) {
      if (value && typeof value === 'object' && k in value) {
        value = value[k];
      } else {
        console.warn(`Translation key not found: ${key}`);
        // 返回带括号的键名,便于在开发时识别缺失的翻译
        return `[MISSING: ${key}]`;
      }
    }

    if (typeof value !== 'string') {
      console.warn(`Translation value is not string: ${key}`, value);
      // 返回带括号的键名,便于在开发时识别类型错误的翻译
      return `[INVALID: ${key}]`;
    }

    // 此时value确定是string类型
    let result: string = value;

    // 处理参数插值
    if (params) {
      result = result.replace(/\{(\w+)\}/g, (match: string, paramKey: string) => {
        return params[paramKey]?.toString() || match;
      });
    }

    return result;
  };

  // 切换语言
  const setLocale = async (newLocale: Locale) => {
    if (newLocale !== currentLocale.value) {
      currentLocale.value = newLocale;
      loadTranslations(newLocale);

      // 保存到localStorage
      localStorage.setItem('astrbot-locale', newLocale);

      // 触发自定义事件,通知相关页面重新加载配置数据
      // 这是因为插件适配器的 i18n 数据是通过后端 API 注入的,
      // 需要根据 Accept-Language 头重新获取
      window.dispatchEvent(new CustomEvent('astrbot-locale-changed', {
        detail: { locale: newLocale }
      }));
    }
  };

  // 获取当前语言
  const locale = computed(() => currentLocale.value);

  // 获取可用语言列表
  const availableLocales: Locale[] = ['zh-CN', 'en-US', 'ru-RU'];

  // 检查是否已加载
  const isLoaded = computed(() => Object.keys(translations.value).length > 0);

  return {
    t,
    locale,
    setLocale,
    availableLocales,
    isLoaded
  };
}

/**
 * 模块特定的翻译函数
 */
export function useModuleI18n(moduleName: string) {
  const { t } = useI18n();

  const tm = (key: string, params?: Record<string, string | number>): string => {
    // 将斜杠转换为点号以匹配嵌套对象结构
    const normalizedModuleName = moduleName.replace(/\//g, '.');
    return t(`${normalizedModuleName}.${key}`, params);
  };

  // 获取原始翻译值(可能是字符串、数组或对象)
  const getRaw = (key: string): any => {
    const normalizedModuleName = moduleName.replace(/\//g, '.');
    const fullKey = `${normalizedModuleName}.${key}`;
    const keys = fullKey.split('.');
    let value: any = translations.value;

    for (const k of keys) {
      if (value && typeof value === 'object' && k in value) {
        value = value[k];
      } else {
        return null;
      }
    }

    return value;
  };

  return { tm, getRaw };
}

/**
 * 语言切换器组合函数
 */
export function useLanguageSwitcher() {
  const { locale, setLocale, availableLocales } = useI18n();

  const languageOptions = computed(() => [
    { value: 'zh-CN', label: '简体中文', flag: '🇨🇳' },
    { value: 'en-US', label: 'English', flag: '🇺🇸' },
    { value: 'ru-RU', label: 'Русский', flag: '🇷🇺' }
  ]);

  const currentLanguage = computed(() => {
    return languageOptions.value.find(lang => lang.value === locale.value);
  });

  const switchLanguage = async (newLocale: Locale) => {
    await setLocale(newLocale);
  };

  return {
    locale,
    languageOptions,
    currentLanguage,
    switchLanguage,
    availableLocales
  };
}

/**
 * 将动态翻译数据(如插件提供的 i18n)合并到当前翻译中。
 * @param modulePath 模块路径,如 'features.config-metadata'
 * @param allLocaleData 所有语言的翻译数据,如 { "zh-CN": {...}, "en-US": {...} }
 */
export function mergeDynamicTranslations(modulePath: string, allLocaleData: Record<string, any>) {
  const locale = currentLocale.value;
  const localeData = allLocaleData[locale];
  if (!localeData || typeof localeData !== 'object') return;

  const pathParts = modulePath.split('.');
  let target: any = translations.value;
  for (const part of pathParts) {
    if (!(part in target) || typeof target[part] !== 'object') {
      target[part] = {};
    }
    target = target[part];
  }

  deepMerge(target, localeData);

  // 触发响应式更新
  translations.value = { ...translations.value };
}

function deepMerge(target: Record<string, any>, source: Record<string, any>) {
  for (const key of Object.keys(source)) {
    if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
      if (!(key in target) || typeof target[key] !== 'object') {
        target[key] = {};
      }
      deepMerge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
}

// 初始化函数(在应用启动时调用)
export async function setupI18n() {
  // 从localStorage获取保存的语言设置
  const savedLocale = localStorage.getItem('astrbot-locale') as Locale;
  const initialLocale = savedLocale && ['zh-CN', 'en-US', 'ru-RU'].includes(savedLocale)
    ? savedLocale
    : 'zh-CN';

  await initI18n(initialLocale);
}