Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

<script setup lang="ts">
import { invoke } from '@tauri-apps/api/core'
import { useLocale } from '~/composables/useLocale'
import { useWallet } from '~/composables/useWallet'
import { useTheme } from '~/composables/useTheme'
import { useSettingsStore } from '~/stores/settings'
Expand All @@ -31,8 +32,14 @@ const nodesStore = useNodesStore()
const filesStore = useFilesStore()
const connectionStore = useConnectionStore()

const { init: initLocale } = useLocale()

onMounted(async () => {
// Config first — useLocale.init() reads the persisted choice from
// settingsStore.i18nLocale, so the store must be hydrated before we resolve.
await settingsStore.loadConfig()
await initLocale()

await settingsStore.loadDevnetManifest()
nodesStore.init()
filesStore.loadHistory()
Expand Down
32 changes: 16 additions & 16 deletions components/AppHeader.vue
Original file line number Diff line number Diff line change
@@ -1,43 +1,43 @@
<template>
<header class="flex h-12 items-center justify-between border-b border-autonomi-border bg-autonomi-surface px-4">
<!-- Page title -->
<h1 class="text-sm font-medium text-autonomi-text">{{ pageTitle }}</h1>
<h1 class="text-sm font-medium text-autonomi-text">{{ $t(pageTitleKey) }}</h1>

<!-- Right side: status indicators + wallet -->
<div class="flex items-center gap-4">
<!-- Active transfers indicator -->
<div v-if="filesStore.hasActiveTransfers" class="flex items-center gap-2 text-xs">
<span class="text-autonomi-blue">↑↓</span>
<span class="text-autonomi-muted">
{{ filesStore.pinnedFiles.length }} active
{{ $t('header.active_transfers', { count: filesStore.pinnedFiles.length }) }}
</span>
</div>

<!-- Network connection indicator -->
<div
v-if="connectionStore.isConnecting"
class="flex items-center gap-2 rounded-md border border-autonomi-border px-2.5 py-1 text-xs text-autonomi-muted"
title="Connecting to the Autonomi network"
:title="$t('header.connecting_tooltip')"
>
<div class="h-2.5 w-2.5 animate-spin rounded-full border-2 border-yellow-500 border-t-transparent" />
<span>Connecting</span>
<span>{{ $t('header.connecting') }}</span>
</div>
<div
v-else-if="connectionStore.isConnected"
class="flex items-center gap-2 rounded-md border border-autonomi-border px-2.5 py-1 text-xs text-autonomi-text"
title="Connected to the Autonomi network"
:title="$t('header.connected_tooltip')"
>
<span class="text-autonomi-success">●</span>
<span>Network</span>
<span>{{ $t('header.connected') }}</span>
</div>
<button
v-else-if="connectionStore.hasFailed"
class="flex items-center gap-2 rounded-md border border-red-500/30 bg-red-500/5 px-2.5 py-1 text-xs text-red-400 hover:bg-red-500/10"
title="Connection failed — click to retry"
:title="$t('header.offline_tooltip')"
@click="connectionStore.retry()"
>
<span>●</span>
<span>Offline · Retry</span>
<span>{{ $t('header.offline') }}</span>
</button>

<!-- Indelible indicator (replaces wallet when connected) -->
Expand All @@ -64,7 +64,7 @@
class="rounded-md bg-autonomi-blue px-2.5 py-1 text-xs font-medium text-white hover:opacity-90"
@click="openModal"
>
Connect Wallet
{{ $t('header.connect_wallet') }}
</button>
</template>
</div>
Expand Down Expand Up @@ -98,14 +98,14 @@ function openModal() {
}
}

const pageTitle = computed(() => {
const titles: Record<string, string> = {
'/': 'Nodes',
'/files': 'Files',
'/wallet': 'Wallet',
'/settings': 'Settings',
const pageTitleKey = computed(() => {
const keys: Record<string, string> = {
'/': 'nav.nodes',
'/files': 'nav.files',
'/wallet': 'nav.wallet',
'/settings': 'nav.settings',
}
return titles[route.path] ?? 'Autonomi'
return keys[route.path] ?? 'header.title'
})

</script>
26 changes: 13 additions & 13 deletions components/AppSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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') }}
</div>

<!-- Network mode indicator -->
<div
v-if="settingsStore.devnetActive && networkLabel"
v-if="settingsStore.devnetActive && networkLabelKey"
class="mb-2 rounded-md bg-amber-500/10 border border-amber-500/20 px-3 py-1.5 text-center text-xs font-medium text-amber-400"
>
{{ networkLabel }}
{{ $t(networkLabelKey) }}
</div>

<!-- Update banner -->
Expand All @@ -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') }}
</div>
<div class="text-xs font-bold uppercase tracking-wide">Update Available</div>
<div class="text-xs font-bold uppercase tracking-wide">{{ $t('sidebar.update_available') }}</div>
<div class="truncate text-xs font-medium opacity-80">v{{ updaterStore.version }}</div>
</div>
<span v-if="updaterStore.installing" class="h-4 w-4 animate-spin rounded-full border-2 border-gray-900/30 border-t-gray-900" />
Expand All @@ -60,7 +60,7 @@
: 'text-autonomi-muted hover:bg-autonomi-border/50 hover:text-autonomi-text'"
>
<span class="text-base">{{ item.icon }}</span>
<span>{{ item.label }}</span>
<span>{{ $t(item.labelKey) }}</span>
</NuxtLink>
</nav>

Expand All @@ -74,7 +74,7 @@
: 'text-autonomi-muted hover:bg-autonomi-border/50 hover:text-autonomi-text'"
>
<span class="text-base">⚙</span>
<span>Settings</span>
<span>{{ $t('nav.settings') }}</span>
</NuxtLink>
</div>
</aside>
Expand Down Expand Up @@ -109,18 +109,18 @@ const isPrereleaseBuild = computed(() =>
/-(?:rc|beta|alpha)\./.test(currentVersion.value ?? ''),
)

const networkLabel = computed<string | null>(() => {
const networkLabelKey = computed<string | null>(() => {
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) {
Expand Down
80 changes: 80 additions & 0 deletions composables/useLocale.ts
Original file line number Diff line number Diff line change
@@ -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<SupportedLocale, string> = {
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: <name>" synchronously after app init. */
const osLocaleRef = ref<SupportedLocale | null>(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<SupportedLocale> {
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: <name>" 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 }
}
34 changes: 34 additions & 0 deletions locales/en.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
35 changes: 35 additions & 0 deletions locales/ja.json
Original file line number Diff line number Diff line change
@@ -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 テストネット"
}
}
Loading