diff --git a/package.json b/package.json index 59a2521d0..0a7830a06 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@types/node": "^22.13.4", "@unocss/eslint-config": "^65.5.0", "@unocss/eslint-plugin": "^65.5.0", + "@vitest/coverage-v8": "3.0.5", "bumpp": "^10.0.3", "cross-env": "^7.0.3", "eslint": "^9.20.1", diff --git a/packages/stage-tamagotchi/renderer.vite.config.ts b/packages/stage-tamagotchi/renderer.vite.config.ts index 3739dc482..8eb86067c 100644 --- a/packages/stage-tamagotchi/renderer.vite.config.ts +++ b/packages/stage-tamagotchi/renderer.vite.config.ts @@ -32,7 +32,7 @@ export default defineConfig({ runtimeOnly: true, compositionOnly: true, fullInstall: true, - include: [resolve(import.meta.dirname, 'locales/**')], + include: [resolve(import.meta.dirname, 'src', 'renderer', 'locales/**')], }), DownloadLive2DSDK(), Download('https://dist.ayaka.moe/live2d-models/hiyori_free_zh.zip', 'hiyori_free_zh.zip', 'assets/live2d/models'), diff --git a/packages/stage-tamagotchi/src/main/index.ts b/packages/stage-tamagotchi/src/main/index.ts index 9c40aef86..9a50934ba 100644 --- a/packages/stage-tamagotchi/src/main/index.ts +++ b/packages/stage-tamagotchi/src/main/index.ts @@ -5,6 +5,9 @@ import { app, BrowserWindow, dialog, ipcMain, Menu, screen, shell } from 'electr import { animate } from 'popmotion' import icon from '../../build/icon.png?asset' +import { createI18n } from './locales' + +const { t, setLocale } = createI18n() let globalMouseTracker: ReturnType | null = null let mainWindow: BrowserWindow @@ -138,33 +141,39 @@ function createSettingsWindow(): void { } } -// This method will be called when Electron has finished -// initialization and is ready to create browser windows. -// Some APIs can only be used after this event occurs. -app.whenReady().then(() => { - // Menu - const menu = Menu.buildFromTemplate([ +function buildMenu(): Menu { + return Menu.buildFromTemplate([ { label: 'airi', role: 'appMenu', submenu: [ { role: 'about', + label: t('menu.about'), }, { role: 'toggleDevTools', + label: t('menu.toggleDevTools'), }, { - label: 'Settings', + label: t('menu.settings'), click: () => createSettingsWindow(), }, { - label: 'Quit', + label: t('menu.quit'), click: () => app.quit(), }, ], }, ]) +} + +// This method will be called when Electron has finished +// initialization and is ready to create browser windows. +// Some APIs can only be used after this event occurs. +app.whenReady().then(() => { + // Menu + const menu = buildMenu() Menu.setApplicationMenu(menu) // Set app user model id for windows @@ -177,14 +186,19 @@ app.whenReady().then(() => { optimizer.watchWindowShortcuts(window) }) - // IPC test - // TODO: i18n + ipcMain.on('locale-changed', (_, locale: string) => { + setLocale(locale) + + Menu.setApplicationMenu(buildMenu()) + }) + ipcMain.on('quit', () => { dialog.showMessageBox({ type: 'info', - title: 'Quit', - message: 'Are you sure you want to quit?', - buttons: ['Quit', 'Cancel'], + title: t('quitDialog.title'), + message: t('quitDialog.message'), + buttons: [t('quitDialog.buttons.0'), t('quitDialog.buttons.1')], + defaultId: 0, }).then((result) => { if (result.response === 0) { app.quit() diff --git a/packages/stage-tamagotchi/src/main/locales/en-US.ts b/packages/stage-tamagotchi/src/main/locales/en-US.ts new file mode 100644 index 000000000..e5a0a972c --- /dev/null +++ b/packages/stage-tamagotchi/src/main/locales/en-US.ts @@ -0,0 +1,13 @@ +export default { + menu: { + settings: 'Settings', + quit: 'Quit', + about: 'About', + toggleDevTools: 'Toggle Developer Tools', + }, + quitDialog: { + title: 'Quit', + message: 'Are you sure you want to quit?', + buttons: ['Quit', 'Cancel'], + }, +} diff --git a/packages/stage-tamagotchi/src/main/locales/index.test.ts b/packages/stage-tamagotchi/src/main/locales/index.test.ts new file mode 100644 index 000000000..21b8304ca --- /dev/null +++ b/packages/stage-tamagotchi/src/main/locales/index.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'vitest' + +import { createI18n } from '.' + +describe('createI18n', () => { + it('should return the correct locale', () => { + const { t } = createI18n() + expect(t('menu.settings')).toBe('Settings') + }) + + it('should return key if the key is not found', () => { + const { t } = createI18n() + expect(t('menu.not.found')).toBe('menu.not.found') + }) + + it('should set the correct locale', () => { + const { t, setLocale } = createI18n() + setLocale('zh-CN') + expect(t('menu.settings')).toBe('设置') + }) + + it('should return the correct locale in array', () => { + const { t } = createI18n() + expect(t('quitDialog.buttons.0')).toBe('Quit') + expect(t('quitDialog.buttons.1')).toBe('Cancel') + }) +}) diff --git a/packages/stage-tamagotchi/src/main/locales/index.ts b/packages/stage-tamagotchi/src/main/locales/index.ts new file mode 100644 index 000000000..2a4c0c51f --- /dev/null +++ b/packages/stage-tamagotchi/src/main/locales/index.ts @@ -0,0 +1,46 @@ +import enUS from './en-US' +import zhCN from './zh-CN' + +// TODO: compact locales, such as 'en' can be 'en-US' +const locales = { + 'en-US': enUS, + 'zh-CN': zhCN, +} + +export function createI18n() { + let locale = 'en-US' + let messages = locales['en-US'] + + function t(key: string) { + const path = key.split('.') + let current = messages + let result = '' + + while (path.length > 0) { + const k = path.shift() + if (k && current && k in current) { + current = current[k] + } + else { + return key + } + } + + if (typeof current === 'string') { + result = current + } + + return result + } + + function setLocale(l: string) { + locale = l + messages = locales[l] + } + + return { + t, + setLocale, + locale, + } +} diff --git a/packages/stage-tamagotchi/src/main/locales/zh-CN.ts b/packages/stage-tamagotchi/src/main/locales/zh-CN.ts new file mode 100644 index 000000000..79e9cb2d7 --- /dev/null +++ b/packages/stage-tamagotchi/src/main/locales/zh-CN.ts @@ -0,0 +1,13 @@ +export default { + menu: { + settings: '设置', + quit: '退出', + about: '关于', + toggleDevTools: '切换开发者工具', + }, + quitDialog: { + title: '退出', + message: '确定要退出吗?', + buttons: ['退出', '取消'], + }, +} diff --git a/packages/stage-tamagotchi/locales/en.yml b/packages/stage-tamagotchi/src/renderer/locales/en.yml similarity index 99% rename from packages/stage-tamagotchi/locales/en.yml rename to packages/stage-tamagotchi/src/renderer/locales/en.yml index 5c9406ca5..78c62999d 100644 --- a/packages/stage-tamagotchi/locales/en.yml +++ b/packages/stage-tamagotchi/src/renderer/locales/en.yml @@ -63,6 +63,8 @@ settings: placeholder_mobile: OpenAI API BaseURL title: Settings voices: Voice + quit: Quit + viewer: Viewer stage: chat: message: diff --git a/packages/stage-tamagotchi/locales/zh-CN.yml b/packages/stage-tamagotchi/src/renderer/locales/zh-CN.yml similarity index 98% rename from packages/stage-tamagotchi/locales/zh-CN.yml rename to packages/stage-tamagotchi/src/renderer/locales/zh-CN.yml index e1e42858a..db251417b 100644 --- a/packages/stage-tamagotchi/locales/zh-CN.yml +++ b/packages/stage-tamagotchi/src/renderer/locales/zh-CN.yml @@ -50,6 +50,8 @@ settings: placeholder_mobile: OpenAI BaseURL title: 设置 voices: 声线 + quit: 退出 + viewer: 查看器 stage: message: 消息 select-a-audio-input: 选择一个音频输入设备 diff --git a/packages/stage-tamagotchi/src/renderer/src/App.vue b/packages/stage-tamagotchi/src/renderer/src/App.vue index a78fb6fd3..339f078a5 100644 --- a/packages/stage-tamagotchi/src/renderer/src/App.vue +++ b/packages/stage-tamagotchi/src/renderer/src/App.vue @@ -1,5 +1,16 @@