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
@@ -76,8 +76,8 @@
|
||||
|
||||
<div class="hidden md:flex mb-8 items-start justify-between">
|
||||
<div>
|
||||
<h1 class="text-4xl font-bold text-white mb-2">App Store</h1>
|
||||
<p class="text-white/70">Discover and install apps for your new sovereign life</p>
|
||||
<h1 class="text-4xl font-bold text-white mb-2">{{ t('marketplace.title') }}</h1>
|
||||
<p class="text-white/70">{{ t('marketplace.subtitle') }}</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -95,7 +95,7 @@
|
||||
: 'text-white/60 hover:text-white/80 border border-transparent'
|
||||
]"
|
||||
>
|
||||
Curated
|
||||
{{ t('marketplace.curatedTab') }}
|
||||
</button>
|
||||
<button
|
||||
@click="marketplaceSource = 'community'; loadNostrMarketplace()"
|
||||
@@ -108,7 +108,7 @@
|
||||
: 'text-white/60 hover:text-white/80 border border-transparent'
|
||||
]"
|
||||
>
|
||||
Community
|
||||
{{ t('marketplace.communityTab') }}
|
||||
<span v-if="nostrApps.length > 0" class="ml-1 text-xs px-1.5 py-0.5 rounded-full bg-white/10">{{ nostrApps.length }}</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -133,8 +133,8 @@
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="Search apps..."
|
||||
aria-label="Search apps"
|
||||
:placeholder="t('marketplace.searchPlaceholder')"
|
||||
:aria-label="t('marketplace.searchApps')"
|
||||
class="flex-shrink-0 w-64 px-4 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>
|
||||
@@ -159,8 +159,8 @@
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="text"
|
||||
placeholder="Search apps..."
|
||||
aria-label="Search apps"
|
||||
:placeholder="t('marketplace.searchPlaceholder')"
|
||||
:aria-label="t('marketplace.searchApps')"
|
||||
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>
|
||||
@@ -227,7 +227,7 @@
|
||||
disabled
|
||||
class="flex-1 px-4 py-2 bg-white/20 rounded-lg text-white/60 text-sm font-medium cursor-not-allowed"
|
||||
>
|
||||
Already Installed
|
||||
{{ t('marketplace.alreadyInstalled') }}
|
||||
</button>
|
||||
<button
|
||||
v-else-if="app.source === 'local' || app.dockerImage"
|
||||
@@ -241,16 +241,16 @@
|
||||
<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>
|
||||
{{ installingApps.get(app.id)?.message || 'Installing...' }}
|
||||
{{ installingApps.get(app.id)?.message || t('common.installing') }}
|
||||
</span>
|
||||
<span v-else>Install</span>
|
||||
<span v-else>{{ t('common.install') }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
disabled
|
||||
class="flex-1 px-4 py-2 bg-white/10 rounded-lg text-white/40 text-sm font-medium cursor-not-allowed"
|
||||
>
|
||||
Not Available
|
||||
{{ t('common.notAvailable') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -263,14 +263,14 @@
|
||||
<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>
|
||||
<p class="text-white/70">{{ marketplaceSource === 'community' ? 'Querying Nostr relays for apps...' : 'Loading apps...' }}</p>
|
||||
<p class="text-white/70">{{ marketplaceSource === 'community' ? t('marketplace.queryingRelays') : t('common.loading') }}</p>
|
||||
</div>
|
||||
<div v-else-if="nostrError && marketplaceSource === 'community'" class="flex flex-col items-center gap-4">
|
||||
<p class="text-white/70">No community apps discovered yet.</p>
|
||||
<p class="text-white/70">{{ t('marketplace.noCommunityApps') }}</p>
|
||||
<p class="text-white/40 text-sm">{{ nostrError }}</p>
|
||||
<button @click="nostrApps = []; loadNostrMarketplace()" class="px-4 py-2 glass-button rounded-lg text-sm">Retry</button>
|
||||
<button @click="nostrApps = []; loadNostrMarketplace()" class="px-4 py-2 glass-button rounded-lg text-sm">{{ t('common.retry') }}</button>
|
||||
</div>
|
||||
<p v-else class="text-white/70">No apps found in {{ categories.find(c => c.id === selectedCategory)?.name }}{{ searchQuery ? ` matching "${searchQuery}"` : '' }}</p>
|
||||
<p v-else class="text-white/70">{{ searchQuery && selectedCategory !== 'all' ? t('marketplace.noResults', { category: categories.find(c => c.id === selectedCategory)?.name, query: searchQuery }) : searchQuery ? t('marketplace.noResultsSearch', { query: searchQuery }) : t('marketplace.noResultsCategory', { category: categories.find(c => c.id === selectedCategory)?.name }) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Scrollable Apps Section -->
|
||||
@@ -298,7 +298,7 @@
|
||||
<div ref="filterModalRef" class="glass-card p-6 w-full rounded-t-3xl max-h-[80vh] overflow-y-auto">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-2xl font-bold text-white">Filter by Category</h2>
|
||||
<h2 class="text-2xl font-bold text-white">{{ t('marketplace.filterByCategory') }}</h2>
|
||||
<button
|
||||
@click="closeFilterModal()"
|
||||
class="text-white/60 hover:text-white transition-colors"
|
||||
@@ -372,6 +372,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import { useMarketplaceApp, type MarketplaceAppInfo } from '@/composables/useMarketplaceApp'
|
||||
@@ -381,22 +382,23 @@ type MarketplaceApp = Partial<MarketplaceAppInfo> & { id: string; trustScore?: n
|
||||
|
||||
const router = useRouter()
|
||||
const store = useAppStore()
|
||||
const { t } = useI18n()
|
||||
const { setCurrentApp } = useMarketplaceApp()
|
||||
|
||||
// Category state
|
||||
const selectedCategory = ref('all')
|
||||
|
||||
const categories = [
|
||||
{ id: 'all', name: 'All' },
|
||||
{ id: 'community', name: 'Community' },
|
||||
{ id: 'commerce', name: 'Commerce' },
|
||||
{ id: 'money', name: 'Money' },
|
||||
{ id: 'data', name: 'Data' },
|
||||
{ id: 'home', name: 'Home' },
|
||||
{ id: 'car', name: 'Auto' },
|
||||
{ id: 'networking', name: 'Networking' },
|
||||
{ id: 'other', name: 'Other' }
|
||||
]
|
||||
const categories = computed(() => [
|
||||
{ id: 'all', name: t('marketplace.all') },
|
||||
{ id: 'community', name: t('marketplace.community') },
|
||||
{ id: 'commerce', name: t('marketplace.commerce') },
|
||||
{ id: 'money', name: t('marketplace.money') },
|
||||
{ id: 'data', name: t('marketplace.data') },
|
||||
{ id: 'home', name: t('marketplace.homeCategory') },
|
||||
{ id: 'car', name: t('marketplace.auto') },
|
||||
{ id: 'networking', name: t('marketplace.networking') },
|
||||
{ id: 'other', name: t('marketplace.other') }
|
||||
])
|
||||
|
||||
// Installation state - support multiple concurrent installations
|
||||
interface InstallProgress {
|
||||
@@ -425,7 +427,7 @@ watch(() => store.packages, (packages) => {
|
||||
...current,
|
||||
status: 'downloading',
|
||||
progress: Math.min(pct, 95),
|
||||
message: progress.size > 0 ? `Downloading: ${downloadedMB} / ${totalMB} MB (${pct}%)` : 'Downloading...',
|
||||
message: progress.size > 0 ? `Downloading: ${downloadedMB} / ${totalMB} MB (${pct}%)` : t('marketplace.downloading'),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -606,7 +608,7 @@ const allApps = computed(() => {
|
||||
// Only show categories that have at least one app
|
||||
const categoriesWithApps = computed(() => {
|
||||
const apps = allApps.value
|
||||
return categories.filter(cat => {
|
||||
return categories.value.filter(cat => {
|
||||
if (cat.id === 'all') return apps.length > 0
|
||||
return apps.some(app => app.category === cat.id)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user