Added frontend localization
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
@@ -165,8 +204,8 @@ function animationDelay(index: number, base = 0.1) {
|
||||
<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 & 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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user