diff --git a/apps/stage-tamagotchi/electron.vite.config.ts b/apps/stage-tamagotchi/electron.vite.config.ts index e8af3c675..9275fe2f2 100644 --- a/apps/stage-tamagotchi/electron.vite.config.ts +++ b/apps/stage-tamagotchi/electron.vite.config.ts @@ -46,6 +46,12 @@ export default defineConfig({ }, }, ], + + resolve: { + alias: { + '@proj-airi/i18n': resolve(join(import.meta.dirname, '..', '..', 'packages', 'i18n', 'src')), + }, + }, }, preload: { build: { diff --git a/apps/stage-tamagotchi/package.json b/apps/stage-tamagotchi/package.json index e25fd3ee3..a2fdfffa1 100644 --- a/apps/stage-tamagotchi/package.json +++ b/apps/stage-tamagotchi/package.json @@ -48,6 +48,7 @@ "@formkit/auto-animate": "^0.9.0", "@guiiai/logg": "catalog:", "@huggingface/transformers": "^3.8.1", + "@intlify/core": "catalog:", "@modelcontextprotocol/sdk": "catalog:", "@moeru/eventa": "^1.0.0-beta.1", "@moeru/std": "catalog:", @@ -84,6 +85,7 @@ "@xsai/stream-transcription": "0.4.0-beta.8", "@xsai/tool": "catalog:", "@xsai/utils-chat": "catalog:", + "alien-signals": "catalog:", "animejs": "^4.3.6", "colorjs.io": "^0.6.1", "crossws": "^0.4.4", diff --git a/apps/stage-tamagotchi/src/main/configs/global.ts b/apps/stage-tamagotchi/src/main/configs/global.ts new file mode 100644 index 000000000..5ba7f5613 --- /dev/null +++ b/apps/stage-tamagotchi/src/main/configs/global.ts @@ -0,0 +1,14 @@ +import { object, optional, string } from 'valibot' + +import { createConfig } from '../libs/electron/persistence' + +export const globalAppConfigSchema = object({ + language: optional(string(), 'en'), +}) + +export function createGlobalAppConfig() { + const config = createConfig('app', 'options.json', globalAppConfigSchema) + config.setup() + + return config +} diff --git a/apps/stage-tamagotchi/src/main/index.ts b/apps/stage-tamagotchi/src/main/index.ts index 46a2e69e6..94fa4830c 100644 --- a/apps/stage-tamagotchi/src/main/index.ts +++ b/apps/stage-tamagotchi/src/main/index.ts @@ -2,6 +2,8 @@ import { dirname } from 'node:path' import { env, platform } from 'node:process' import { fileURLToPath } from 'node:url' +import messages from '@proj-airi/i18n/locales' + import { electronApp, optimizer } from '@electron-toolkit/utils' import { Format, LogLevel, setGlobalFormat, setGlobalLogLevel, useLogg } from '@guiiai/logg' import { initScreenCaptureForMain } from '@proj-airi/electron-screen-capture/main' @@ -13,8 +15,10 @@ import { isLinux } from 'std-env' import icon from '../../resources/icon.png?asset' import { openDebugger, setupDebugger } from './app/debugger' +import { createGlobalAppConfig } from './configs/global' import { emitAppBeforeQuit, emitAppReady, emitAppWindowAllClosed } from './libs/bootkit/lifecycle' import { setElectronMainDirname } from './libs/electron/location' +import { createI18n } from './libs/i18n' import { setupServerChannel } from './services/airi/channel-server' import { setupMcpStdioManager } from './services/airi/mcp-servers' import { setupPluginHost } from './services/airi/plugins' @@ -76,9 +80,15 @@ initScreenCaptureForMain() app.whenReady().then(async () => { injeca.setLogger(createLoggLogger(useLogg('injeca').useGlobalConfig())) + const appConfig = injeca.provide('configs:app', () => createGlobalAppConfig()) const electronApp = injeca.provide('host:electron:app', () => app) const autoUpdater = injeca.provide('services:auto-updater', () => setupAutoUpdater()) + const i18n = injeca.provide('libs:i18n', { + dependsOn: { appConfig }, + build: ({ dependsOn }) => createI18n({ messages, locale: dependsOn.appConfig.get()?.language }), + }) + const serverChannel = injeca.provide('modules:channel-server', { dependsOn: { app: electronApp }, build: async () => setupServerChannel(), @@ -97,40 +107,43 @@ app.whenReady().then(async () => { const beatSync = injeca.provide('windows:beat-sync', () => setupBeatSync()) const devtoolsMarkdownStressWindow = injeca.provide('windows:devtools:markdown-stress', () => setupDevtoolsWindow()) - const noticeWindow = injeca.provide('windows:notice', () => setupNoticeWindowManager()) + const noticeWindow = injeca.provide('windows:notice', { + dependsOn: { i18n, serverChannel }, + build: ({ dependsOn }) => setupNoticeWindowManager(dependsOn), + }) const widgetsManager = injeca.provide('windows:widgets', { - dependsOn: { serverChannel }, - build: () => setupWidgetsWindowManager(), + dependsOn: { serverChannel, i18n }, + build: ({ dependsOn }) => setupWidgetsWindowManager(dependsOn), }) const aboutWindow = injeca.provide('windows:about', { - dependsOn: { autoUpdater }, + dependsOn: { autoUpdater, i18n, serverChannel }, build: ({ dependsOn }) => setupAboutWindowReusable(dependsOn), }) const chatWindow = injeca.provide('windows:chat', { - dependsOn: { widgetsManager, serverChannel, mcpStdioManager }, + dependsOn: { widgetsManager, serverChannel, mcpStdioManager, i18n }, build: ({ dependsOn }) => setupChatWindowReusableFunc(dependsOn), }) const settingsWindow = injeca.provide('windows:settings', { - dependsOn: { widgetsManager, beatSync, autoUpdater, devtoolsMarkdownStressWindow, serverChannel, mcpStdioManager }, + dependsOn: { widgetsManager, beatSync, autoUpdater, devtoolsMarkdownStressWindow, serverChannel, mcpStdioManager, i18n }, build: async ({ dependsOn }) => setupSettingsWindowReusableFunc(dependsOn), }) const mainWindow = injeca.provide('windows:main', { - dependsOn: { settingsWindow, chatWindow, widgetsManager, noticeWindow, beatSync, autoUpdater, serverChannel, mcpStdioManager }, + dependsOn: { settingsWindow, chatWindow, widgetsManager, noticeWindow, beatSync, autoUpdater, serverChannel, mcpStdioManager, i18n }, build: async ({ dependsOn }) => setupMainWindow(dependsOn), }) const captionWindow = injeca.provide('windows:caption', { - dependsOn: { mainWindow, serverChannel }, + dependsOn: { mainWindow, serverChannel, i18n }, build: async ({ dependsOn }) => setupCaptionWindowManager(dependsOn), }) const tray = injeca.provide('app:tray', { - dependsOn: { mainWindow, settingsWindow, captionWindow, widgetsWindow: widgetsManager, beatSyncBgWindow: beatSync, aboutWindow }, + dependsOn: { mainWindow, settingsWindow, captionWindow, widgetsWindow: widgetsManager, beatSyncBgWindow: beatSync, aboutWindow, i18n }, build: async ({ dependsOn }) => setupTray(dependsOn), }) diff --git a/apps/stage-tamagotchi/src/main/libs/electron/persistence.ts b/apps/stage-tamagotchi/src/main/libs/electron/persistence.ts index 95b678dfb..bc9b42a9a 100644 --- a/apps/stage-tamagotchi/src/main/libs/electron/persistence.ts +++ b/apps/stage-tamagotchi/src/main/libs/electron/persistence.ts @@ -49,12 +49,19 @@ function parseWithSchema( return { issues: result.issues } } +export interface Config { + setup: () => ConfigDiagnostics> + get: () => InferOutput | undefined + update: (newData: InferOutput) => void + getDiagnostics: () => ConfigDiagnostics> | undefined +} + export function createConfig( namespace: string, filename: string, schema: TSchema, options?: CreateConfigOptions>, -) { +): Config { const key = `${namespace}:${filename}` const autoHeal = options?.autoHeal ?? Boolean(options?.default) diff --git a/apps/stage-tamagotchi/src/main/libs/i18n/index.ts b/apps/stage-tamagotchi/src/main/libs/i18n/index.ts new file mode 100644 index 000000000..a1c4378c3 --- /dev/null +++ b/apps/stage-tamagotchi/src/main/libs/i18n/index.ts @@ -0,0 +1,184 @@ +import type { + CoreOptions, + IsEmptyObject, + LocaleDetector, + NamedValue, + PickupPaths, + RemovedIndexResources, + TranslateOptions, +} from '@intlify/core' + +import { useLogg } from '@guiiai/logg' +import { createCoreContext, translate } from '@intlify/core' +import { effect, signal } from 'alien-signals' +import { isString } from 'es-toolkit' + +type ResolveResourceKeys< + // eslint-disable-next-line ts/no-empty-object-type + Schema extends Record = {}, + // eslint-disable-next-line ts/no-empty-object-type + DefineLocaleMessageSchema extends Record = {}, + DefinedLocaleMessage extends + RemovedIndexResources = RemovedIndexResources, + SchemaPaths = IsEmptyObject extends false + ? PickupPaths<{ [K in keyof Schema]: Schema[K] }> + : never, + DefineMessagesPaths = IsEmptyObject extends false + ? PickupPaths<{ + [K in keyof DefinedLocaleMessage]: DefinedLocaleMessage[K] + }> + : never, +> = SchemaPaths | DefineMessagesPaths + +interface TranslationFunction< + // eslint-disable-next-line ts/no-empty-object-type + Schema extends Record = {}, + // eslint-disable-next-line ts/no-empty-object-type + DefineLocaleMessageSchema extends Record = {}, + ResourceKeys = ResolveResourceKeys, +> { + /** + * @param {Key | ResourceKeys} key - A translation key + * @returns {string} A translated message, if the key is not found, return the key + */ + (key: Key | ResourceKeys): string + /** + * @param {Key | ResourceKeys} key - A translation key + * @param {number} plural - A plural choice number + * @returns {string} A translated message, if the key is not found, return the key + */ + (key: Key | ResourceKeys, plural: number): string + /** + * @param {Key | ResourceKeys} key - A translation key + * @param {number} plural - A plural choice number + * @param {TranslateOptions} options - A translate options, about details see {@link TranslateOptions} + * @returns {string} A translated message, if the key is not found, return the key + */ + (key: Key | ResourceKeys, plural: number, options: TranslateOptions): string + /** + * @param {Key | ResourceKeys} key - A translation key + * @param {string} defaultMsg - A default message, if the key is not found + * @returns {string} A translated message, if the key is not found, return the `defaultMsg` argument + */ + (key: Key | ResourceKeys, defaultMsg: string): string + /** + * @param {Key | ResourceKeys} key - A translation key + * @param {string} defaultMsg - A default message, if the key is not found + * @param {TranslateOptions} options - A translate options, about details see {@link TranslateOptions} + * @returns {string} A translated message, if the key is not found, return the `defaultMsg` argument + */ + ( + key: Key | ResourceKeys, + defaultMsg: string, + options: TranslateOptions + ): string + /** + * @param {Key | ResourceKeys} key - A translation key + * @param {unknown[]} list - A list for list interpolation + * @returns {string} A translated message, if the key is not found, return the key + */ + (key: Key | ResourceKeys, list: unknown[]): string + /** + * @param {Key | ResourceKeys} key - A translation key + * @param {unknown[]} list - A list for list interpolation + * @param {number} plural - A plural choice number + * @returns {string} A translated message, if the key is not found, return the key + */ + (key: Key | ResourceKeys, list: unknown[], plural: number): string + /** + * @param {Key | ResourceKeys} key - A translation key + * @param {unknown[]} list - A list for list interpolation + * @param {string} defaultMsg - A default message, if the key is not found + * @returns {string} A translated message, if the key is not found, return the `defaultMsg` argument + */ + (key: Key | ResourceKeys, list: unknown[], defaultMsg: string): string + /** + * @param {Key | ResourceKeys} key - A translation key + * @param {unknown[]} list - A list for list interpolation + * @param {TranslateOptions} options - A translate options, about details see {@link TranslateOptions} + * @returns {string} A translated message, if the key is not found, return the key + */ + (key: Key | ResourceKeys, list: unknown[], options: TranslateOptions): string + /** + * @param {Key | ResourceKeys} key - A translation key + * @param {NamedValue} named - A named value for named interpolation + * @returns {string} A translated message, if the key is not found, return the key + */ + (key: Key | ResourceKeys, named: NamedValue): string + /** + * @param {Key | ResourceKeys} key - A translation key + * @param {NamedValue} named - A named value for named interpolation + * @param {number} plural - A plural choice number + * @returns {string} A translated message, if the key is not found, return the key + */ + (key: Key | ResourceKeys, named: NamedValue, plural: number): string + /** + * @param {Key | ResourceKeys} key - A translation key + * @param {NamedValue} named - A named value for named interpolation + * @param {string} defaultMsg - A default message, if the key is not found + * @returns {string} A translated message, if the key is not found, return the `defaultMsg` argument + */ + (key: Key | ResourceKeys, named: NamedValue, defaultMsg: string): string + /** + * @param {Key | ResourceKeys} key - A translation key + * @param {NamedValue} named - A named value for named interpolation + * @param {TranslateOptions} options - A translate options, about details see {@link TranslateOptions} + * @returns {string} A translated message, if the key is not found, return the key + */ + ( + key: Key | ResourceKeys, + named: NamedValue, + options: TranslateOptions + ): string +} + +export interface I18n = Record> { + t: TranslationFunction + locale: + (() => (string | LocaleDetector | undefined)) | ((value: string | LocaleDetector | undefined) => void) +} + +export function createI18n = Record>(options: CoreOptions): I18n { + const log = useLogg('i18n').useGlobalConfig() + + const locale = signal(options.locale) + + const context = createCoreContext({ + fallbackLocale: options.fallbackLocale, + fallbackWarn: false, + missingWarn: false, + warnHtmlMessage: false, + fallbackFormat: true, + ...options, + }) + + const t: TranslationFunction = ( + key: string, + ...args: unknown[] + ) => { + if (context == null) { + log.error('cannot initialize core context for i18n') + + return key + } + + const ret = Reflect.apply(translate, null, [context, key, ...args]) + return isString(ret) ? ret : key + } + + effect(() => { + locale() + + if (context != null) { + const l = locale() + if (l != null) { + context.locale = l + } + } + }) + + return { + t, + locale, + } +} diff --git a/apps/stage-tamagotchi/src/main/services/airi/i18n/index.ts b/apps/stage-tamagotchi/src/main/services/airi/i18n/index.ts new file mode 100644 index 000000000..19363c24c --- /dev/null +++ b/apps/stage-tamagotchi/src/main/services/airi/i18n/index.ts @@ -0,0 +1,28 @@ +import type { LocaleDetector } from '@intlify/core' +import type { createContext } from '@moeru/eventa/adapters/electron/main' +import type { BrowserWindow } from 'electron' +import type { ProvidedBy } from 'injeca' + +import type { globalAppConfigSchema } from '../../../configs/global' +import type { Config } from '../../../libs/electron/persistence' +import type { I18n } from '../../../libs/i18n' + +import { defineInvokeHandler } from '@moeru/eventa' +import { injeca } from 'injeca' + +import { i18nGetLocale, i18nSetLocale } from '../../../../shared/eventa' + +export async function createI18nService(params: { context: ReturnType['context'], window: BrowserWindow, i18n: I18n }) { + const { config } = await injeca.resolve({ config: 'configs:app' } as { config: ProvidedBy> }) + params.i18n.locale(config.get()?.language || 'en') + + defineInvokeHandler(params.context, i18nSetLocale, (locale) => { + config.update({ ...config.get(), language: locale }) + params.i18n.locale(locale) + }) + + defineInvokeHandler(params.context, i18nGetLocale, () => { + const locale = params.i18n.locale as () => string | LocaleDetector | undefined + return locale() + }) +} diff --git a/apps/stage-tamagotchi/src/main/windows/about/index.ts b/apps/stage-tamagotchi/src/main/windows/about/index.ts index cc3a84864..1dd1b3488 100644 --- a/apps/stage-tamagotchi/src/main/windows/about/index.ts +++ b/apps/stage-tamagotchi/src/main/windows/about/index.ts @@ -1,3 +1,5 @@ +import type { I18n } from '../../libs/i18n' +import type { ServerChannel } from '../../services/airi/channel-server' import type { AutoUpdater } from '../../services/electron/auto-updater' import { join, resolve } from 'node:path' @@ -10,7 +12,11 @@ import { baseUrl, getElectronMainDirname, load, withHashRoute } from '../../libs import { createReusableWindow } from '../../libs/electron/window-manager' import { setupAboutWindowElectronInvokes } from './rpc/index.electron' -export function setupAboutWindowReusable(params: { autoUpdater: AutoUpdater }) { +export function setupAboutWindowReusable(params: { + autoUpdater: AutoUpdater + i18n: I18n + serverChannel: ServerChannel +}) { return createReusableWindow(async () => { const window = new BrowserWindow({ title: 'About AIRI', @@ -35,7 +41,12 @@ export function setupAboutWindowReusable(params: { autoUpdater: AutoUpdater }) { await load(window, withHashRoute(baseUrl(resolve(getElectronMainDirname(), '..', 'renderer')), '/about')) - setupAboutWindowElectronInvokes({ window, autoUpdater: params.autoUpdater }) + await setupAboutWindowElectronInvokes({ + window, + autoUpdater: params.autoUpdater, + i18n: params.i18n, + serverChannel: params.serverChannel, + }) return window }).getWindow diff --git a/apps/stage-tamagotchi/src/main/windows/about/rpc/index.electron.ts b/apps/stage-tamagotchi/src/main/windows/about/rpc/index.electron.ts index c48b34fea..42e47e2f8 100644 --- a/apps/stage-tamagotchi/src/main/windows/about/rpc/index.electron.ts +++ b/apps/stage-tamagotchi/src/main/windows/about/rpc/index.electron.ts @@ -1,13 +1,21 @@ import type { BrowserWindow } from 'electron' +import type { I18n } from '../../../libs/i18n' +import type { ServerChannel } from '../../../services/airi/channel-server' import type { AutoUpdater } from '../../../services/electron/auto-updater' import { createContext } from '@moeru/eventa/adapters/electron/main' import { ipcMain } from 'electron' import { createAutoUpdaterService } from '../../../services/electron' +import { setupBaseWindowElectronInvokes } from '../../shared/window' -export function setupAboutWindowElectronInvokes(params: { window: BrowserWindow, autoUpdater: AutoUpdater }) { +export async function setupAboutWindowElectronInvokes(params: { + window: BrowserWindow + autoUpdater: AutoUpdater + i18n: I18n + serverChannel: ServerChannel +}) { // TODO: once we refactored eventa to support window-namespaced contexts, // we can remove the setMaxListeners call below since eventa will be able to dispatch and // manage events within eventa's context system. @@ -15,5 +23,7 @@ export function setupAboutWindowElectronInvokes(params: { window: BrowserWindow, const { context } = createContext(ipcMain, params.window) + await setupBaseWindowElectronInvokes({ context, window: params.window, i18n: params.i18n, serverChannel: params.serverChannel }) + createAutoUpdaterService({ context, window: params.window, service: params.autoUpdater }) } diff --git a/apps/stage-tamagotchi/src/main/windows/caption/index.ts b/apps/stage-tamagotchi/src/main/windows/caption/index.ts index 80d2a960c..b73ebf2ee 100644 --- a/apps/stage-tamagotchi/src/main/windows/caption/index.ts +++ b/apps/stage-tamagotchi/src/main/windows/caption/index.ts @@ -1,6 +1,7 @@ import type { BrowserWindow, BrowserWindowConstructorOptions, Rectangle } from 'electron' import type { InferOutput } from 'valibot' +import type { I18n } from '../../libs/i18n' import type { ServerChannel } from '../../services/airi/channel-server' import { createHash } from 'node:crypto' @@ -20,7 +21,6 @@ import { captionGetIsFollowingWindow, captionIsFollowingWindowChanged } from '.. import { baseUrl, getElectronMainDirname, load, withHashRoute } from '../../libs/electron/location' import { createConfig } from '../../libs/electron/persistence' import { createReusableWindow } from '../../libs/electron/window-manager' -import { createServerChannelService } from '../../services/airi/channel-server' import { mapForBreakpoints, resolutionBreakpoints, widthFrom } from '../shared/display' import { setupBaseWindowElectronInvokes, transparentWindowConfig } from '../shared/window' @@ -148,6 +148,7 @@ function createCaptionWindow(options?: BrowserWindowConstructorOptions) { export function setupCaptionWindowManager(params: { mainWindow: BrowserWindow serverChannel: ServerChannel + i18n: I18n }) { const matrixHash = computeDisplayMatrixHash() @@ -274,8 +275,7 @@ export function setupCaptionWindowManager(params: { const { context } = createContext(ipcMain, window) eventaContext = context - setupBaseWindowElectronInvokes({ context, window }) - createServerChannelService({ serverChannel: params.serverChannel }) + await setupBaseWindowElectronInvokes({ context, window, serverChannel: params.serverChannel, i18n: params.i18n }) const cfg = getConfig() const saved = cfg?.matrices?.[matrixHash]?.bounds diff --git a/apps/stage-tamagotchi/src/main/windows/chat/index.ts b/apps/stage-tamagotchi/src/main/windows/chat/index.ts index 35800d40e..32fe07e59 100644 --- a/apps/stage-tamagotchi/src/main/windows/chat/index.ts +++ b/apps/stage-tamagotchi/src/main/windows/chat/index.ts @@ -1,3 +1,4 @@ +import type { I18n } from '../../libs/i18n' import type { ServerChannel } from '../../services/airi/channel-server' import type { McpStdioManager } from '../../services/airi/mcp-servers' import type { WidgetsWindowManager } from '../widgets' @@ -16,6 +17,7 @@ export function setupChatWindowReusableFunc(params: { widgetsManager: WidgetsWindowManager serverChannel: ServerChannel mcpStdioManager: McpStdioManager + i18n: I18n }) { return createReusableWindow(async () => { const window = new BrowserWindow({ @@ -38,11 +40,12 @@ export function setupChatWindowReusableFunc(params: { await load(window, withHashRoute(baseUrl(resolve(getElectronMainDirname(), '..', 'renderer')), '/chat')) - setupChatWindowElectronInvokes({ + await setupChatWindowElectronInvokes({ window, widgetsManager: params.widgetsManager, serverChannel: params.serverChannel, mcpStdioManager: params.mcpStdioManager, + i18n: params.i18n, }) return window diff --git a/apps/stage-tamagotchi/src/main/windows/chat/rpc/index.electron.ts b/apps/stage-tamagotchi/src/main/windows/chat/rpc/index.electron.ts index 368216d6a..25f228e9f 100644 --- a/apps/stage-tamagotchi/src/main/windows/chat/rpc/index.electron.ts +++ b/apps/stage-tamagotchi/src/main/windows/chat/rpc/index.electron.ts @@ -1,5 +1,6 @@ import type { BrowserWindow } from 'electron' +import type { I18n } from '../../../libs/i18n' import type { ServerChannel } from '../../../services/airi/channel-server' import type { McpStdioManager } from '../../../services/airi/mcp-servers' import type { WidgetsWindowManager } from '../../widgets' @@ -9,16 +10,16 @@ import { createContext } from '@moeru/eventa/adapters/electron/main' import { ipcMain } from 'electron' import { electronOpenMainDevtools } from '../../../../shared/eventa' -import { createServerChannelService } from '../../../services/airi/channel-server' import { createMcpServersService } from '../../../services/airi/mcp-servers' import { createWidgetsService } from '../../../services/airi/widgets' -import { createScreenService, createWindowService } from '../../../services/electron' +import { setupBaseWindowElectronInvokes } from '../../shared/window' -export function setupChatWindowElectronInvokes(params: { +export async function setupChatWindowElectronInvokes(params: { window: BrowserWindow widgetsManager: WidgetsWindowManager serverChannel: ServerChannel mcpStdioManager: McpStdioManager + i18n: I18n }) { // TODO: once we refactored eventa to support window-namespaced contexts, // we can remove the setMaxListeners call below since eventa will be able to dispatch and @@ -27,10 +28,9 @@ export function setupChatWindowElectronInvokes(params: { const { context } = createContext(ipcMain, params.window) - createScreenService({ context, window: params.window }) - createWindowService({ context, window: params.window }) + await setupBaseWindowElectronInvokes({ context, window: params.window, i18n: params.i18n, serverChannel: params.serverChannel }) + createWidgetsService({ context, widgetsManager: params.widgetsManager, window: params.window }) - createServerChannelService({ serverChannel: params.serverChannel }) createMcpServersService({ context, manager: params.mcpStdioManager }) defineInvokeHandler(context, electronOpenMainDevtools, () => params.window.webContents.openDevTools({ mode: 'detach' })) diff --git a/apps/stage-tamagotchi/src/main/windows/dashboard/index.ts b/apps/stage-tamagotchi/src/main/windows/dashboard/index.ts index ffefddbef..b5cdf64f6 100644 --- a/apps/stage-tamagotchi/src/main/windows/dashboard/index.ts +++ b/apps/stage-tamagotchi/src/main/windows/dashboard/index.ts @@ -1,6 +1,8 @@ import type { Rectangle } from 'electron' import type { InferOutput } from 'valibot' +import type { I18n } from '../../libs/i18n' +import type { ServerChannel } from '../../services/airi/channel-server' import type { NoticeWindowManager } from '../notice' import { dirname, join, resolve } from 'node:path' @@ -43,6 +45,8 @@ export async function setupDashboardWindow(params: { chatWindow: () => Promise noticeWindow: NoticeWindowManager onWindowCreated?: (window: BrowserWindow) => void + serverChannel: ServerChannel + i18n: I18n }) { const { setup: setupConfig, @@ -129,11 +133,13 @@ export async function setupDashboardWindow(params: { await load(window, withHashRoute(baseUrl(resolve(getElectronMainDirname(), '..', 'renderer')), '/dashboard')) - setupDashboardWindowElectronInvokes({ + await setupDashboardWindowElectronInvokes({ window, settingsWindow: params.settingsWindow, chatWindow: params.chatWindow, noticeWindow: params.noticeWindow, + i18n: params.i18n, + serverChannel: params.serverChannel, }) /** diff --git a/apps/stage-tamagotchi/src/main/windows/dashboard/rpc/index.electron.ts b/apps/stage-tamagotchi/src/main/windows/dashboard/rpc/index.electron.ts index be5cb938d..e1d7d3b31 100644 --- a/apps/stage-tamagotchi/src/main/windows/dashboard/rpc/index.electron.ts +++ b/apps/stage-tamagotchi/src/main/windows/dashboard/rpc/index.electron.ts @@ -1,5 +1,7 @@ import type { BrowserWindow } from 'electron' +import type { I18n } from '../../../libs/i18n' +import type { ServerChannel } from '../../../services/airi/channel-server' import type { NoticeWindowManager } from '../../notice' import { defineInvokeHandler } from '@moeru/eventa' @@ -10,11 +12,13 @@ import { electronOpenChat, electronOpenMainDevtools, electronOpenSettings, notic import { toggleWindowShow } from '../../shared' import { setupBaseWindowElectronInvokes } from '../../shared/window' -export function setupDashboardWindowElectronInvokes(params: { +export async function setupDashboardWindowElectronInvokes(params: { window: BrowserWindow settingsWindow: () => Promise chatWindow: () => Promise noticeWindow: NoticeWindowManager + i18n: I18n + serverChannel: ServerChannel }) { // TODO: once we refactored eventa to support window-namespaced contexts, // we can remove the setMaxListeners call below since eventa will be able to dispatch and @@ -23,7 +27,7 @@ export function setupDashboardWindowElectronInvokes(params: { const { context } = createContext(ipcMain, params.window) - setupBaseWindowElectronInvokes({ context, window: params.window }) + await setupBaseWindowElectronInvokes({ context, window: params.window, serverChannel: params.serverChannel, i18n: params.i18n }) defineInvokeHandler(context, electronOpenMainDevtools, () => params.window.webContents.openDevTools({ mode: 'detach' })) defineInvokeHandler(context, electronOpenSettings, async () => toggleWindowShow(await params.settingsWindow())) diff --git a/apps/stage-tamagotchi/src/main/windows/inlay/index.ts b/apps/stage-tamagotchi/src/main/windows/inlay/index.ts index 71a86013e..3d2a9a109 100644 --- a/apps/stage-tamagotchi/src/main/windows/inlay/index.ts +++ b/apps/stage-tamagotchi/src/main/windows/inlay/index.ts @@ -1,3 +1,6 @@ +import type { I18n } from '../../libs/i18n' +import type { ServerChannel } from '../../services/airi/channel-server' + import { join, resolve } from 'node:path' import { BrowserWindow, shell } from 'electron' @@ -10,7 +13,10 @@ import { currentDisplayBounds, mapForBreakpoints, resolutionBreakpoints, widthFr import { spotlightLikeWindowConfig } from '../shared/window' import { setupInlayWindowInvokes } from './rpc/index.electron' -export async function setupInlayWindow() { +export async function setupInlayWindow(params: { + serverChannel: ServerChannel + i18n: I18n +}) { const window = new BrowserWindow({ title: 'Inlay', width: 450, @@ -63,7 +69,7 @@ export async function setupInlayWindow() { await load(window, withHashRoute(baseUrl(resolve(getElectronMainDirname(), '..', 'renderer')), '/inlay')) - setupInlayWindowInvokes({ inlayWindow: window }) + await setupInlayWindowInvokes({ inlayWindow: window, serverChannel: params.serverChannel, i18n: params.i18n }) return window } diff --git a/apps/stage-tamagotchi/src/main/windows/inlay/rpc/index.electron.ts b/apps/stage-tamagotchi/src/main/windows/inlay/rpc/index.electron.ts index 8943d47e6..e672e142e 100644 --- a/apps/stage-tamagotchi/src/main/windows/inlay/rpc/index.electron.ts +++ b/apps/stage-tamagotchi/src/main/windows/inlay/rpc/index.electron.ts @@ -1,12 +1,17 @@ import type { BrowserWindow } from 'electron' +import type { I18n } from '../../../libs/i18n' +import type { ServerChannel } from '../../../services/airi/channel-server' + import { createContext } from '@moeru/eventa/adapters/electron/main' import { ipcMain } from 'electron' -import { createWindowService } from '../../../services/electron' +import { setupBaseWindowElectronInvokes } from '../../shared/window' export async function setupInlayWindowInvokes(params: { inlayWindow: BrowserWindow + serverChannel: ServerChannel + i18n: I18n }) { // TODO: once we refactored eventa to support window-namespaced contexts, // we can remove the setMaxListeners call below since eventa will be able to dispatch and @@ -15,5 +20,10 @@ export async function setupInlayWindowInvokes(params: { const { context } = createContext(ipcMain, params.inlayWindow) - createWindowService({ context, window: params.inlayWindow }) + await setupBaseWindowElectronInvokes({ + context, + window: params.inlayWindow, + serverChannel: params.serverChannel, + i18n: params.i18n, + }) } diff --git a/apps/stage-tamagotchi/src/main/windows/main/index.ts b/apps/stage-tamagotchi/src/main/windows/main/index.ts index aa87193c1..0f9d9c6f3 100644 --- a/apps/stage-tamagotchi/src/main/windows/main/index.ts +++ b/apps/stage-tamagotchi/src/main/windows/main/index.ts @@ -1,6 +1,7 @@ import type { Rectangle } from 'electron' import type { InferOutput } from 'valibot' +import type { I18n } from '../../libs/i18n' import type { ServerChannel } from '../../services/airi/channel-server' import type { McpStdioManager } from '../../services/airi/mcp-servers' import type { AutoUpdater } from '../../services/electron/auto-updater' @@ -52,6 +53,7 @@ export async function setupMainWindow(params: { onWindowCreated?: (window: BrowserWindow) => void serverChannel: ServerChannel mcpStdioManager: McpStdioManager + i18n: I18n }) { const { setup: setupConfig, @@ -155,7 +157,7 @@ export async function setupMainWindow(params: { await load(window, baseUrl(resolve(getElectronMainDirname(), '..', 'renderer'))) - setupMainWindowElectronInvokes({ + await setupMainWindowElectronInvokes({ window, settingsWindow: params.settingsWindow, chatWindow: params.chatWindow, @@ -164,6 +166,7 @@ export async function setupMainWindow(params: { autoUpdater: params.autoUpdater, serverChannel: params.serverChannel, mcpStdioManager: params.mcpStdioManager, + i18n: params.i18n, }) /** diff --git a/apps/stage-tamagotchi/src/main/windows/main/rpc/index.electron.ts b/apps/stage-tamagotchi/src/main/windows/main/rpc/index.electron.ts index e3dc76b52..f9eb06e00 100644 --- a/apps/stage-tamagotchi/src/main/windows/main/rpc/index.electron.ts +++ b/apps/stage-tamagotchi/src/main/windows/main/rpc/index.electron.ts @@ -1,5 +1,6 @@ import type { BrowserWindow } from 'electron' +import type { I18n } from '../../../libs/i18n' import type { ServerChannel } from '../../../services/airi/channel-server' import type { McpStdioManager } from '../../../services/airi/mcp-servers' import type { AutoUpdater } from '../../../services/electron/auto-updater' @@ -11,14 +12,13 @@ import { createContext } from '@moeru/eventa/adapters/electron/main' import { ipcMain } from 'electron' import { electronOpenChat, electronOpenMainDevtools, electronOpenSettings, noticeWindowEventa } from '../../../../shared/eventa' -import { createServerChannelService } from '../../../services/airi/channel-server' import { createMcpServersService } from '../../../services/airi/mcp-servers' import { createWidgetsService } from '../../../services/airi/widgets' import { createAutoUpdaterService } from '../../../services/electron' import { toggleWindowShow } from '../../shared' import { setupBaseWindowElectronInvokes } from '../../shared/window' -export function setupMainWindowElectronInvokes(params: { +export async function setupMainWindowElectronInvokes(params: { window: BrowserWindow settingsWindow: () => Promise chatWindow: () => Promise @@ -27,6 +27,7 @@ export function setupMainWindowElectronInvokes(params: { autoUpdater: AutoUpdater serverChannel: ServerChannel mcpStdioManager: McpStdioManager + i18n: I18n }) { // TODO: once we refactored eventa to support window-namespaced contexts, // we can remove the setMaxListeners call below since eventa will be able to dispatch and @@ -35,10 +36,9 @@ export function setupMainWindowElectronInvokes(params: { const { context } = createContext(ipcMain, params.window) - setupBaseWindowElectronInvokes({ context, window: params.window }) + await setupBaseWindowElectronInvokes({ context, window: params.window, serverChannel: params.serverChannel, i18n: params.i18n }) createWidgetsService({ context, widgetsManager: params.widgetsManager, window: params.window }) createAutoUpdaterService({ context, window: params.window, service: params.autoUpdater }) - createServerChannelService({ serverChannel: params.serverChannel }) createMcpServersService({ context, manager: params.mcpStdioManager }) defineInvokeHandler(context, electronOpenMainDevtools, () => params.window.webContents.openDevTools({ mode: 'detach' })) diff --git a/apps/stage-tamagotchi/src/main/windows/notice/index.ts b/apps/stage-tamagotchi/src/main/windows/notice/index.ts index 970ed37a2..4b2180c25 100644 --- a/apps/stage-tamagotchi/src/main/windows/notice/index.ts +++ b/apps/stage-tamagotchi/src/main/windows/notice/index.ts @@ -1,6 +1,8 @@ import type { BrowserWindow } from 'electron' import type { RequestWindowPayload } from '../../../shared/eventa' +import type { I18n } from '../../libs/i18n' +import type { ServerChannel } from '../../services/airi/channel-server' import { join, resolve } from 'node:path' @@ -17,7 +19,10 @@ export interface NoticeWindowManager { open: (payload: RequestWindowPayload) => Promise } -export function setupNoticeWindowManager(): NoticeWindowManager { +export function setupNoticeWindowManager(params: { + i18n: I18n + serverChannel: ServerChannel +}): NoticeWindowManager { const rendererBase = baseUrl(resolve(getElectronMainDirname(), '..', 'renderer')) function createWindow(_id: string): BrowserWindow { @@ -48,6 +53,8 @@ export function setupNoticeWindowManager(): NoticeWindowManager { const manager = createReferencedWindowManager({ eventa: noticeWindowEventa, + i18n: params.i18n, + serverChannel: params.serverChannel, createWindow, loadRoute: loadNoticeRoute, }) diff --git a/apps/stage-tamagotchi/src/main/windows/notice/rpc/index.electron.ts b/apps/stage-tamagotchi/src/main/windows/notice/rpc/index.electron.ts deleted file mode 100644 index 9afb5ae7c..000000000 --- a/apps/stage-tamagotchi/src/main/windows/notice/rpc/index.electron.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { BrowserWindow } from 'electron' - -import type { RequestWindowActionDefault } from '../../../../shared/eventa' - -import { defineInvokeHandler } from '@moeru/eventa' -import { createContext } from '@moeru/eventa/adapters/electron/main' -import { ipcMain } from 'electron' - -import { noticeWindowEventa } from '../../../../shared/eventa' -import { createWindowService } from '../../../services/electron' - -export function setupNoticeWindowInvokes(params: { - window: BrowserWindow - onAction: (payload: { id?: string, action: RequestWindowActionDefault }) => void - onPageMounted: () => { id: string, type?: string, payload?: Record } | undefined - onPageUnmounted: () => void -}) { - // TODO: once we refactored eventa to support window-namespaced contexts, - // we can remove the setMaxListeners call below since eventa will be able to dispatch and - // manage events within eventa's context system. - ipcMain.setMaxListeners(0) - - const { context } = createContext(ipcMain, params.window) - - createWindowService({ context, window: params.window }) - - defineInvokeHandler(context, noticeWindowEventa.windowAction, (payload) => { - params.onAction({ id: payload?.id, action: payload?.action ?? 'close' }) - }) - defineInvokeHandler(context, noticeWindowEventa.pageMounted, () => params.onPageMounted()) - defineInvokeHandler(context, noticeWindowEventa.pageUnmounted, () => params.onPageUnmounted()) - - return { context } -} diff --git a/apps/stage-tamagotchi/src/main/windows/settings/index.ts b/apps/stage-tamagotchi/src/main/windows/settings/index.ts index abf7c83c9..429869f47 100644 --- a/apps/stage-tamagotchi/src/main/windows/settings/index.ts +++ b/apps/stage-tamagotchi/src/main/windows/settings/index.ts @@ -1,3 +1,4 @@ +import type { I18n } from '../../libs/i18n' import type { ServerChannel } from '../../services/airi/channel-server' import type { McpStdioManager } from '../../services/airi/mcp-servers' import type { AutoUpdater } from '../../services/electron/auto-updater' @@ -22,6 +23,7 @@ export function setupSettingsWindowReusableFunc(params: { onWindowCreated?: (window: BrowserWindow) => void serverChannel: ServerChannel mcpStdioManager: McpStdioManager + i18n: I18n }) { return createReusableWindow(async () => { const window = new BrowserWindow({ @@ -54,6 +56,7 @@ export function setupSettingsWindowReusableFunc(params: { devtoolsMarkdownStressWindow: params.devtoolsMarkdownStressWindow, serverChannel: params.serverChannel, mcpStdioManager: params.mcpStdioManager, + i18n: params.i18n, }) initScreenCaptureForWindow(window) diff --git a/apps/stage-tamagotchi/src/main/windows/settings/rpc/index.electron.ts b/apps/stage-tamagotchi/src/main/windows/settings/rpc/index.electron.ts index 0cb7da594..ce29005aa 100644 --- a/apps/stage-tamagotchi/src/main/windows/settings/rpc/index.electron.ts +++ b/apps/stage-tamagotchi/src/main/windows/settings/rpc/index.electron.ts @@ -1,5 +1,6 @@ import type { BrowserWindow } from 'electron' +import type { I18n } from '../../../libs/i18n' import type { ServerChannel } from '../../../services/airi/channel-server' import type { McpStdioManager } from '../../../services/airi/mcp-servers' import type { AutoUpdater } from '../../../services/electron/auto-updater' @@ -11,10 +12,10 @@ import { createContext } from '@moeru/eventa/adapters/electron/main' import { ipcMain } from 'electron' import { electronOpenDevtoolsWindow, electronOpenSettingsDevtools } from '../../../../shared/eventa' -import { createServerChannelService } from '../../../services/airi/channel-server' import { createMcpServersService } from '../../../services/airi/mcp-servers' import { createWidgetsService } from '../../../services/airi/widgets' -import { createAutoUpdaterService, createScreenService, createWindowService } from '../../../services/electron' +import { createAutoUpdaterService } from '../../../services/electron' +import { setupBaseWindowElectronInvokes } from '../../shared/window' export async function setupSettingsWindowInvokes(params: { settingsWindow: BrowserWindow @@ -23,6 +24,7 @@ export async function setupSettingsWindowInvokes(params: { devtoolsMarkdownStressWindow: DevtoolsWindowManager serverChannel: ServerChannel mcpStdioManager: McpStdioManager + i18n: I18n }) { // TODO: once we refactored eventa to support window-namespaced contexts, // we can remove the setMaxListeners call below since eventa will be able to dispatch and @@ -31,11 +33,10 @@ export async function setupSettingsWindowInvokes(params: { const { context } = createContext(ipcMain, params.settingsWindow) - createScreenService({ context, window: params.settingsWindow }) - createWindowService({ context, window: params.settingsWindow }) + await setupBaseWindowElectronInvokes({ context, window: params.settingsWindow, i18n: params.i18n, serverChannel: params.serverChannel }) + createWidgetsService({ context, widgetsManager: params.widgetsManager, window: params.settingsWindow }) createAutoUpdaterService({ context, window: params.settingsWindow, service: params.autoUpdater }) - createServerChannelService({ serverChannel: params.serverChannel }) createMcpServersService({ context, manager: params.mcpStdioManager }) defineInvokeHandler(context, electronOpenSettingsDevtools, async () => params.settingsWindow.webContents.openDevTools({ mode: 'detach' })) diff --git a/apps/stage-tamagotchi/src/main/windows/shared/referenced-window.ts b/apps/stage-tamagotchi/src/main/windows/shared/referenced-window.ts index 4b35d7cba..aef61aa6a 100644 --- a/apps/stage-tamagotchi/src/main/windows/shared/referenced-window.ts +++ b/apps/stage-tamagotchi/src/main/windows/shared/referenced-window.ts @@ -1,11 +1,15 @@ import type { BrowserWindow } from 'electron' import type { createRequestWindowEventa, RequestWindowPayload } from '../../../shared/eventa' +import type { I18n } from '../../libs/i18n' +import type { ServerChannel } from '../../services/airi/channel-server' import { defineInvokeHandler } from '@moeru/eventa' import { createContext } from '@moeru/eventa/adapters/electron/main' import { ipcMain } from 'electron' +import { setupBaseWindowElectronInvokes } from './window' + export interface ReferencedWindowHandle { id: string window: BrowserWindow @@ -25,12 +29,14 @@ export interface ReferencedWindowManager(params: { eventa: ReturnType + i18n: I18n + serverChannel: ServerChannel createWindow: (id: string) => BrowserWindow loadRoute: (window: BrowserWindow, payload: Payload & { id: string }) => Promise }): ReferencedWindowManager { const windows = new Map['context'] }>() - function bindContext(id: string, payload: Payload, win: BrowserWindow) { + async function bindContext(id: string, payload: Payload, win: BrowserWindow) { // TODO: once we refactored eventa to support window-namespaced contexts, // we can remove the setMaxListeners call below since eventa will be able to dispatch and // manage events within eventa's context system. @@ -49,6 +55,8 @@ export function createReferencedWindowManager windows.delete(id)) return { window: win, context } @@ -60,7 +68,7 @@ export function createReferencedWindowManager['context'] window: BrowserWindow + serverChannel: ServerChannel + i18n: I18n }) { createScreenService({ context: params.context, window: params.window }) createWindowService({ context: params.context, window: params.window }) createAppService({ context: params.context, window: params.window }) + await createI18nService({ context: params.context, window: params.window, i18n: params.i18n }) + + createServerChannelService({ serverChannel: params.serverChannel }) } diff --git a/apps/stage-tamagotchi/src/main/windows/widgets/index.ts b/apps/stage-tamagotchi/src/main/windows/widgets/index.ts index 5488da43b..25ff283dc 100644 --- a/apps/stage-tamagotchi/src/main/windows/widgets/index.ts +++ b/apps/stage-tamagotchi/src/main/windows/widgets/index.ts @@ -2,6 +2,8 @@ import type { BrowserWindow, Rectangle } from 'electron' import type { InferOutput } from 'valibot' import type { WidgetsAddPayload, WidgetSnapshot } from '../../../shared/eventa' +import type { I18n } from '../../libs/i18n' +import type { ServerChannel } from '../../services/airi/channel-server' import { join, resolve } from 'node:path' @@ -93,7 +95,10 @@ interface WidgetWindowContext { window?: BrowserWindow } -export function setupWidgetsWindowManager(): WidgetsWindowManager { +export function setupWidgetsWindowManager(params: { + serverChannel: ServerChannel + i18n: I18n +}): WidgetsWindowManager { const { setup, get: getConfigRaw, update } = createConfig('windows-widgets', 'config.json', widgetsWindowConfigSchema, { default: {}, autoHeal: true, @@ -144,7 +149,14 @@ export function setupWidgetsWindowManager(): WidgetsWindowManager { const initialRoute = pendingRoute ?? defaultRoute await loadWithRoute(window, initialRoute) - await setupWidgetsWindowInvokes({ widgetWindow: window, widgetsManager: widgetsManager! }) + + await setupWidgetsWindowInvokes({ + widgetWindow: window, + widgetsManager: widgetsManager!, + i18n: params.i18n, + serverChannel: params.serverChannel, + }) + pendingRoute = undefined window.on('closed', () => { diff --git a/apps/stage-tamagotchi/src/main/windows/widgets/rpc/index.electron.ts b/apps/stage-tamagotchi/src/main/windows/widgets/rpc/index.electron.ts index 3c501286f..bcb4334b3 100644 --- a/apps/stage-tamagotchi/src/main/windows/widgets/rpc/index.electron.ts +++ b/apps/stage-tamagotchi/src/main/windows/widgets/rpc/index.electron.ts @@ -1,14 +1,21 @@ import type { BrowserWindow } from 'electron' +import type { I18n } from '../../../libs/i18n' +import type { ServerChannel } from '../../../services/airi/channel-server' import type { WidgetsWindowManager } from '../../widgets' import { createContext } from '@moeru/eventa/adapters/electron/main' import { ipcMain } from 'electron' import { createWidgetsService } from '../../../services/airi/widgets' -import { createScreenService, createWindowService } from '../../../services/electron' +import { setupBaseWindowElectronInvokes } from '../../shared/window' -export async function setupWidgetsWindowInvokes(params: { widgetWindow: BrowserWindow, widgetsManager: WidgetsWindowManager }) { +export async function setupWidgetsWindowInvokes(params: { + widgetWindow: BrowserWindow + widgetsManager: WidgetsWindowManager + i18n: I18n + serverChannel: ServerChannel +}) { // TODO: once we refactored eventa to support window-namespaced contexts, // we can remove the setMaxListeners call below since eventa will be able to dispatch and // manage events within eventa's context system. @@ -16,7 +23,7 @@ export async function setupWidgetsWindowInvokes(params: { widgetWindow: BrowserW const { context } = createContext(ipcMain, params.widgetWindow) - createScreenService({ context, window: params.widgetWindow }) - createWindowService({ context, window: params.widgetWindow }) + setupBaseWindowElectronInvokes({ context, window: params.widgetWindow, i18n: params.i18n, serverChannel: params.serverChannel }) + createWidgetsService({ context, widgetsManager: params.widgetsManager, window: params.widgetWindow }) } diff --git a/apps/stage-tamagotchi/src/renderer/App.vue b/apps/stage-tamagotchi/src/renderer/App.vue index 86a3c9f90..135656ee8 100644 --- a/apps/stage-tamagotchi/src/renderer/App.vue +++ b/apps/stage-tamagotchi/src/renderer/App.vue @@ -38,6 +38,7 @@ import { electronPluginUnload, electronPluginUpdateCapability, electronStartTrackMousePosition, + i18nSetLocale, pluginProtocolListProviders, pluginProtocolListProvidersEventName, } from '../shared/eventa' @@ -73,6 +74,7 @@ const startTrackingCursorPoint = useElectronEventaInvoke(electronStartTrackMouse const reportPluginCapability = useElectronEventaInvoke(electronPluginUpdateCapability) const listMcpTools = useElectronEventaInvoke(electronMcpListTools) const callMcpTool = useElectronEventaInvoke(electronMcpCallTool) +const setLocale = useElectronEventaInvoke(i18nSetLocale) // NOTICE: register plugin host bridge during setup to avoid race with pages using it in immediate watchers. pluginHostInspectorStore.setBridge({ @@ -93,6 +95,7 @@ setMcpToolBridge({ watch(language, () => { i18n.locale.value = language.value + setLocale(language.value) }) const { updateThemeColor } = useThemeColor(themeColorFromValue({ light: 'rgb(255 255 255)', dark: 'rgb(18 18 18)' })) diff --git a/apps/stage-tamagotchi/src/shared/eventa.ts b/apps/stage-tamagotchi/src/shared/eventa.ts index 212e2bb19..8ac4e0fa5 100644 --- a/apps/stage-tamagotchi/src/shared/eventa.ts +++ b/apps/stage-tamagotchi/src/shared/eventa.ts @@ -1,3 +1,5 @@ +import type { Locale } from '@intlify/core' + import { defineEventa, defineInvokeEventa } from '@moeru/eventa' export const electronStartTrackMousePosition = defineInvokeEventa('eventa:invoke:electron:start-tracking-mouse-position') @@ -203,5 +205,8 @@ export const widgetsRemoveEvent = defineEventa<{ id: string }>('eventa:event:ele export const widgetsClearEvent = defineEventa('eventa:event:electron:windows:widgets:clear') export const widgetsUpdateEvent = defineEventa<{ id: string, componentProps?: Record }>('eventa:event:electron:windows:widgets:update') +export const i18nSetLocale = defineInvokeEventa('eventa:invoke:electron:i18n:set-locale') +export const i18nGetLocale = defineInvokeEventa('eventa:invoke:electron:i18n:get-locale') + export { electron } from '@proj-airi/electron-eventa' export * from '@proj-airi/electron-eventa/electron-updater' diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4b9a89eb7..2720d2143 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,6 +39,9 @@ catalogs: '@iconify-json/tabler': specifier: ^1.2.27 version: 1.2.27 + '@intlify/core': + specifier: ^11.2.8 + version: 11.2.8 '@modelcontextprotocol/sdk': specifier: ^1.27.1 version: 1.27.1 @@ -120,6 +123,9 @@ catalogs: '@xsai/utils-chat': specifier: 0.4.0-beta.13 version: 0.4.0-beta.13 + alien-signals: + specifier: ^3.1.2 + version: 3.1.2 async-mutex: specifier: 0.5.0 version: 0.5.0 @@ -920,6 +926,9 @@ importers: '@huggingface/transformers': specifier: ^3.8.1 version: 3.8.1 + '@intlify/core': + specifier: 'catalog:' + version: 11.2.8 '@modelcontextprotocol/sdk': specifier: 'catalog:' version: 1.27.1(@cfworker/json-schema@4.1.1)(zod@4.3.6) @@ -1028,6 +1037,9 @@ importers: '@xsai/utils-chat': specifier: 'catalog:' version: 0.4.0-beta.13 + alien-signals: + specifier: 'catalog:' + version: 3.1.2 animejs: specifier: ^4.3.6 version: 4.3.6 @@ -3142,7 +3154,7 @@ importers: version: 14.1.0(vue@3.5.29(typescript@5.9.3)) '@wxt-dev/module-vue': specifier: ^1.0.3 - version: 1.0.3(vite@7.3.1(@types/node@24.10.14)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))(wxt@0.20.18(@types/node@24.10.14)(canvas@3.2.1)(eslint@9.39.3(jiti@2.6.1))(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(rollup@4.59.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + version: 1.0.3(vite@8.0.0-beta.15(@types/node@24.10.14)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))(wxt@0.20.18(@types/node@24.10.14)(canvas@3.2.1)(eslint@9.39.3(jiti@2.6.1))(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(rollup@4.59.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) nanoid: specifier: ^5.1.6 version: 5.1.6 @@ -5780,6 +5792,10 @@ packages: resolution: {integrity: sha512-nBq6Y1tVkjIUsLsdOjDSJj4AsjvD0UG3zsg9Fyc+OivwlA/oMHSKooUy9tpKj0HqZ+NWFifweHavdljlBLTwdA==} engines: {node: '>= 16'} + '@intlify/core@11.2.8': + resolution: {integrity: sha512-su9kRlQAkG+SBP5cufTYmwPnqjur8etZVa2lnR80CgE5JqA0pXwGUF7W08dR/a6T2oDoYPh53/S8O0CGbfx1qg==} + engines: {node: '>= 16'} + '@intlify/message-compiler@11.2.8': resolution: {integrity: sha512-A5n33doOjmHsBtCN421386cG1tWp5rpOjOYPNsnpjIJbQ4POF0QY2ezhZR9kr0boKwaHjbOifvyQvHj2UTrDFQ==} engines: {node: '>= 16'} @@ -19281,6 +19297,11 @@ snapshots: '@intlify/message-compiler': 11.2.8 '@intlify/shared': 11.2.8 + '@intlify/core@11.2.8': + dependencies: + '@intlify/core-base': 11.2.8 + '@intlify/shared': 11.2.8 + '@intlify/message-compiler@11.2.8': dependencies: '@intlify/shared': 11.2.8 @@ -23310,9 +23331,9 @@ snapshots: '@types/filesystem': 0.0.36 '@types/har-format': 1.2.16 - '@wxt-dev/module-vue@1.0.3(vite@7.3.1(@types/node@24.10.14)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))(wxt@0.20.18(@types/node@24.10.14)(canvas@3.2.1)(eslint@9.39.3(jiti@2.6.1))(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(rollup@4.59.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + '@wxt-dev/module-vue@1.0.3(vite@8.0.0-beta.15(@types/node@24.10.14)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3))(wxt@0.20.18(@types/node@24.10.14)(canvas@3.2.1)(eslint@9.39.3(jiti@2.6.1))(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(rollup@4.59.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: - '@vitejs/plugin-vue': 6.0.4(vite@7.3.1(@types/node@24.10.14)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3)) + '@vitejs/plugin-vue': 6.0.4(vite@8.0.0-beta.15(@types/node@24.10.14)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.29(typescript@5.9.3)) wxt: 0.20.18(@types/node@24.10.14)(canvas@3.2.1)(eslint@9.39.3(jiti@2.6.1))(jiti@2.6.1)(less@4.5.1)(lightningcss@1.31.1)(rollup@4.59.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - vite diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 095e7dd54..b07805b25 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -26,7 +26,6 @@ patchedDependencies: mineflayer-pathfinder: patches/mineflayer-pathfinder.patch mineflayer@4.33.0: patches/mineflayer@4.33.0.patch srvx@0.9.8: patches/srvx@0.9.8.patch - catalog: '@capacitor/cli': ^8.1.0 '@capacitor/core': ^8.1.0 @@ -39,6 +38,7 @@ catalog: '@histoire/plugin-vue': 1.0.0-beta.1 '@iconify-json/logos': ^1.2.10 '@iconify-json/tabler': ^1.2.27 + '@intlify/core': ^11.2.8 '@modelcontextprotocol/sdk': ^1.27.1 '@moeru/eslint-config': 0.1.0-beta.15 '@moeru/eventa': 1.0.0-alpha.14 @@ -66,6 +66,7 @@ catalog: '@xsai/stream-text': ^0.4.3 '@xsai/tool': ^0.4.3 '@xsai/utils-chat': 0.4.0-beta.13 + alien-signals: ^3.1.2 async-mutex: 0.5.0 better-auth: ^1.4.19 builder-util-runtime: ^9.5.1