Added frontend localization

This commit is contained in:
2026-04-05 22:46:52 +03:00
parent 4902889401
commit 6872563c62
14 changed files with 675 additions and 170 deletions
+11 -2
View File
@@ -45,7 +45,7 @@ func NewServer(cfg config.Config) *Server {
}
swaggerHandler := ginSwagger.WrapHandler(swaggerFiles.Handler)
router.GET(openAPIEndpoint, func(c *gin.Context) {
serveSwaggerIndex := func(c *gin.Context) {
recorder := httptest.NewRecorder()
proxyCtx, _ := gin.CreateTestContext(recorder)
proxyCtx.Request = c.Request.Clone(c.Request.Context())
@@ -69,8 +69,17 @@ func NewServer(cfg config.Config) *Server {
c.Status(recorder.Code)
_, _ = c.Writer.WriteString(body)
}
router.GET(openAPIEndpoint, serveSwaggerIndex)
router.GET(openAPIEndpoint+"/*any", func(c *gin.Context) {
if c.Param("any") == "/" {
serveSwaggerIndex(c)
return
}
swaggerHandler(c)
})
router.GET(openAPIEndpoint+"/*any", swaggerHandler)
}
apiV1 := router.Group("/api/v1")
+26 -23
View File
@@ -1,21 +1,24 @@
<script setup lang="ts">
import { BarChart3, Calendar, PieChart, TrendingUp } from 'lucide-vue-next';
import { useI18n } from '@/i18n';
const { t } = useI18n();
const categoryData = [
{ name: 'Food', value: 850, color: '#f97316' },
{ name: 'Transport', value: 420, color: '#ef4444' },
{ name: 'Shopping', value: 680, color: '#8b5cf6' },
{ name: 'Bills', value: 1250, color: '#3b82f6' },
{ name: 'Others', value: 320, color: '#6366f1' },
{ nameKey: 'finance.analytics.food', value: 850, color: '#f97316' },
{ nameKey: 'finance.analytics.transport', value: 420, color: '#ef4444' },
{ nameKey: 'finance.analytics.shopping', value: 680, color: '#8b5cf6' },
{ nameKey: 'finance.analytics.bills', value: 1250, color: '#3b82f6' },
{ nameKey: 'finance.analytics.others', value: 320, color: '#6366f1' },
];
const monthlyData = [
{ month: 'Oct', income: 7200, expenses: 4800 },
{ month: 'Nov', income: 8100, expenses: 5200 },
{ month: 'Dec', income: 7800, expenses: 4500 },
{ month: 'Jan', income: 8400, expenses: 5100 },
{ month: 'Feb', income: 7900, expenses: 4900 },
{ month: 'Mar', income: 8240, expenses: 3120 },
{ monthKey: 'finance.analytics.month.oct', income: 7200, expenses: 4800 },
{ monthKey: 'finance.analytics.month.nov', income: 8100, expenses: 5200 },
{ monthKey: 'finance.analytics.month.dec', income: 7800, expenses: 4500 },
{ monthKey: 'finance.analytics.month.jan', income: 8400, expenses: 5100 },
{ monthKey: 'finance.analytics.month.feb', income: 7900, expenses: 4900 },
{ monthKey: 'finance.analytics.month.mar', income: 8240, expenses: 3120 },
];
const totalExpenses = categoryData.reduce((sum, item) => sum + item.value, 0);
@@ -39,18 +42,18 @@ const donutGradient = (() => {
<div class="mb-3 flex h-9 w-9 items-center justify-center rounded-[11px] bg-emerald-500/10">
<TrendingUp class="h-[18px] w-[18px] text-emerald-400" :stroke-width="2" />
</div>
<p class="mb-1 text-[12px] font-medium text-zinc-500">Avg. Income</p>
<p class="mb-1 text-[12px] font-medium text-zinc-500">{{ t('finance.analytics.avgIncome') }}</p>
<p class="text-[20px] font-bold text-white">$8,107</p>
<p class="mt-1 text-[11px] font-medium text-emerald-400">+5.2% growth</p>
<p class="mt-1 text-[11px] font-medium text-emerald-400">{{ t('finance.analytics.growth') }}</p>
</div>
<div class="rounded-[16px] border border-white/[0.06] bg-[#16161F] p-4">
<div class="mb-3 flex h-9 w-9 items-center justify-center rounded-[11px] bg-rose-500/10">
<BarChart3 class="h-[18px] w-[18px] text-rose-400" :stroke-width="2" />
</div>
<p class="mb-1 text-[12px] font-medium text-zinc-500">Avg. Expenses</p>
<p class="mb-1 text-[12px] font-medium text-zinc-500">{{ t('finance.analytics.avgExpenses') }}</p>
<p class="text-[20px] font-bold text-white">$4,603</p>
<p class="mt-1 text-[11px] font-medium text-rose-400">-12% decrease</p>
<p class="mt-1 text-[11px] font-medium text-rose-400">{{ t('finance.analytics.decrease') }}</p>
</div>
</div>
@@ -59,7 +62,7 @@ const donutGradient = (() => {
<div class="flex h-9 w-9 items-center justify-center rounded-[11px] bg-purple-500/10">
<PieChart class="h-[18px] w-[18px] text-purple-400" :stroke-width="2" />
</div>
<h3 class="text-[15px] font-semibold text-white">Spending by Category</h3>
<h3 class="text-[15px] font-semibold text-white">{{ t('finance.analytics.byCategory') }}</h3>
</div>
<div class="mb-4 flex h-48 items-center justify-center">
@@ -69,11 +72,11 @@ const donutGradient = (() => {
</div>
<div class="space-y-2.5">
<div v-for="category in categoryData" :key="category.name" class="flex items-center gap-3">
<div v-for="category in categoryData" :key="category.nameKey" class="flex items-center gap-3">
<div class="h-3 w-3 flex-shrink-0 rounded-full" :style="{ backgroundColor: category.color }" />
<div class="min-w-0 flex-1">
<div class="mb-1 flex items-center justify-between">
<span class="text-[13px] font-medium text-white">{{ category.name }}</span>
<span class="text-[13px] font-medium text-white">{{ t(category.nameKey) }}</span>
<span class="text-[12px] font-semibold text-zinc-400">${{ category.value }}</span>
</div>
<div class="h-1.5 overflow-hidden rounded-full bg-white/[0.05]">
@@ -98,21 +101,21 @@ const donutGradient = (() => {
<div class="flex h-9 w-9 items-center justify-center rounded-[11px] bg-blue-500/10">
<Calendar class="h-[18px] w-[18px] text-blue-400" :stroke-width="2" />
</div>
<h3 class="text-[15px] font-semibold text-white">Last 6 Months</h3>
<h3 class="text-[15px] font-semibold text-white">{{ t('finance.analytics.lastMonths') }}</h3>
</div>
<div class="space-y-3">
<div v-for="month in monthlyData" :key="month.month" class="flex items-center gap-3 rounded-[12px] bg-white/[0.02] p-3">
<div v-for="month in monthlyData" :key="month.monthKey" class="flex items-center gap-3 rounded-[12px] bg-white/[0.02] p-3">
<div class="w-12 text-center">
<p class="text-[13px] font-semibold text-white">{{ month.month }}</p>
<p class="text-[13px] font-semibold text-white">{{ t(month.monthKey) }}</p>
</div>
<div class="flex-1">
<div class="mb-1.5 flex items-center justify-between">
<span class="text-[11px] text-zinc-500">Income</span>
<span class="text-[11px] text-zinc-500">{{ t('finance.analytics.month.income') }}</span>
<span class="text-[12px] font-semibold text-emerald-400">${{ month.income }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-[11px] text-zinc-500">Expenses</span>
<span class="text-[11px] text-zinc-500">{{ t('finance.analytics.month.expenses') }}</span>
<span class="text-[12px] font-semibold text-rose-400">${{ month.expenses }}</span>
</div>
</div>
+6 -3
View File
@@ -1,5 +1,8 @@
<script setup lang="ts">
import { ArrowDownRight, ArrowUpRight, TrendingUp } from 'lucide-vue-next';
import { useI18n } from '../i18n';
const { t } = useI18n();
</script>
<template>
@@ -10,7 +13,7 @@ import { ArrowDownRight, ArrowUpRight, TrendingUp } from 'lucide-vue-next';
<div class="relative z-10">
<div class="mb-6 flex items-start justify-between">
<div>
<p class="mb-2 text-[13px] font-medium text-purple-100/70">Total Balance</p>
<p class="mb-2 text-[13px] font-medium text-purple-100/70">{{ t('home.balance.total') }}</p>
<h2 class="text-[40px] font-bold leading-none tracking-tight text-white">$24,850</h2>
</div>
<div class="flex items-center gap-1.5 rounded-full border border-white/10 bg-white/15 px-3 py-1.5 backdrop-blur-sm">
@@ -25,7 +28,7 @@ import { ArrowDownRight, ArrowUpRight, TrendingUp } from 'lucide-vue-next';
<div class="flex h-6 w-6 items-center justify-center rounded-full bg-emerald-400/20">
<ArrowUpRight class="h-3.5 w-3.5 text-emerald-400" :stroke-width="2.5" />
</div>
<p class="text-[12px] font-medium text-white/70">Income</p>
<p class="text-[12px] font-medium text-white/70">{{ t('home.balance.income') }}</p>
</div>
<p class="text-[20px] font-bold text-white">$8,240</p>
</div>
@@ -34,7 +37,7 @@ import { ArrowDownRight, ArrowUpRight, TrendingUp } from 'lucide-vue-next';
<div class="flex h-6 w-6 items-center justify-center rounded-full bg-rose-400/20">
<ArrowDownRight class="h-3.5 w-3.5 text-rose-400" :stroke-width="2.5" />
</div>
<p class="text-[12px] font-medium text-white/70">Expenses</p>
<p class="text-[12px] font-medium text-white/70">{{ t('home.balance.expenses') }}</p>
</div>
<p class="text-[20px] font-bold text-white">$3,120</p>
</div>
+24 -13
View File
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { computed } from 'vue';
import {
Briefcase,
Car,
@@ -12,18 +13,21 @@ import {
Utensils,
Zap,
} from 'lucide-vue-next';
import { useI18n } from '../i18n';
const { t } = useI18n();
const categories = [
{ name: 'Food & Dining', icon: Utensils, color: 'orange', spent: 850, budget: 1200 },
{ name: 'Shopping', icon: ShoppingBag, color: 'emerald', spent: 680, budget: 800 },
{ name: 'Transport', icon: Car, color: 'red', spent: 420, budget: 500 },
{ name: 'Housing', icon: Home, color: 'indigo', spent: 1250, budget: 1250 },
{ name: 'Entertainment', icon: Film, color: 'purple', spent: 215, budget: 300 },
{ name: 'Coffee', icon: Coffee, color: 'amber', spent: 145, budget: 150 },
{ name: 'Bills & Utilities', icon: Zap, color: 'yellow', spent: 380, budget: 400 },
{ name: 'Work', icon: Briefcase, color: 'blue', spent: 120, budget: 200 },
{ name: 'Gifts & Donations', icon: Heart, color: 'pink', spent: 95, budget: 150 },
{ name: 'Others', icon: Gift, color: 'zinc', spent: 65, budget: 100 },
{ nameKey: 'finance.categories.foodDining', icon: Utensils, color: 'orange', spent: 850, budget: 1200 },
{ nameKey: 'finance.categories.shopping', icon: ShoppingBag, color: 'emerald', spent: 680, budget: 800 },
{ nameKey: 'finance.categories.transport', icon: Car, color: 'red', spent: 420, budget: 500 },
{ nameKey: 'finance.categories.housing', icon: Home, color: 'indigo', spent: 1250, budget: 1250 },
{ nameKey: 'finance.categories.entertainment', icon: Film, color: 'purple', spent: 215, budget: 300 },
{ nameKey: 'finance.categories.coffee', icon: Coffee, color: 'amber', spent: 145, budget: 150 },
{ nameKey: 'finance.categories.billsUtilities', icon: Zap, color: 'yellow', spent: 380, budget: 400 },
{ nameKey: 'finance.categories.work', icon: Briefcase, color: 'blue', spent: 120, budget: 200 },
{ nameKey: 'finance.categories.giftsDonations', icon: Heart, color: 'pink', spent: 95, budget: 150 },
{ nameKey: 'finance.categories.others', icon: Gift, color: 'zinc', spent: 65, budget: 100 },
] as const;
const colorMap = {
@@ -38,13 +42,20 @@ const colorMap = {
pink: { bg: 'bg-pink-500/10', text: 'text-pink-400', bar: 'bg-pink-500' },
zinc: { bg: 'bg-zinc-500/10', text: 'text-zinc-400', bar: 'bg-zinc-500' },
} as const;
const localizedCategories = computed(() =>
categories.map((category) => ({
...category,
name: t(category.nameKey),
})),
);
</script>
<template>
<div class="space-y-2.5">
<div
v-for="category in categories"
:key="category.name"
v-for="category in localizedCategories"
:key="category.nameKey"
class="group cursor-pointer rounded-[16px] border border-white/[0.06] bg-[#16161F] p-4 transition-all hover:border-white/[0.1] hover:bg-[#1A1A24]"
>
<div class="mb-3 flex items-center gap-3">
@@ -54,7 +65,7 @@ const colorMap = {
<div class="min-w-0 flex-1">
<h4 class="mb-0.5 text-[14px] font-semibold text-white">{{ category.name }}</h4>
<p class="text-[12px] text-zinc-500">${{ category.spent }} of ${{ category.budget }}</p>
<p class="text-[12px] text-zinc-500">${{ category.spent }} {{ t('finance.categories.of') }} ${{ category.budget }}</p>
</div>
<div class="text-right">
@@ -1,9 +1,11 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { Eye, EyeOff, TrendingUp } from 'lucide-vue-next';
import { useI18n } from '../i18n';
const isVisible = ref(true);
const chartData = [20000, 21500, 20800, 23200, 22500, 24850];
const { t } = useI18n();
const polylinePoints = computed(() => {
const max = Math.max(...chartData);
@@ -30,7 +32,7 @@ const polylinePoints = computed(() => {
<div class="mb-6 flex items-start justify-between">
<div class="flex-1">
<div class="mb-2 flex items-center gap-2">
<p class="text-[13px] font-medium text-purple-100/70">Total Balance</p>
<p class="text-[13px] font-medium text-purple-100/70">{{ t('finance.balance.total') }}</p>
<button
type="button"
@click="isVisible = !isVisible"
@@ -50,7 +52,7 @@ const polylinePoints = computed(() => {
<TrendingUp class="h-3 w-3 text-emerald-300" :stroke-width="2.5" />
<span class="text-[12px] font-semibold text-emerald-300">+12.5%</span>
</div>
<span class="text-[12px] text-purple-200/60">vs last month</span>
<span class="text-[12px] text-purple-200/60">{{ t('finance.balance.vsLastMonth') }}</span>
</div>
</div>
@@ -63,14 +65,14 @@ const polylinePoints = computed(() => {
<div class="grid grid-cols-2 gap-3">
<div class="rounded-[14px] border border-white/10 bg-white/10 p-3 backdrop-blur-sm">
<p class="mb-1 text-[11px] font-medium text-white/60">This Month</p>
<p class="mb-1 text-[11px] font-medium text-white/60">{{ t('finance.balance.thisMonth') }}</p>
<p class="text-[18px] font-bold text-white">$8,240</p>
<p class="mt-0.5 text-[11px] font-medium text-emerald-300">+18% income</p>
<p class="mt-0.5 text-[11px] font-medium text-emerald-300">+18% {{ t('finance.balance.income') }}</p>
</div>
<div class="rounded-[14px] border border-white/10 bg-white/10 p-3 backdrop-blur-sm">
<p class="mb-1 text-[11px] font-medium text-white/60">Expenses</p>
<p class="mb-1 text-[11px] font-medium text-white/60">{{ t('finance.balance.expenses') }}</p>
<p class="text-[18px] font-bold text-white">$3,120</p>
<p class="mt-0.5 text-[11px] font-medium text-rose-300">-8% saved</p>
<p class="mt-0.5 text-[11px] font-medium text-rose-300">-8% {{ t('finance.balance.saved') }}</p>
</div>
</div>
</div>
+10 -8
View File
@@ -1,25 +1,27 @@
<script setup lang="ts">
import { ref } from 'vue';
import { computed, ref } from 'vue';
import { Bell, Plus, Settings, Wallet as WalletIcon } from 'lucide-vue-next';
import FinanceBalanceCard from './FinanceBalanceCard.vue';
import TransactionsList from './TransactionsList.vue';
import AnalyticsView from './AnalyticsView.vue';
import CategoriesView from './CategoriesView.vue';
import Navigation from './Navigation.vue';
import { useI18n } from '../i18n';
type Tab = 'transactions' | 'analytics' | 'categories';
const emit = defineEmits<{
navigate: [screen: string];
}>();
const { t } = useI18n();
const activeTab = ref<Tab>('transactions');
const tabs: Array<{ id: Tab; label: string }> = [
{ id: 'transactions', label: 'Transactions' },
{ id: 'analytics', label: 'Analytics' },
{ id: 'categories', label: 'Categories' },
];
const tabs = computed<Array<{ id: Tab; label: string }>>(() => [
{ id: 'transactions', label: t('finance.tab.transactions') },
{ id: 'analytics', label: t('finance.tab.analytics') },
{ id: 'categories', label: t('finance.tab.categories') },
]);
</script>
<template>
@@ -31,8 +33,8 @@ const tabs: Array<{ id: Tab; label: string }> = [
<WalletIcon class="h-5 w-5 text-white" :stroke-width="2.5" />
</div>
<div>
<p class="mb-0.5 text-[11px] font-normal text-zinc-500">Family budget</p>
<h1 class="text-[17px] font-semibold tracking-tight text-white">Finance</h1>
<p class="mb-0.5 text-[11px] font-normal text-zinc-500">{{ t('finance.header.eyebrow') }}</p>
<h1 class="text-[17px] font-semibold tracking-tight text-white">{{ t('finance.header.title') }}</h1>
</div>
</div>
<div class="flex items-center gap-2">
+39 -2
View File
@@ -1,9 +1,46 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
import { Bell, Settings, User } from 'lucide-vue-next';
import { useI18n } from '@/i18n';
const emit = defineEmits<{
navigate: [screen: string];
}>();
const { t } = useI18n();
const currentHour = ref(new Date().getHours());
let timerId: number | undefined;
const greeting = computed(() => {
if (currentHour.value < 5) {
return t('header.greeting.night');
}
if (currentHour.value < 12) {
return t('header.greeting.morning');
}
if (currentHour.value < 18) {
return t('header.greeting.afternoon');
}
return t('header.greeting.evening');
});
function updateCurrentHour() {
currentHour.value = new Date().getHours();
}
onMounted(() => {
updateCurrentHour();
timerId = window.setInterval(updateCurrentHour, 60_000);
});
onBeforeUnmount(() => {
if (timerId !== undefined) {
window.clearInterval(timerId);
}
});
</script>
<template>
@@ -13,8 +50,8 @@ const emit = defineEmits<{
<User class="h-5 w-5 text-white" :stroke-width="2.5" />
</div>
<div>
<p class="mb-0.5 text-[11px] font-normal text-zinc-500">Good evening</p>
<h1 class="text-[17px] font-semibold tracking-tight text-white">Anderson Family</h1>
<p class="mb-0.5 text-[11px] font-normal text-zinc-500">{{ greeting }}</p>
<h1 class="text-[17px] font-semibold tracking-tight text-white">{{ t('header.familyName') }}</h1>
</div>
</div>
<div class="flex items-center gap-2">
+18 -8
View File
@@ -1,9 +1,11 @@
<script setup lang="ts">
import {Calendar, Heart, Home, Sparkles, Wallet} from 'lucide-vue-next';
import { computed, type Component } from 'vue';
import { useI18n } from '../i18n';
interface NavItem {
icon: unknown;
label: string;
icon: Component;
labelKey: string;
id: string;
}
@@ -14,14 +16,22 @@ const props = defineProps<{
const emit = defineEmits<{
navigate: [screen: string];
}>();
const { t } = useI18n();
const navItems: NavItem[] = [
{ icon: Calendar, label: 'Calendar', id: 'calendar' },
{ icon: Wallet, label: 'Finance', id: 'finance' },
{ icon: Home, label: 'Home', id: 'home' },
{ icon: Heart, label: 'Votes', id: 'votes' },
{ icon: Sparkles, label: 'Intimacy', id: 'intimacy' },
{ icon: Calendar, labelKey: 'nav.calendar', id: 'calendar' },
{ icon: Wallet, labelKey: 'nav.finance', id: 'finance' },
{ icon: Home, labelKey: 'nav.home', id: 'home' },
{ icon: Heart, labelKey: 'nav.votes', id: 'votes' },
{ icon: Sparkles, labelKey: 'nav.intimacy', id: 'intimacy' },
];
const localizedNavItems = computed(() =>
navItems.map((item) => ({
...item,
label: t(item.labelKey),
})),
);
</script>
<template>
@@ -29,7 +39,7 @@ const navItems: NavItem[] = [
<div class="pointer-events-auto rounded-[20px] border border-white/[0.08] bg-[#1A1A24]/95 px-2 py-3 shadow-[0_8px_32px_rgba(0,0,0,0.6)] backdrop-blur-xl">
<div class="flex items-center justify-between">
<button
v-for="item in navItems"
v-for="item in localizedNavItems"
:key="item.id"
type="button"
@click="emit('navigate', item.id)"
@@ -1,35 +1,38 @@
<script setup lang="ts">
import { Activity, CreditCard, Popcorn, ShoppingCart, Users } from 'lucide-vue-next';
import { useI18n } from '../i18n';
const { t } = useI18n();
const activities = [
{
icon: ShoppingCart,
title: 'Grocery Shopping',
subtitle: 'Sarah • $124.50',
titleKey: 'home.activity.shopping',
subtitleKey: 'home.activity.shoppingSubtitle',
time: '2h',
bgColor: 'bg-emerald-500/10',
textColor: 'text-emerald-400',
},
{
icon: Popcorn,
title: 'Movie Vote',
subtitle: '3 votes for "Dune 2"',
titleKey: 'home.activity.movieVote',
subtitleKey: 'home.activity.movieVoteSubtitle',
time: '5h',
bgColor: 'bg-purple-500/10',
textColor: 'text-purple-400',
},
{
icon: CreditCard,
title: 'Subscription',
subtitle: 'Netflix • $15.99',
titleKey: 'home.activity.subscription',
subtitleKey: 'home.activity.subscriptionSubtitle',
time: '1d',
bgColor: 'bg-rose-500/10',
textColor: 'text-rose-400',
},
{
icon: Users,
title: 'New Event',
subtitle: 'Family BBQ Saturday',
titleKey: 'home.activity.newEvent',
subtitleKey: 'home.activity.newEventSubtitle',
time: '2d',
bgColor: 'bg-blue-500/10',
textColor: 'text-blue-400',
@@ -44,25 +47,25 @@ const activities = [
<div class="flex h-9 w-9 items-center justify-center rounded-[12px] bg-blue-500/10">
<Activity class="h-[18px] w-[18px] text-blue-400" :stroke-width="2" />
</div>
<h3 class="text-[15px] font-semibold text-white">Activity</h3>
<h3 class="text-[15px] font-semibold text-white">{{ t('home.activity.title') }}</h3>
</div>
<button class="text-[12px] font-medium text-purple-400 transition-colors hover:text-purple-300">
View all
{{ t('home.activity.viewAll') }}
</button>
</div>
<div class="space-y-2">
<div
v-for="activity in activities"
:key="activity.title"
:key="activity.titleKey"
class="group flex cursor-pointer items-center gap-3 rounded-[12px] p-3 transition-all hover:bg-white/[0.03]"
>
<div :class="['flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-[12px] transition-transform group-hover:scale-110', activity.bgColor]">
<component :is="activity.icon" :class="['h-[18px] w-[18px]', activity.textColor]" :stroke-width="2" />
</div>
<div class="min-w-0 flex-1">
<p class="mb-0.5 text-[14px] font-medium text-white">{{ activity.title }}</p>
<p class="truncate text-[12px] text-zinc-500">{{ activity.subtitle }}</p>
<p class="mb-0.5 text-[14px] font-medium text-white">{{ t(activity.titleKey) }}</p>
<p class="truncate text-[12px] text-zinc-500">{{ t(activity.subtitleKey) }}</p>
</div>
<span class="flex-shrink-0 text-[12px] font-medium text-zinc-600">{{ activity.time }}</span>
</div>
+115 -42
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { reactive, type Component } from 'vue'
import { computed, reactive, type Component } from 'vue'
import {
Bell,
Calendar,
@@ -19,6 +19,7 @@ import {
Utensils,
Wallet,
} from 'lucide-vue-next'
import { type Locale, useI18n } from '../i18n'
interface ModuleItem {
id: string;
@@ -43,6 +44,7 @@ const props = defineProps<{
const emit = defineEmits<{
navigate: [screen: string];
}>()
const { locale, setLocale, t } = useI18n()
const itemColorMap = {
purple: { bg: 'bg-purple-500/10', text: 'text-purple-400' },
@@ -68,15 +70,15 @@ const familyItems: SettingItemData[] = [
{
icon: Users,
color: 'blue',
label: 'Family Members',
description: 'Manage family access',
label: 'settings.family.members.label',
description: 'settings.family.members.description',
badge: '4',
},
{
icon: Crown,
color: 'amber',
label: 'Roles & Permissions',
description: 'Control what members can do',
label: 'settings.family.roles.label',
description: 'settings.family.roles.description',
},
]
@@ -84,14 +86,14 @@ const customizationItems: SettingItemData[] = [
{
icon: LayoutGrid,
color: 'purple',
label: 'Dashboard Widgets',
description: 'Customize your home screen',
label: 'settings.customization.widgets.label',
description: 'settings.customization.widgets.description',
},
{
icon: Palette,
color: 'pink',
label: 'Theme & Appearance',
description: 'Dark mode, colors, fonts',
label: 'settings.customization.theme.label',
description: 'settings.customization.theme.description',
},
]
@@ -99,20 +101,20 @@ const privacyItems: SettingItemData[] = [
{
icon: Lock,
color: 'emerald',
label: 'Privacy Settings',
description: 'Control data sharing',
label: 'settings.privacy.label',
description: 'settings.privacy.description',
},
{
icon: Eye,
color: 'indigo',
label: 'Visibility Controls',
description: 'Who can see what',
label: 'settings.visibility.label',
description: 'settings.visibility.description',
},
{
icon: Shield,
color: 'cyan',
label: 'Security',
description: 'Password, 2FA, biometrics',
label: 'settings.security.label',
description: 'settings.security.description',
},
]
@@ -120,25 +122,62 @@ const advancedItems: SettingItemData[] = [
{
icon: Bell,
color: 'orange',
label: 'Notifications',
description: 'Push, email, SMS settings',
label: 'settings.notifications.label',
description: 'settings.notifications.description',
},
{
icon: SettingsIcon,
color: 'zinc',
label: 'App Preferences',
description: 'Language, timezone, units',
label: 'settings.preferences.label',
description: 'settings.preferences.description',
},
]
const modules = reactive<ModuleItem[]>([
{ id: 'finance', name: 'Finance Tracking', icon: Wallet, color: 'purple', enabled: true },
{ id: 'calendar', name: 'Family Calendar', icon: Calendar, color: 'blue', enabled: true },
{ id: 'food', name: 'Food Voting', icon: Utensils, color: 'orange', enabled: true },
{ id: 'movies', name: 'Movie Voting', icon: Film, color: 'pink', enabled: true },
{ id: 'activities', name: 'Activity Feed', icon: Heart, color: 'red', enabled: true },
{ id: 'finance', name: 'settings.module.finance', icon: Wallet, color: 'purple', enabled: true },
{ id: 'calendar', name: 'settings.module.calendar', icon: Calendar, color: 'blue', enabled: true },
{ id: 'food', name: 'settings.module.food', icon: Utensils, color: 'orange', enabled: true },
{ id: 'movies', name: 'settings.module.movies', icon: Film, color: 'pink', enabled: true },
{ id: 'activities', name: 'settings.module.activities', icon: Heart, color: 'red', enabled: true },
])
const languageOptions: Array<{ value: Locale; labelKey: string }> = [
{ value: 'en', labelKey: 'language.english' },
{ value: 'ru', labelKey: 'language.russian' },
]
const localizedFamilyItems = computed(() =>
familyItems.map((item) => ({
...item,
label: t(item.label),
description: t(item.description),
})),
)
const localizedCustomizationItems = computed(() =>
customizationItems.map((item) => ({
...item,
label: t(item.label),
description: t(item.description),
})),
)
const localizedPrivacyItems = computed(() =>
privacyItems.map((item) => ({
...item,
label: t(item.label),
description: t(item.description),
})),
)
const localizedAdvancedItems = computed(() =>
advancedItems.map((item) => ({
...item,
label: t(item.label),
description: t(item.description),
})),
)
function toggleModule(id: string) {
const moduleItem = modules.find((item) => item.id === id)
if (moduleItem) {
@@ -161,12 +200,12 @@ function animationDelay(index: number, base = 0.1) {
<div class="relative mx-auto flex min-h-screen max-w-md flex-col">
<header class="flex items-center justify-between px-5 pt-6 pb-4">
<div class="flex items-center gap-3">
<div class="flex h-11 w-11 items-center justify-center rounded-[16px] bg-gradient-to-br from-purple-500 to-blue-600 shadow-lg shadow-purple-500/20">
<SettingsIcon class="h-5 w-5 text-white" :stroke-width="2.5" />
</div>
<div class="flex h-11 w-11 items-center justify-center rounded-[16px] bg-gradient-to-br from-purple-500 to-blue-600 shadow-lg shadow-purple-500/20">
<SettingsIcon class="h-5 w-5 text-white" :stroke-width="2.5" />
</div>
<div>
<p class="mb-0.5 text-[11px] font-normal text-zinc-500">Manage your family hub</p>
<h1 class="text-[17px] font-semibold tracking-tight text-white">Settings</h1>
<p class="mb-0.5 text-[11px] font-normal text-zinc-500">{{ t('settings.header.eyebrow') }}</p>
<h1 class="text-[17px] font-semibold tracking-tight text-white">{{ t('settings.header.title') }}</h1>
</div>
</div>
<button
@@ -194,7 +233,7 @@ function animationDelay(index: number, base = 0.1) {
<div class="min-w-0 flex-1">
<h2 class="mb-0.5 text-[18px] font-bold text-white">John Smith</h2>
<p class="mb-1 text-[13px] font-medium text-purple-300">Family Admin</p>
<p class="mb-1 text-[13px] font-medium text-purple-300">{{ t('settings.profile.role') }}</p>
<p class="text-[12px] text-zinc-500">john.smith@email.com</p>
</div>
@@ -206,11 +245,11 @@ function animationDelay(index: number, base = 0.1) {
<section class="screen-enter" :style="animationDelay(1)">
<h3 class="mb-3 px-1 text-[12px] font-semibold uppercase tracking-wider text-zinc-400">
Family
{{ t('settings.section.family') }}
</h3>
<div class="space-y-2">
<button
v-for="item in familyItems"
v-for="item in localizedFamilyItems"
:key="item.label"
type="button"
class="group flex w-full items-center gap-3 rounded-[16px] border border-white/[0.06] bg-[#16161F] p-4 transition-all hover:border-white/[0.1] hover:bg-[#1A1A24]"
@@ -232,11 +271,11 @@ function animationDelay(index: number, base = 0.1) {
<section class="screen-enter" :style="animationDelay(2)">
<h3 class="mb-3 px-1 text-[12px] font-semibold uppercase tracking-wider text-zinc-400">
Customization
{{ t('settings.section.customization') }}
</h3>
<div class="space-y-2">
<button
v-for="item in customizationItems"
v-for="item in localizedCustomizationItems"
:key="item.label"
type="button"
class="group flex w-full items-center gap-3 rounded-[16px] border border-white/[0.06] bg-[#16161F] p-4 transition-all hover:border-white/[0.1] hover:bg-[#1A1A24]"
@@ -253,9 +292,43 @@ function animationDelay(index: number, base = 0.1) {
</div>
</section>
<section class="screen-enter" :style="animationDelay(2.5)">
<h3 class="mb-3 px-1 text-[12px] font-semibold uppercase tracking-wider text-zinc-400">
{{ t('language.title') }}
</h3>
<div class="rounded-[16px] border border-white/[0.06] bg-[#16161F] p-2">
<div class="mb-3 flex items-center gap-3 px-2 pt-2">
<div class="flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-[13px] bg-blue-500/10">
<SettingsIcon class="h-[19px] w-[19px] text-blue-400" :stroke-width="2" />
</div>
<div class="min-w-0 flex-1">
<p class="mb-0.5 text-[14px] font-semibold text-white">{{ t('language.title') }}</p>
<p class="text-[12px] text-zinc-500">{{ t('language.description') }}</p>
</div>
</div>
<div class="grid grid-cols-2 gap-2">
<button
v-for="option in languageOptions"
:key="option.value"
type="button"
:class="[
'rounded-[12px] border px-3 py-2.5 text-[13px] font-semibold transition-all',
locale === option.value
? 'border-purple-500/30 bg-gradient-to-br from-purple-500/15 to-blue-500/15 text-purple-300'
: 'border-white/[0.06] bg-[#1A1A24] text-zinc-400 hover:bg-[#222230]',
]"
@click="setLocale(option.value)"
>
{{ t(option.labelKey) }}
</button>
</div>
</div>
</section>
<section class="screen-enter" :style="animationDelay(3)">
<h3 class="mb-3 px-1 text-[12px] font-semibold uppercase tracking-wider text-zinc-400">
Active Modules
{{ t('settings.section.activeModules') }}
</h3>
<div class="space-y-2">
<div
@@ -268,9 +341,9 @@ function animationDelay(index: number, base = 0.1) {
<component :is="module.icon" :class="['h-[19px] w-[19px]', moduleColorMap[module.color].text]" :stroke-width="2" />
</div>
<div class="min-w-0 flex-1">
<p class="text-[14px] font-semibold text-white">{{ module.name }}</p>
<p class="text-[14px] font-semibold text-white">{{ t(module.name) }}</p>
<p :class="['text-[12px]', module.enabled ? 'text-emerald-400' : 'text-zinc-600']">
{{ module.enabled ? 'Active' : 'Disabled' }}
{{ module.enabled ? t('settings.module.active') : t('settings.module.disabled') }}
</p>
</div>
<button
@@ -295,11 +368,11 @@ function animationDelay(index: number, base = 0.1) {
<section class="screen-enter" :style="animationDelay(4)">
<h3 class="mb-3 px-1 text-[12px] font-semibold uppercase tracking-wider text-zinc-400">
Privacy &amp; Security
{{ t('settings.section.privacy') }}
</h3>
<div class="space-y-2">
<button
v-for="item in privacyItems"
v-for="item in localizedPrivacyItems"
:key="item.label"
type="button"
class="group flex w-full items-center gap-3 rounded-[16px] border border-white/[0.06] bg-[#16161F] p-4 transition-all hover:border-white/[0.1] hover:bg-[#1A1A24]"
@@ -318,11 +391,11 @@ function animationDelay(index: number, base = 0.1) {
<section class="screen-enter" :style="animationDelay(5)">
<h3 class="mb-3 px-1 text-[12px] font-semibold uppercase tracking-wider text-zinc-400">
Advanced
{{ t('settings.section.advanced') }}
</h3>
<div class="space-y-2">
<button
v-for="item in advancedItems"
v-for="item in localizedAdvancedItems"
:key="item.label"
type="button"
class="group flex w-full items-center gap-3 rounded-[16px] border border-white/[0.06] bg-[#16161F] p-4 transition-all hover:border-white/[0.1] hover:bg-[#1A1A24]"
@@ -346,7 +419,7 @@ function animationDelay(index: number, base = 0.1) {
>
<LogOut class="h-[18px] w-[18px] text-red-400 transition-colors group-hover:text-red-300" :stroke-width="2" />
<span class="text-[14px] font-semibold text-red-400 transition-colors group-hover:text-red-300">
Sign Out
{{ t('settings.signOut') }}
</span>
</button>
+26 -23
View File
@@ -1,44 +1,47 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import { Clock, Heart, MapPin, Sparkles, Star, X } from 'lucide-vue-next';
import { useI18n } from '../i18n';
interface SwipeItem {
id: number;
type: 'food' | 'movie';
title: string;
subtitle: string;
titleKey: string;
subtitleKey: string;
rating: number;
location?: string;
duration?: string;
locationKey?: string;
durationKey?: string;
image: string;
}
const { t } = useI18n();
const cards: SwipeItem[] = [
{
id: 1,
type: 'food',
title: 'Italian Restaurant',
subtitle: 'Authentic pasta & pizza',
titleKey: 'home.swipe.card.italian.title',
subtitleKey: 'home.swipe.card.italian.subtitle',
rating: 4.8,
location: '2.3 km away',
locationKey: 'home.swipe.card.italian.location',
image: 'https://images.unsplash.com/photo-1555396273-367ea4eb4db5?w=800&q=80',
},
{
id: 2,
type: 'movie',
title: 'Dune: Part Two',
subtitle: 'Sci-Fi Epic',
titleKey: 'home.swipe.card.dune.title',
subtitleKey: 'home.swipe.card.dune.subtitle',
rating: 4.9,
duration: '2h 46m',
durationKey: 'home.swipe.card.dune.duration',
image: 'https://images.unsplash.com/photo-1536440136628-849c177e76a1?w=800&q=80',
},
{
id: 3,
type: 'food',
title: 'Sushi House',
subtitle: 'Fresh Japanese cuisine',
titleKey: 'home.swipe.card.sushi.title',
subtitleKey: 'home.swipe.card.sushi.subtitle',
rating: 4.7,
location: '1.8 km away',
locationKey: 'home.swipe.card.sushi.location',
image: 'https://images.unsplash.com/photo-1579584425555-c3ce17fd4351?w=800&q=80',
},
];
@@ -89,8 +92,8 @@ function onPointerUp(event: PointerEvent) {
<Sparkles class="h-[18px] w-[18px] text-purple-400" :stroke-width="2" />
</div>
<div>
<h3 class="text-[15px] font-semibold text-white">Vote Now</h3>
<p class="text-[11px] text-zinc-500">Swipe to decide</p>
<h3 class="text-[15px] font-semibold text-white">{{ t('home.swipe.title') }}</h3>
<p class="text-[11px] text-zinc-500">{{ t('home.swipe.subtitle') }}</p>
</div>
</div>
@@ -113,7 +116,7 @@ function onPointerUp(event: PointerEvent) {
@pointerdown="onPointerDown"
@pointerup="onPointerUp"
>
<img :src="currentCard.image" :alt="currentCard.title" class="h-full w-full object-cover" />
<img :src="currentCard.image" :alt="t(currentCard.titleKey)" class="h-full w-full object-cover" />
<div class="absolute inset-0 bg-gradient-to-t from-black via-black/50 to-transparent" />
<div class="absolute top-4 right-4 flex items-center gap-1.5 rounded-full border border-white/10 bg-black/60 px-3 py-1.5 backdrop-blur-md">
@@ -124,19 +127,19 @@ function onPointerUp(event: PointerEvent) {
<div class="absolute bottom-0 left-0 right-0 p-5">
<div class="mb-3">
<span class="mb-3 inline-flex items-center gap-1.5 rounded-lg border border-purple-500/20 bg-purple-500/20 px-2.5 py-1.5 text-[11px] font-semibold text-purple-300 backdrop-blur-sm">
{{ currentCard.type === 'food' ? 'Restaurant' : 'Movie' }}
{{ currentCard.type === 'food' ? t('home.swipe.type.food') : t('home.swipe.type.movie') }}
</span>
<h4 class="mb-1 text-[24px] font-bold leading-tight text-white">{{ currentCard.title }}</h4>
<p class="mb-3 text-[14px] text-zinc-300">{{ currentCard.subtitle }}</p>
<h4 class="mb-1 text-[24px] font-bold leading-tight text-white">{{ t(currentCard.titleKey) }}</h4>
<p class="mb-3 text-[14px] text-zinc-300">{{ t(currentCard.subtitleKey) }}</p>
</div>
<div class="flex items-center gap-4 text-[12px] text-zinc-400">
<div v-if="currentCard.location" class="flex items-center gap-1.5">
<div v-if="currentCard.locationKey" class="flex items-center gap-1.5">
<MapPin class="h-3.5 w-3.5" :stroke-width="2" />
<span class="font-medium">{{ currentCard.location }}</span>
<span class="font-medium">{{ t(currentCard.locationKey) }}</span>
</div>
<div v-if="currentCard.duration" class="flex items-center gap-1.5">
<div v-if="currentCard.durationKey" class="flex items-center gap-1.5">
<Clock class="h-3.5 w-3.5" :stroke-width="2" />
<span class="font-medium">{{ currentCard.duration }}</span>
<span class="font-medium">{{ t(currentCard.durationKey) }}</span>
</div>
</div>
</div>
+22 -10
View File
@@ -1,22 +1,34 @@
<script setup lang="ts">
import { computed } from 'vue';
import { Calendar, Clock, MapPin } from 'lucide-vue-next';
import { useI18n } from '../i18n';
const { locale, t } = useI18n();
const todayEvents = [
{
time: '6:30 PM',
title: 'Family Dinner',
location: 'Home',
titleKey: 'home.today.event.familyDinner',
locationKey: 'home.today.location.home',
colorFrom: '#a855f7',
colorTo: '#9333ea',
},
{
time: '8:00 PM',
title: 'Movie Night',
location: 'Living Room',
titleKey: 'home.today.event.movieNight',
locationKey: 'home.today.location.livingRoom',
colorFrom: '#3b82f6',
colorTo: '#2563eb',
},
];
const formattedToday = computed(() =>
new Intl.DateTimeFormat(locale.value, {
weekday: 'long',
month: 'long',
day: 'numeric',
}).format(new Date()),
);
</script>
<template>
@@ -27,17 +39,17 @@ const todayEvents = [
<Calendar class="h-[18px] w-[18px] text-purple-400" :stroke-width="2" />
</div>
<div>
<h3 class="text-[15px] font-semibold text-white">Today</h3>
<p class="text-[12px] text-zinc-500">Wednesday, April 2</p>
<h3 class="text-[15px] font-semibold text-white">{{ t('home.today.title') }}</h3>
<p class="text-[12px] text-zinc-500">{{ formattedToday }}</p>
</div>
</div>
<span class="text-[12px] font-medium text-zinc-600">{{ todayEvents.length }} events</span>
<span class="text-[12px] font-medium text-zinc-600">{{ todayEvents.length }} {{ t('home.today.events') }}</span>
</div>
<div class="space-y-2.5">
<div
v-for="event in todayEvents"
:key="event.title"
:key="event.titleKey"
class="group flex cursor-pointer items-center gap-3 rounded-[14px] border border-white/[0.04] bg-white/[0.02] p-3.5 transition-all hover:border-white/[0.08] hover:bg-white/[0.05]"
>
<div class="flex min-w-[52px] flex-col items-center justify-center rounded-[10px] border border-white/[0.06] bg-white/[0.04] py-2 px-2.5">
@@ -45,10 +57,10 @@ const todayEvents = [
<span class="text-[13px] font-semibold leading-none text-white">{{ event.time }}</span>
</div>
<div class="min-w-0 flex-1">
<h4 class="mb-0.5 text-[14px] font-semibold text-white">{{ event.title }}</h4>
<h4 class="mb-0.5 text-[14px] font-semibold text-white">{{ t(event.titleKey) }}</h4>
<div class="flex items-center gap-1 text-zinc-500">
<MapPin class="h-3 w-3" :stroke-width="2" />
<span class="text-[12px]">{{ event.location }}</span>
<span class="text-[12px]">{{ t(event.locationKey) }}</span>
</div>
</div>
<div
+21 -17
View File
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { type Component } from 'vue';
import {
Car,
Coffee,
@@ -9,24 +10,27 @@ import {
TrendingUp,
Utensils,
} from 'lucide-vue-next';
import { useI18n } from '../i18n';
interface Transaction {
id: string;
title: string;
category: string;
categoryKey: string;
amount: number;
type: 'income' | 'expense';
icon: unknown;
icon: Component;
color: keyof typeof colorMap;
time: string;
}
interface TransactionGroup {
date: string;
dateKey: string;
total: number;
transactions: Transaction[];
}
const { t } = useI18n();
const colorMap = {
emerald: { bg: 'bg-emerald-500/10', text: 'text-emerald-400' },
orange: { bg: 'bg-orange-500/10', text: 'text-orange-400' },
@@ -40,13 +44,13 @@ const colorMap = {
const transactionGroups: TransactionGroup[] = [
{
date: 'Today',
dateKey: 'finance.transactions.today',
total: -245.5,
transactions: [
{
id: '1',
title: 'Whole Foods Market',
category: 'Groceries',
categoryKey: 'finance.category.groceries',
amount: -124.5,
type: 'expense',
icon: ShoppingBag,
@@ -56,7 +60,7 @@ const transactionGroups: TransactionGroup[] = [
{
id: '2',
title: 'Uber Eats',
category: 'Food & Dining',
categoryKey: 'finance.category.foodDining',
amount: -45,
type: 'expense',
icon: Utensils,
@@ -66,7 +70,7 @@ const transactionGroups: TransactionGroup[] = [
{
id: '3',
title: 'Starbucks',
category: 'Coffee',
categoryKey: 'finance.category.coffee',
amount: -12.5,
type: 'expense',
icon: Coffee,
@@ -76,7 +80,7 @@ const transactionGroups: TransactionGroup[] = [
{
id: '4',
title: 'Freelance Payment',
category: 'Income',
categoryKey: 'finance.category.income',
amount: 850,
type: 'income',
icon: TrendingUp,
@@ -86,13 +90,13 @@ const transactionGroups: TransactionGroup[] = [
],
},
{
date: 'Yesterday',
dateKey: 'finance.transactions.yesterday',
total: -89.99,
transactions: [
{
id: '5',
title: 'Shell Gas Station',
category: 'Transport',
categoryKey: 'finance.category.transport',
amount: -65,
type: 'expense',
icon: Car,
@@ -102,7 +106,7 @@ const transactionGroups: TransactionGroup[] = [
{
id: '6',
title: 'Netflix Subscription',
category: 'Entertainment',
categoryKey: 'finance.category.entertainment',
amount: -15.99,
type: 'expense',
icon: Film,
@@ -112,7 +116,7 @@ const transactionGroups: TransactionGroup[] = [
{
id: '7',
title: 'Charity Donation',
category: 'Donation',
categoryKey: 'finance.category.donation',
amount: -25,
type: 'expense',
icon: Heart,
@@ -122,13 +126,13 @@ const transactionGroups: TransactionGroup[] = [
],
},
{
date: 'Apr 1',
dateKey: 'finance.transactions.apr1',
total: -1250,
transactions: [
{
id: '8',
title: 'Rent Payment',
category: 'Housing',
categoryKey: 'finance.category.housing',
amount: -1250,
type: 'expense',
icon: Home,
@@ -142,9 +146,9 @@ const transactionGroups: TransactionGroup[] = [
<template>
<div class="space-y-6">
<div v-for="group in transactionGroups" :key="group.date">
<div v-for="group in transactionGroups" :key="group.dateKey">
<div class="mb-3 flex items-center justify-between px-1">
<h3 class="text-[14px] font-semibold text-white">{{ group.date }}</h3>
<h3 class="text-[14px] font-semibold text-white">{{ t(group.dateKey) }}</h3>
<span :class="['text-[13px] font-semibold', group.total >= 0 ? 'text-emerald-400' : 'text-zinc-400']">
{{ group.total >= 0 ? '+' : '' }}${{ Math.abs(group.total).toFixed(2) }}
</span>
@@ -163,7 +167,7 @@ const transactionGroups: TransactionGroup[] = [
<div class="min-w-0 flex-1">
<p class="mb-0.5 truncate text-[14px] font-semibold text-white">{{ transaction.title }}</p>
<div class="flex items-center gap-2">
<span class="text-[12px] text-zinc-500">{{ transaction.category }}</span>
<span class="text-[12px] text-zinc-500">{{ t(transaction.categoryKey) }}</span>
<span class="text-zinc-700"></span>
<span class="text-[12px] text-zinc-600">{{ transaction.time }}</span>
</div>
+333
View File
@@ -0,0 +1,333 @@
import { computed, reactive } from 'vue'
export type Locale = 'en' | 'ru'
type Messages = Record<string, string>
const STORAGE_KEY = 'familyhub-locale'
const messages: Record<Locale, Messages> = {
en: {
'language.english': 'English',
'language.russian': 'Russian',
'language.title': 'Language',
'language.description': 'Choose your app language',
'nav.calendar': 'Calendar',
'nav.finance': 'Finance',
'nav.home': 'Home',
'nav.votes': 'Votes',
'nav.intimacy': 'Intimacy',
'header.greeting.night': 'Good night',
'header.greeting.morning': 'Good morning',
'header.greeting.afternoon': 'Good afternoon',
'header.greeting.evening': 'Good evening',
'header.familyName': 'Anderson Family',
'home.balance.total': 'Total Balance',
'home.balance.income': 'Income',
'home.balance.expenses': 'Expenses',
'home.today.title': 'Today',
'home.today.events': 'events',
'home.today.event.familyDinner': 'Family Dinner',
'home.today.event.movieNight': 'Movie Night',
'home.today.location.home': 'Home',
'home.today.location.livingRoom': 'Living Room',
'home.activity.title': 'Activity',
'home.activity.viewAll': 'View all',
'home.activity.shopping': 'Grocery Shopping',
'home.activity.shoppingSubtitle': 'Sarah • $124.50',
'home.activity.movieVote': 'Movie Vote',
'home.activity.movieVoteSubtitle': '3 votes for "Dune 2"',
'home.activity.subscription': 'Subscription',
'home.activity.subscriptionSubtitle': 'Netflix • $15.99',
'home.activity.newEvent': 'New Event',
'home.activity.newEventSubtitle': 'Family BBQ Saturday',
'home.swipe.title': 'Vote Now',
'home.swipe.subtitle': 'Swipe to decide',
'home.swipe.type.food': 'Restaurant',
'home.swipe.type.movie': 'Movie',
'home.swipe.card.italian.title': 'Italian Restaurant',
'home.swipe.card.italian.subtitle': 'Authentic pasta & pizza',
'home.swipe.card.italian.location': '2.3 km away',
'home.swipe.card.dune.title': 'Dune: Part Two',
'home.swipe.card.dune.subtitle': 'Sci-Fi Epic',
'home.swipe.card.dune.duration': '2h 46m',
'home.swipe.card.sushi.title': 'Sushi House',
'home.swipe.card.sushi.subtitle': 'Fresh Japanese cuisine',
'home.swipe.card.sushi.location': '1.8 km away',
'finance.header.eyebrow': 'Family budget',
'finance.header.title': 'Finance',
'finance.tab.transactions': 'Transactions',
'finance.tab.analytics': 'Analytics',
'finance.tab.categories': 'Categories',
'finance.balance.total': 'Total Balance',
'finance.balance.vsLastMonth': 'vs last month',
'finance.balance.thisMonth': 'This Month',
'finance.balance.income': 'income',
'finance.balance.expenses': 'Expenses',
'finance.balance.saved': 'saved',
'finance.transactions.today': 'Today',
'finance.transactions.yesterday': 'Yesterday',
'finance.transactions.apr1': 'Apr 1',
'finance.category.groceries': 'Groceries',
'finance.category.foodDining': 'Food & Dining',
'finance.category.coffee': 'Coffee',
'finance.category.income': 'Income',
'finance.category.transport': 'Transport',
'finance.category.entertainment': 'Entertainment',
'finance.category.donation': 'Donation',
'finance.category.housing': 'Housing',
'finance.analytics.avgIncome': 'Avg. Income',
'finance.analytics.growth': '+5.2% growth',
'finance.analytics.avgExpenses': 'Avg. Expenses',
'finance.analytics.decrease': '-12% decrease',
'finance.analytics.byCategory': 'Spending by Category',
'finance.analytics.lastMonths': 'Last 6 Months',
'finance.analytics.month.oct': 'Oct',
'finance.analytics.month.nov': 'Nov',
'finance.analytics.month.dec': 'Dec',
'finance.analytics.month.jan': 'Jan',
'finance.analytics.month.feb': 'Feb',
'finance.analytics.month.mar': 'Mar',
'finance.analytics.month.income': 'Income',
'finance.analytics.month.expenses': 'Expenses',
'finance.analytics.food': 'Food',
'finance.analytics.transport': 'Transport',
'finance.analytics.shopping': 'Shopping',
'finance.analytics.bills': 'Bills',
'finance.analytics.others': 'Others',
'finance.categories.foodDining': 'Food & Dining',
'finance.categories.shopping': 'Shopping',
'finance.categories.transport': 'Transport',
'finance.categories.housing': 'Housing',
'finance.categories.entertainment': 'Entertainment',
'finance.categories.coffee': 'Coffee',
'finance.categories.billsUtilities': 'Bills & Utilities',
'finance.categories.work': 'Work',
'finance.categories.giftsDonations': 'Gifts & Donations',
'finance.categories.others': 'Others',
'finance.categories.of': 'of',
'settings.header.eyebrow': 'Manage your family hub',
'settings.header.title': 'Settings',
'settings.profile.role': 'Family Admin',
'settings.section.family': 'Family',
'settings.section.customization': 'Customization',
'settings.section.activeModules': 'Active Modules',
'settings.section.privacy': 'Privacy & Security',
'settings.section.advanced': 'Advanced',
'settings.family.members.label': 'Family Members',
'settings.family.members.description': 'Manage family access',
'settings.family.roles.label': 'Roles & Permissions',
'settings.family.roles.description': 'Control what members can do',
'settings.customization.widgets.label': 'Dashboard Widgets',
'settings.customization.widgets.description': 'Customize your home screen',
'settings.customization.theme.label': 'Theme & Appearance',
'settings.customization.theme.description': 'Dark mode, colors, fonts',
'settings.privacy.label': 'Privacy Settings',
'settings.privacy.description': 'Control data sharing',
'settings.visibility.label': 'Visibility Controls',
'settings.visibility.description': 'Who can see what',
'settings.security.label': 'Security',
'settings.security.description': 'Password, 2FA, biometrics',
'settings.notifications.label': 'Notifications',
'settings.notifications.description': 'Push, email, SMS settings',
'settings.preferences.label': 'App Preferences',
'settings.preferences.description': 'Language, timezone, units',
'settings.module.finance': 'Finance Tracking',
'settings.module.calendar': 'Family Calendar',
'settings.module.food': 'Food Voting',
'settings.module.movies': 'Movie Voting',
'settings.module.activities': 'Activity Feed',
'settings.module.active': 'Active',
'settings.module.disabled': 'Disabled',
'settings.signOut': 'Sign Out',
},
ru: {
'language.english': 'Английский',
'language.russian': 'Русский',
'language.title': 'Язык',
'language.description': 'Выберите язык приложения',
'nav.calendar': 'Календарь',
'nav.finance': 'Финансы',
'nav.home': 'Главная',
'nav.votes': 'Голосования',
'nav.intimacy': 'Близость',
'header.greeting.night': 'Доброй ночи',
'header.greeting.morning': 'Доброе утро',
'header.greeting.afternoon': 'Добрый день',
'header.greeting.evening': 'Добрый вечер',
'header.familyName': 'Семья Андерсон',
'home.balance.total': 'Общий баланс',
'home.balance.income': 'Доходы',
'home.balance.expenses': 'Расходы',
'home.today.title': 'Сегодня',
'home.today.events': 'события',
'home.today.event.familyDinner': 'Семейный ужин',
'home.today.event.movieNight': 'Киновечер',
'home.today.location.home': 'Дом',
'home.today.location.livingRoom': 'Гостиная',
'home.activity.title': 'Активность',
'home.activity.viewAll': 'Смотреть все',
'home.activity.shopping': 'Покупка продуктов',
'home.activity.shoppingSubtitle': 'Сара • $124.50',
'home.activity.movieVote': 'Голосование за фильм',
'home.activity.movieVoteSubtitle': '3 голоса за "Дюна 2"',
'home.activity.subscription': 'Подписка',
'home.activity.subscriptionSubtitle': 'Netflix • $15.99',
'home.activity.newEvent': 'Новое событие',
'home.activity.newEventSubtitle': 'Семейное барбекю в субботу',
'home.swipe.title': 'Голосуйте',
'home.swipe.subtitle': 'Свайпните, чтобы выбрать',
'home.swipe.type.food': 'Ресторан',
'home.swipe.type.movie': 'Фильм',
'home.swipe.card.italian.title': 'Итальянский ресторан',
'home.swipe.card.italian.subtitle': 'Аутентичная паста и пицца',
'home.swipe.card.italian.location': '2.3 км отсюда',
'home.swipe.card.dune.title': 'Дюна: Часть вторая',
'home.swipe.card.dune.subtitle': 'Научно-фантастический эпик',
'home.swipe.card.dune.duration': '2 ч 46 мин',
'home.swipe.card.sushi.title': 'Суши Хаус',
'home.swipe.card.sushi.subtitle': 'Свежая японская кухня',
'home.swipe.card.sushi.location': '1.8 км отсюда',
'finance.header.eyebrow': 'Семейный бюджет',
'finance.header.title': 'Финансы',
'finance.tab.transactions': 'Транзакции',
'finance.tab.analytics': 'Аналитика',
'finance.tab.categories': 'Категории',
'finance.balance.total': 'Общий баланс',
'finance.balance.vsLastMonth': 'по сравнению с прошлым месяцем',
'finance.balance.thisMonth': 'Этот месяц',
'finance.balance.income': 'доход',
'finance.balance.expenses': 'Расходы',
'finance.balance.saved': 'сэкономлено',
'finance.transactions.today': 'Сегодня',
'finance.transactions.yesterday': 'Вчера',
'finance.transactions.apr1': '1 апр',
'finance.category.groceries': 'Продукты',
'finance.category.foodDining': 'Еда и рестораны',
'finance.category.coffee': 'Кофе',
'finance.category.income': 'Доход',
'finance.category.transport': 'Транспорт',
'finance.category.entertainment': 'Развлечения',
'finance.category.donation': 'Пожертвование',
'finance.category.housing': 'Жильё',
'finance.analytics.avgIncome': 'Средний доход',
'finance.analytics.growth': '+5.2% рост',
'finance.analytics.avgExpenses': 'Средние расходы',
'finance.analytics.decrease': '-12% снижение',
'finance.analytics.byCategory': 'Расходы по категориям',
'finance.analytics.lastMonths': 'Последние 6 месяцев',
'finance.analytics.month.oct': 'Окт',
'finance.analytics.month.nov': 'Ноя',
'finance.analytics.month.dec': 'Дек',
'finance.analytics.month.jan': 'Янв',
'finance.analytics.month.feb': 'Фев',
'finance.analytics.month.mar': 'Мар',
'finance.analytics.month.income': 'Доход',
'finance.analytics.month.expenses': 'Расходы',
'finance.analytics.food': 'Еда',
'finance.analytics.transport': 'Транспорт',
'finance.analytics.shopping': 'Покупки',
'finance.analytics.bills': 'Счета',
'finance.analytics.others': 'Прочее',
'finance.categories.foodDining': 'Еда и рестораны',
'finance.categories.shopping': 'Покупки',
'finance.categories.transport': 'Транспорт',
'finance.categories.housing': 'Жильё',
'finance.categories.entertainment': 'Развлечения',
'finance.categories.coffee': 'Кофе',
'finance.categories.billsUtilities': 'Счета и коммунальные',
'finance.categories.work': 'Работа',
'finance.categories.giftsDonations': 'Подарки и пожертвования',
'finance.categories.others': 'Прочее',
'finance.categories.of': 'из',
'settings.header.eyebrow': 'Управляйте семейным хабом',
'settings.header.title': 'Настройки',
'settings.profile.role': 'Администратор семьи',
'settings.section.family': 'Семья',
'settings.section.customization': 'Кастомизация',
'settings.section.activeModules': 'Активные модули',
'settings.section.privacy': 'Конфиденциальность и защита',
'settings.section.advanced': 'Дополнительно',
'settings.family.members.label': 'Члены семьи',
'settings.family.members.description': 'Управление доступом семьи',
'settings.family.roles.label': 'Роли и разрешения',
'settings.family.roles.description': 'Контроль возможностей участников',
'settings.customization.widgets.label': 'Виджеты дашборда',
'settings.customization.widgets.description': 'Настройте главный экран',
'settings.customization.theme.label': 'Тема и внешний вид',
'settings.customization.theme.description': 'Тёмная тема, цвета, шрифты',
'settings.privacy.label': 'Настройки приватности',
'settings.privacy.description': 'Управление обменом данными',
'settings.visibility.label': 'Параметры видимости',
'settings.visibility.description': 'Кто что может видеть',
'settings.security.label': 'Безопасность',
'settings.security.description': 'Пароль, 2FA, биометрия',
'settings.notifications.label': 'Уведомления',
'settings.notifications.description': 'Push, email, SMS настройки',
'settings.preferences.label': 'Параметры приложения',
'settings.preferences.description': 'Язык, часовой пояс, единицы',
'settings.module.finance': 'Учёт финансов',
'settings.module.calendar': 'Семейный календарь',
'settings.module.food': 'Голосование за еду',
'settings.module.movies': 'Голосование за фильмы',
'settings.module.activities': 'Лента активности',
'settings.module.active': 'Активен',
'settings.module.disabled': 'Отключён',
'settings.signOut': 'Выйти',
},
}
function detectLocale(): Locale {
if (typeof window === 'undefined') {
return 'en'
}
const stored = window.localStorage.getItem(STORAGE_KEY)
if (stored === 'en' || stored === 'ru') {
return stored
}
const browserLocale = window.navigator.language.toLowerCase()
return browserLocale.startsWith('ru') ? 'ru' : 'en'
}
const state = reactive({
locale: detectLocale() as Locale,
})
export function setLocale(locale: Locale) {
state.locale = locale
if (typeof window !== 'undefined') {
window.localStorage.setItem(STORAGE_KEY, locale)
}
}
export function useI18n() {
const locale = computed(() => state.locale)
function t(key: string): string {
return messages[state.locale][key] ?? messages.en[key] ?? key
}
return {
locale,
setLocale,
t,
}
}