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
c1131251f9
commit
d15e90c26d
@@ -5,12 +5,12 @@
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
<span>Back to Goals</span>
|
||||
<span>{{ t('goalDetail.backToGoals') }}</span>
|
||||
</button>
|
||||
|
||||
<!-- Goal not found -->
|
||||
<div v-if="!goal" class="glass-card p-12 text-center">
|
||||
<p class="text-white/70">Goal not found.</p>
|
||||
<p class="text-white/70">{{ t('goalDetail.notFound') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Goal wizard -->
|
||||
@@ -23,7 +23,7 @@
|
||||
<!-- Progress bar -->
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<span class="text-sm text-white/60">Step {{ currentStepDisplay }} of {{ goal.steps.length }}</span>
|
||||
<span class="text-sm text-white/60">{{ t('goalDetail.stepOf', { current: currentStepDisplay, total: goal.steps.length }) }}</span>
|
||||
<span class="goal-status-badge" :class="statusBadgeClass">{{ statusLabel }}</span>
|
||||
</div>
|
||||
<div class="w-full h-2 bg-white/10 rounded-full overflow-hidden">
|
||||
@@ -40,10 +40,9 @@
|
||||
v-if="showSyncMessage"
|
||||
class="glass-card p-6 mb-6 border-l-4 border-orange-400"
|
||||
>
|
||||
<h3 class="text-lg font-semibold text-white mb-1">Sovereignty takes a little patience</h3>
|
||||
<h3 class="text-lg font-semibold text-white mb-1">{{ t('goalDetail.syncTitle') }}</h3>
|
||||
<p class="text-white/60 text-sm leading-relaxed">
|
||||
Your Bitcoin node is syncing the entire blockchain so you don't have to trust anyone else.
|
||||
This takes 2-3 days on first run. Meanwhile, you can explore your node, set up your identity, or back up your keys.
|
||||
{{ t('goalDetail.syncMessage') }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -94,28 +93,28 @@
|
||||
:disabled="isInstalling"
|
||||
class="glass-button glass-button-sm rounded-lg px-5 py-2 text-sm font-medium"
|
||||
>
|
||||
{{ isInstalling ? 'Installing...' : `Install ${step.title.replace('Install ', '')}` }}
|
||||
{{ isInstalling ? t('common.installing') : t('goalDetail.installApp', { name: step.title.replace('Install ', '') }) }}
|
||||
</button>
|
||||
<button
|
||||
v-else-if="step.action === 'configure'"
|
||||
@click="openConfigureStep(step)"
|
||||
class="glass-button glass-button-sm rounded-lg px-5 py-2 text-sm font-medium"
|
||||
>
|
||||
Open & Configure
|
||||
{{ t('goalDetail.openAndConfigure') }}
|
||||
</button>
|
||||
<button
|
||||
v-else-if="step.action === 'info'"
|
||||
@click="completeInfoStep(step)"
|
||||
class="glass-button glass-button-sm rounded-lg px-5 py-2 text-sm font-medium"
|
||||
>
|
||||
I've Done This
|
||||
{{ t('goalDetail.iveDoneThis') }}
|
||||
</button>
|
||||
<button
|
||||
v-else-if="isStepCompleted(step) || isAppInstalled(step.appId || '')"
|
||||
disabled
|
||||
class="glass-button glass-button-sm rounded-lg px-5 py-2 text-sm font-medium opacity-50"
|
||||
>
|
||||
Complete
|
||||
{{ t('goalDetail.complete') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -131,10 +130,10 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<h2 class="text-xl font-semibold text-white mb-2">All Set!</h2>
|
||||
<p class="text-white/60 mb-6">{{ goal.title }} is ready to go.</p>
|
||||
<h2 class="text-xl font-semibold text-white mb-2">{{ t('goalDetail.allSet') }}</h2>
|
||||
<p class="text-white/60 mb-6">{{ t('goalDetail.goalReady', { title: goal.title }) }}</p>
|
||||
<RouterLink to="/dashboard/apps" class="glass-button rounded-lg px-6 py-3 font-medium">
|
||||
View My Services
|
||||
{{ t('goalDetail.viewMyServices') }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
</template>
|
||||
@@ -144,11 +143,13 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRoute, useRouter, RouterLink } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { useGoalStore } from '@/stores/goals'
|
||||
import { getGoalById } from '@/data/goals'
|
||||
import type { GoalStep } from '@/types/goals'
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
@@ -193,9 +194,9 @@ const progressPercent = computed(() => {
|
||||
})
|
||||
|
||||
const statusLabel = computed(() => {
|
||||
if (overallStatus.value === 'completed') return 'Completed'
|
||||
if (overallStatus.value === 'in-progress') return 'In Progress'
|
||||
return 'Not Started'
|
||||
if (overallStatus.value === 'completed') return t('goalDetail.completed')
|
||||
if (overallStatus.value === 'in-progress') return t('goalDetail.inProgress')
|
||||
return t('goalDetail.notStarted')
|
||||
})
|
||||
|
||||
const statusBadgeClass = computed(() => {
|
||||
@@ -251,7 +252,7 @@ async function installApp(step: GoalStep) {
|
||||
await appStore.installPackage(step.appId, '', 'latest')
|
||||
goalStore.completeStep(goalId.value, step.id)
|
||||
} catch (err) {
|
||||
console.error('[GoalDetail] Install failed:', err)
|
||||
if (import.meta.env.DEV) console.error('[GoalDetail] Install failed:', err)
|
||||
} finally {
|
||||
isInstalling.value = false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user