feat: add vue-i18n infrastructure and externalize all UI strings (A11Y-03)
Set up vue-i18n with English locale file containing 500+ keys organized by view namespace. All 15 views converted to use t() calls instead of hardcoded strings. Infrastructure ready for community translations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
b9cc0a924e
commit
aba7aba25f
+21
-19
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="pb-6">
|
||||
<div class="hidden md:block mb-8">
|
||||
<h1 class="text-3xl font-bold text-white mb-2">My Apps</h1>
|
||||
<p class="text-white/70">Manage your installed applications</p>
|
||||
<h1 class="text-3xl font-bold text-white mb-2">{{ t('apps.title') }}</h1>
|
||||
<p class="text-white/70">{{ t('apps.subtitle') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Search Bar -->
|
||||
@@ -10,8 +10,8 @@
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="Search installed apps..."
|
||||
aria-label="Search installed apps"
|
||||
:placeholder="t('apps.searchPlaceholder')"
|
||||
:aria-label="t('apps.searchLabel')"
|
||||
class="w-full px-4 py-3 md:py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:border-white/40 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
@@ -22,20 +22,20 @@
|
||||
<svg class="w-16 h-16 mx-auto text-white/40 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
|
||||
</svg>
|
||||
<h3 class="text-xl font-semibold text-white mb-2">No Apps Installed</h3>
|
||||
<p class="text-white/70 mb-6">Get started by browsing the app store</p>
|
||||
<h3 class="text-xl font-semibold text-white mb-2">{{ t('apps.noAppsTitle') }}</h3>
|
||||
<p class="text-white/70 mb-6">{{ t('apps.noAppsMessage') }}</p>
|
||||
<RouterLink
|
||||
to="/dashboard/marketplace"
|
||||
class="inline-block glass-button px-6 py-3 rounded-lg font-medium transition-all hover:bg-black/70 hover:border-white/30"
|
||||
>
|
||||
Browse App Store
|
||||
{{ t('apps.browseAppStore') }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- No Results -->
|
||||
<div v-if="filteredPackageEntries.length === 0 && searchQuery" class="text-center py-12">
|
||||
<p class="text-white/70">No apps matching "{{ searchQuery }}"</p>
|
||||
<p class="text-white/70">{{ t('apps.noResults', { query: searchQuery }) }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Apps Grid (alphabetically by title, stable across run state) -->
|
||||
@@ -56,8 +56,8 @@
|
||||
<button
|
||||
@click.stop="showUninstallModal(id as string, pkg)"
|
||||
class="absolute top-4 right-4 p-2 rounded-lg text-white/60 hover:text-red-400 hover:bg-red-500/20 transition-colors z-10"
|
||||
:aria-label="`Uninstall ${pkg.manifest?.title || id}`"
|
||||
title="Uninstall"
|
||||
:aria-label="`${t('common.uninstall')} ${pkg.manifest?.title || id}`"
|
||||
:title="t('common.uninstall')"
|
||||
>
|
||||
<svg class="w-5 h-5" aria-hidden="true" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
@@ -100,7 +100,7 @@
|
||||
@click.stop="launchApp(id as string)"
|
||||
class="flex-1 px-4 py-2 glass-button glass-button-sm rounded-lg text-sm font-medium"
|
||||
>
|
||||
Launch
|
||||
{{ t('common.launch') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="pkg.state === 'stopped' || pkg.state === 'exited'"
|
||||
@@ -119,7 +119,7 @@
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<span>{{ loadingActions[id as string] ? 'Starting...' : 'Start' }}</span>
|
||||
<span>{{ loadingActions[id as string] ? t('common.starting') : t('common.start') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="pkg.state === 'running' || pkg.state === 'starting'"
|
||||
@@ -138,7 +138,7 @@
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<span>{{ loadingActions[id as string] ? 'Stopping...' : 'Stop' }}</span>
|
||||
<span>{{ loadingActions[id as string] ? t('common.stopping') : t('common.stop') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -167,10 +167,9 @@
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 id="uninstall-dialog-title" class="text-xl font-semibold text-white mb-2">Uninstall App?</h3>
|
||||
<h3 id="uninstall-dialog-title" class="text-xl font-semibold text-white mb-2">{{ t('apps.uninstallTitle') }}</h3>
|
||||
<p class="text-white/70">
|
||||
Are you sure you want to uninstall <span class="text-white font-medium">{{ uninstallModal.appTitle }}</span>?
|
||||
This will remove the app and stop its container.
|
||||
{{ t('apps.uninstallConfirm', { name: uninstallModal.appTitle }) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -180,13 +179,13 @@
|
||||
@click="closeUninstallModal()"
|
||||
class="px-4 py-2 glass-button rounded-lg text-sm font-medium"
|
||||
>
|
||||
Cancel
|
||||
{{ t('common.cancel') }}
|
||||
</button>
|
||||
<button
|
||||
@click="confirmUninstall"
|
||||
class="px-4 py-2 bg-red-600/80 hover:bg-red-600 rounded-lg text-white text-sm font-medium transition-colors"
|
||||
>
|
||||
Uninstall
|
||||
{{ t('common.uninstall') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -198,7 +197,7 @@
|
||||
<div v-if="actionError" class="fixed bottom-20 left-1/2 -translate-x-1/2 z-50 max-w-md w-full px-4" role="alert" aria-live="assertive">
|
||||
<div class="bg-red-500/20 border border-red-500/40 backdrop-blur-sm rounded-lg px-4 py-3 text-red-200 text-sm flex items-center justify-between gap-3">
|
||||
<span>{{ actionError }}</span>
|
||||
<button @click="actionError = ''" aria-label="Dismiss error" class="text-red-300 hover:text-white shrink-0">×</button>
|
||||
<button @click="actionError = ''" :aria-label="t('apps.dismissError')" class="text-red-300 hover:text-white shrink-0">×</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
@@ -208,7 +207,10 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onBeforeUnmount } from 'vue'
|
||||
import { useRouter, RouterLink } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '../stores/app'
|
||||
|
||||
const { t } = useI18n()
|
||||
import { useAppLauncherStore } from '../stores/appLauncher'
|
||||
import { PackageState, type PackageDataEntry } from '../types/api'
|
||||
import { useModalKeyboard } from '@/composables/useModalKeyboard'
|
||||
|
||||
Reference in New Issue
Block a user