diff --git a/app.vue b/app.vue index 112f8ef..9da0a7f 100644 --- a/app.vue +++ b/app.vue @@ -6,6 +6,7 @@ diff --git a/components/AppSidebar.vue b/components/AppSidebar.vue index 2262923..d06cb41 100644 --- a/components/AppSidebar.vue +++ b/components/AppSidebar.vue @@ -17,15 +17,15 @@ v-if="isPrereleaseBuild" class="mb-2 rounded-md bg-teal-500/10 border border-teal-500/20 px-3 py-1.5 text-center text-xs font-medium text-teal-400" > - PRE-RELEASE + {{ $t('sidebar.pre_release') }}
- {{ networkLabel }} + {{ $t(networkLabelKey) }}
@@ -42,9 +42,9 @@ v-if="updaterStore.isPrerelease" class="text-[10px] font-bold uppercase tracking-wider opacity-70" > - Pre-Release + {{ $t('sidebar.update_pre_release_tag') }} -
Update Available
+
{{ $t('sidebar.update_available') }}
v{{ updaterStore.version }}
@@ -60,7 +60,7 @@ : 'text-autonomi-muted hover:bg-autonomi-border/50 hover:text-autonomi-text'" > {{ item.icon }} - {{ item.label }} + {{ $t(item.labelKey) }} @@ -74,7 +74,7 @@ : 'text-autonomi-muted hover:bg-autonomi-border/50 hover:text-autonomi-text'" > - Settings + {{ $t('nav.settings') }} @@ -109,18 +109,18 @@ const isPrereleaseBuild = computed(() => /-(?:rc|beta|alpha)\./.test(currentVersion.value ?? ''), ) -const networkLabel = computed(() => { +const networkLabelKey = computed(() => { switch (settingsStore.devnetChainId) { - case arbitrumSepolia.id: return 'SEPOLIA TESTNET' - case ANVIL_CHAIN_ID: return 'DEVNET' + case arbitrumSepolia.id: return 'sidebar.network_sepolia' + case ANVIL_CHAIN_ID: return 'sidebar.network_devnet' default: return null // mainnet or unset — no badge } }) const mainNav = computed(() => [ - { path: '/', label: 'Nodes', icon: '⬡' }, - { path: '/files', label: 'Files', icon: '◫' }, - { path: '/wallet', label: 'Wallet', icon: '◎' }, + { path: '/', labelKey: 'nav.nodes', icon: '⬡' }, + { path: '/files', labelKey: 'nav.files', icon: '◫' }, + { path: '/wallet', labelKey: 'nav.wallet', icon: '◎' }, ]) function isActive(path: string) { diff --git a/composables/useLocale.ts b/composables/useLocale.ts new file mode 100644 index 0000000..95e8889 --- /dev/null +++ b/composables/useLocale.ts @@ -0,0 +1,80 @@ +import { ref } from 'vue' +import { locale as osLocaleApi } from '@tauri-apps/plugin-os' +import { useI18n } from 'vue-i18n' +import { useSettingsStore } from '~/stores/settings' + +export const SUPPORTED_LOCALES = ['en', 'ja'] as const +export type SupportedLocale = typeof SUPPORTED_LOCALES[number] +const DEFAULT_LOCALE: SupportedLocale = 'en' + +/** Each locale's name in its own script. Used in the Settings picker so the + * user sees "English" / "日本語" regardless of the currently-active UI locale. */ +export const NATIVE_LOCALE_NAMES: Record = { + en: 'English', + ja: '日本語', +} + +/** Module-scoped cache of the OS-resolved locale. Warmed on the first + * detectOsLocale() call so the Settings picker can render + * "System default: " synchronously after app init. */ +const osLocaleRef = ref(null) + +function isSupported(value: string): value is SupportedLocale { + return (SUPPORTED_LOCALES as readonly string[]).includes(value) +} + +function normalizeLocale(raw: string | null | undefined): SupportedLocale { + if (!raw) return DEFAULT_LOCALE + const base = raw.toLowerCase().split('-')[0] + return isSupported(base) ? base : DEFAULT_LOCALE +} + +export async function detectOsLocale(): Promise { + if (osLocaleRef.value !== null) return osLocaleRef.value + try { + osLocaleRef.value = normalizeLocale(await osLocaleApi()) + } catch { + osLocaleRef.value = DEFAULT_LOCALE + } + return osLocaleRef.value +} + +export function useLocale() { + const { locale, t } = useI18n() + const settings = useSettingsStore() + + /** + * Resolve and apply the locale to use at app start. Order: + * 1. Persisted user choice (`settings.i18nLocale`) if supported. + * 2. OS locale via tauri-plugin-os, region-stripped. + * 3. Fallback `en`. + * + * Always warms the OS-locale cache so the Settings picker can render its + * "System default: " label without an extra round-trip. + */ + async function init() { + const persisted = settings.i18nLocale + if (persisted && isSupported(persisted)) { + locale.value = persisted + // Warm the OS cache for the Settings picker label even when the + // persisted choice overrides it. + detectOsLocale().catch(() => {}) + return + } + locale.value = await detectOsLocale() + } + + /** Switch the active locale. `null` means "follow system" — the persisted + * choice is cleared and the live locale falls back to the OS reading. */ + async function setLocale(next: SupportedLocale | null) { + if (next === null) { + await settings.setI18nLocale(null) + locale.value = await detectOsLocale() + return + } + await settings.setI18nLocale(next) + locale.value = next + } + + return { locale, setLocale, init, t, osLocale: osLocaleRef } +} diff --git a/locales/en.json b/locales/en.json new file mode 100644 index 0000000..c6d4417 --- /dev/null +++ b/locales/en.json @@ -0,0 +1,34 @@ +{ + "settings": { + "language": { + "label": "Language", + "description": "UI display language", + "system_default": "System default: {name}", + "system_default_pending": "System default" + } + }, + "nav": { + "nodes": "Nodes", + "files": "Files", + "wallet": "Wallet", + "settings": "Settings" + }, + "header": { + "title": "Autonomi", + "active_transfers": "{count} active", + "connecting": "Connecting", + "connecting_tooltip": "Connecting to the Autonomi network", + "connected": "Network", + "connected_tooltip": "Connected to the Autonomi network", + "offline": "Offline · Retry", + "offline_tooltip": "Connection failed — click to retry", + "connect_wallet": "Connect Wallet" + }, + "sidebar": { + "pre_release": "PRE-RELEASE", + "update_available": "Update Available", + "update_pre_release_tag": "Pre-Release", + "network_devnet": "DEVNET", + "network_sepolia": "SEPOLIA TESTNET" + } +} diff --git a/locales/ja.json b/locales/ja.json new file mode 100644 index 0000000..0341e44 --- /dev/null +++ b/locales/ja.json @@ -0,0 +1,35 @@ +{ + "_translator_notes": "Machine-translated baseline. Community polish via PR welcome.", + "settings": { + "language": { + "label": "言語", + "description": "UI 表示言語", + "system_default": "システムのデフォルト: {name}", + "system_default_pending": "システムのデフォルト" + } + }, + "nav": { + "nodes": "ノード", + "files": "ファイル", + "wallet": "ウォレット", + "settings": "設定" + }, + "header": { + "title": "Autonomi", + "active_transfers": "{count} 件処理中", + "connecting": "接続中", + "connecting_tooltip": "Autonomi ネットワークに接続中", + "connected": "ネットワーク", + "connected_tooltip": "Autonomi ネットワークに接続済み", + "offline": "オフライン · 再試行", + "offline_tooltip": "接続に失敗しました — クリックして再試行", + "connect_wallet": "ウォレットを接続" + }, + "sidebar": { + "pre_release": "プレリリース", + "update_available": "アップデート利用可能", + "update_pre_release_tag": "プレリリース", + "network_devnet": "DEVNET", + "network_sepolia": "SEPOLIA テストネット" + } +} diff --git a/package-lock.json b/package-lock.json index ae4d91b..7816bdf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ant-gui", - "version": "0.6.7", + "version": "0.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ant-gui", - "version": "0.6.7", + "version": "0.7.0", "dependencies": { "@pinia/nuxt": "^0.5.0", "@reown/appkit": "^1.8.19", @@ -14,6 +14,7 @@ "@tauri-apps/api": "^2.0.0", "@tauri-apps/plugin-dialog": "^2.0.0", "@tauri-apps/plugin-opener": "^2.0.0", + "@tauri-apps/plugin-os": "^2.3.2", "@tauri-apps/plugin-process": "^2.0.0", "@tauri-apps/plugin-updater": "^2.0.0", "@wagmi/core": "^3.4.0", @@ -21,6 +22,7 @@ "pinia": "^2.2.0", "viem": "^2.47.1", "vue": "^3.5.0", + "vue-i18n": "^11.4.2", "vue-router": "^4.4.0" }, "devDependencies": { @@ -1063,6 +1065,67 @@ "node": ">=18" } }, + "node_modules/@intlify/core-base": { + "version": "11.4.2", + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.4.2.tgz", + "integrity": "sha512-7fpuCcVmeLv2T9qHsARqGvh8xt+sV2fH+Q+gMHFwB/rPXzo85DpbJFKn7dBH1L5p0c2cSh2DW+2h/64EKrISmA==", + "license": "MIT", + "dependencies": { + "@intlify/devtools-types": "11.4.2", + "@intlify/message-compiler": "11.4.2", + "@intlify/shared": "11.4.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/devtools-types": { + "version": "11.4.2", + "resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.4.2.tgz", + "integrity": "sha512-3u8EN1kB6EMSi96KXs5k7a8y2X2g4+h3X6iwVZU47cP4n+mTuq//WMjG588BzSp/2XQ/dTXo2BLUXX+XS+PNfA==", + "license": "MIT", + "dependencies": { + "@intlify/core-base": "11.4.2", + "@intlify/shared": "11.4.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/message-compiler": { + "version": "11.4.2", + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.4.2.tgz", + "integrity": "sha512-a6CDSGSMTGrg0BjD97x8TBYPf7qQMDlZipJ6UDfv/pd4OIym8TMlHu3MsH0bTNnRdAG2D6EFEykIgiQPqvtTkA==", + "license": "MIT", + "dependencies": { + "@intlify/shared": "11.4.2", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/shared": { + "version": "11.4.2", + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.4.2.tgz", + "integrity": "sha512-NzpHbguRCsOHDwxmlBa9qu/imc+/QWgsYUaK6FZeNC0wK8QfAbhqrktEp/haVzxU1aikH8IX4ytD+mfFEMi/9A==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, "node_modules/@ioredis/commands": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", @@ -5448,6 +5511,15 @@ "@tauri-apps/api": "^2.8.0" } }, + "node_modules/@tauri-apps/plugin-os": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-os/-/plugin-os-2.3.2.tgz", + "integrity": "sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.8.0" + } + }, "node_modules/@tauri-apps/plugin-process": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-process/-/plugin-process-2.3.1.tgz", @@ -14532,6 +14604,27 @@ "integrity": "sha512-RutnB7X8c5hjq39NceArgXg28WZtZpGc3+J16ljMiYnFhKvd8hITxSWQSQ5bvldxMDU6gG5mkxl1MTQLXckVSQ==", "license": "MIT" }, + "node_modules/vue-i18n": { + "version": "11.4.2", + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.4.2.tgz", + "integrity": "sha512-sADDeKXqAGsPX6tK3t3y2ZiMpbVWN12tG+MhTiJ06rVoh58eGtM4wFyw3uWGbVkXByVp9Ne/AP+nSSzI+J9OAQ==", + "license": "MIT", + "dependencies": { + "@intlify/core-base": "11.4.2", + "@intlify/devtools-types": "11.4.2", + "@intlify/shared": "11.4.2", + "@vue/devtools-api": "^6.5.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, "node_modules/vue-router": { "version": "4.6.4", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", diff --git a/package.json b/package.json index 27d5e17..edfcefe 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@tauri-apps/api": "^2.0.0", "@tauri-apps/plugin-dialog": "^2.0.0", "@tauri-apps/plugin-opener": "^2.0.0", + "@tauri-apps/plugin-os": "^2.3.2", "@tauri-apps/plugin-process": "^2.0.0", "@tauri-apps/plugin-updater": "^2.0.0", "@wagmi/core": "^3.4.0", @@ -27,6 +28,7 @@ "pinia": "^2.2.0", "viem": "^2.47.1", "vue": "^3.5.0", + "vue-i18n": "^11.4.2", "vue-router": "^4.4.0" }, "devDependencies": { diff --git a/pages/settings.vue b/pages/settings.vue index 706fffd..4038985 100644 --- a/pages/settings.vue +++ b/pages/settings.vue @@ -107,6 +107,23 @@ + +
+
+

{{ $t('settings.language.label') }}

+

{{ $t('settings.language.description') }}

+
+ +
+