mirror of
https://github.com/ManInDark/HabitTrove.git
synced 2026-01-20 22:24:28 +01:00
Merge Tag v0.2.12
This commit is contained in:
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"i18n-ally.localesPaths": [
|
||||
"i18n",
|
||||
"messages"
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## Version 0.2.12
|
||||
|
||||
### Added
|
||||
|
||||
* 🌍 Added multi-language support! Users can now select their preferred language in settings.
|
||||
* Supported languages: English, Español (Spanish), Deutsch (German), Français (French), Русский (Russian), 简体中文 (Simplified Chinese) and 日本語 (Japanese).
|
||||
|
||||
## Version 0.2.11
|
||||
|
||||
### Added
|
||||
|
||||
@@ -15,6 +15,7 @@ Want to try HabitTrove before installing? Visit the public [demo instance](https
|
||||
- 💰 Create a wishlist of rewards to redeem with earned coins
|
||||
- 📊 View your habit completion streaks and statistics
|
||||
- 📅 Calendar heatmap to visualize your progress (WIP)
|
||||
- 🌍 Multi-language support (English, Español, Deutsch, Français, Русский, 简体中文, 日本語)
|
||||
- 🌙 Dark mode support
|
||||
- 📲 Progressive Web App (PWA) support
|
||||
- 💾 Automatic daily backups with rotation
|
||||
|
||||
@@ -8,7 +8,8 @@ import { DM_Sans } from 'next/font/google'
|
||||
import { Suspense } from 'react'
|
||||
import { loadCoinsData, loadHabitsData, loadServerSettings, loadSettings, loadUsersData, loadWishlistData } from './actions/data'
|
||||
import './globals.css'
|
||||
|
||||
import { NextIntlClientProvider } from 'next-intl';
|
||||
import { getLocale, getMessages } from 'next-intl/server';
|
||||
|
||||
// Inter (clean, modern, excellent readability)
|
||||
// const inter = Inter({
|
||||
@@ -36,6 +37,11 @@ export default async function RootLayout({
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const locale = await getLocale();
|
||||
// Providing all messages to the client
|
||||
// side is the easiest way to get started
|
||||
const messages = await getMessages();
|
||||
|
||||
const [initialSettings, initialHabits, initialCoins, initialWishlist, initialUsers, initialServerSettings] = await Promise.all([
|
||||
loadSettings(),
|
||||
loadHabitsData(),
|
||||
@@ -47,7 +53,7 @@ export default async function RootLayout({
|
||||
|
||||
return (
|
||||
// set suppressHydrationWarning to true to prevent hydration errors when using ThemeProvider (https://ui.shadcn.com/docs/dark-mode/next)
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<html lang={locale} suppressHydrationWarning>
|
||||
<body className={activeFont.className}>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
@@ -78,18 +84,20 @@ export default async function RootLayout({
|
||||
serverSettings: initialServerSettings,
|
||||
}}
|
||||
>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<SessionProvider>
|
||||
<Layout>
|
||||
{children}
|
||||
</Layout>
|
||||
</SessionProvider>
|
||||
</ThemeProvider>
|
||||
<NextIntlClientProvider locale={locale} messages={messages}>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<SessionProvider>
|
||||
<Layout>
|
||||
{children}
|
||||
</Layout>
|
||||
</SessionProvider>
|
||||
</ThemeProvider>
|
||||
</NextIntlClientProvider>
|
||||
</JotaiHydrate>
|
||||
</Suspense>
|
||||
</JotaiProvider>
|
||||
|
||||
@@ -10,14 +10,18 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { settingsAtom } from '@/lib/atoms';
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
import { serverSettingsAtom, settingsAtom } from '@/lib/atoms';
|
||||
import { Settings, WeekDay } from '@/lib/types';
|
||||
import { useAtom } from 'jotai';
|
||||
import { Info } from 'lucide-react'; // Import Info icon
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { saveSettings } from '../actions/data';
|
||||
|
||||
export default function SettingsPage() {
|
||||
const t = useTranslations('SettingsPage');
|
||||
const [settings, setSettings] = useAtom(settingsAtom);
|
||||
const [serverSettings] = useAtom(serverSettingsAtom);
|
||||
|
||||
const updateSettings = async (newSettings: Settings) => {
|
||||
await saveSettings(newSettings)
|
||||
@@ -30,17 +34,17 @@ export default function SettingsPage() {
|
||||
return (
|
||||
<>
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<h1 className="text-3xl font-bold mb-6">Settings</h1>
|
||||
<h1 className="text-3xl font-bold mb-6">{t('title')}</h1>
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle>UI Settings</CardTitle>
|
||||
<CardTitle>{t('uiSettingsTitle')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="number-formatting">Number Formatting</Label>
|
||||
<Label htmlFor="number-formatting">{t('numberFormattingLabel')}</Label>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Format large numbers (e.g., 1K, 1M, 1B)
|
||||
{t('numberFormattingDescription')}
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
@@ -57,9 +61,9 @@ export default function SettingsPage() {
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="number-grouping">Number Grouping</Label>
|
||||
<Label htmlFor="number-grouping">{t('numberGroupingLabel')}</Label>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Use thousand separators (e.g., 1,000 vs 1000)
|
||||
{t('numberGroupingDescription')}
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
@@ -78,14 +82,14 @@ export default function SettingsPage() {
|
||||
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle>System Settings</CardTitle>
|
||||
<CardTitle>{t('systemSettingsTitle')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="timezone">Timezone</Label>
|
||||
<Label htmlFor="timezone">{t('timezoneLabel')}</Label>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Select your timezone for accurate date tracking
|
||||
{t('timezoneDescription')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
@@ -111,9 +115,9 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="timezone">Week Start Day</Label>
|
||||
<Label htmlFor="weekStartDay">{t('weekStartDayLabel')}</Label>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Select your preferred first day of the week
|
||||
{t('weekStartDayDescription')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
@@ -136,9 +140,9 @@ export default function SettingsPage() {
|
||||
['thursday', 4],
|
||||
['friday', 5],
|
||||
['saturday', 6]
|
||||
] as Array<[string, WeekDay]>).map(([dayName, dayNumber]) => (
|
||||
] as Array<["sunday" | "monday" | "tuesday" | "wednesday" | "thursday" | "friday" | "saturday", WeekDay]>).map(([dayName, dayNumber]) => (
|
||||
<option key={dayNumber} value={dayNumber}>
|
||||
{dayName.charAt(0).toUpperCase() + dayName.slice(1)}
|
||||
{t(`weekdays.${dayName}`)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -149,7 +153,7 @@ export default function SettingsPage() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label htmlFor="auto-backup">Auto Backup</Label>
|
||||
<Label htmlFor="auto-backup">{t('autoBackupLabel')}</Label>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
@@ -157,18 +161,14 @@ export default function SettingsPage() {
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" align="start">
|
||||
<p className="max-w-xs text-sm">
|
||||
When enabled, the application data (habits, coins, settings, etc.)
|
||||
will be automatically backed up daily around 2 AM server time.
|
||||
Backups are stored as ZIP files in the `backups/` directory
|
||||
at the project root. Only the last 7 backups are kept; older
|
||||
ones are automatically deleted.
|
||||
{t('autoBackupTooltip')}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Automatically back up data daily
|
||||
{t('autoBackupDescription')}
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
@@ -184,6 +184,48 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
{/* End of Auto Backup section */}
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Label htmlFor="language-select">{t('languageLabel')}</Label>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{t('languageDescription')}
|
||||
</div>
|
||||
{serverSettings.isDemo && (
|
||||
<div className="text-sm text-red-500">
|
||||
{t('languageDisabledInDemoTooltip')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<select
|
||||
id="language-select"
|
||||
value={settings.system.language}
|
||||
disabled={serverSettings.isDemo}
|
||||
onChange={(e) => {
|
||||
updateSettings({
|
||||
...settings,
|
||||
system: { ...settings.system, language: e.target.value }
|
||||
});
|
||||
toast({
|
||||
title: t('languageChangedTitle'),
|
||||
description: t('languageChangedDescription'),
|
||||
variant: 'default',
|
||||
});
|
||||
}}
|
||||
className={`w-[200px] rounded-md border border-input bg-background px-3 py-2 ${serverSettings.isDemo ? 'cursor-not-allowed opacity-50' : ''}`}
|
||||
>
|
||||
{/* Add more languages as needed */}
|
||||
<option value="en">English</option>
|
||||
<option value="es">Español</option>
|
||||
<option value="de">Deutsch</option>
|
||||
<option value="fr">Français</option>
|
||||
<option value="ru">Русский</option>
|
||||
<option value="zh">简体中文</option>
|
||||
<option value="ja">日本語</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div >
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Button } from "./ui/button"
|
||||
import { Star, History } from "lucide-react"
|
||||
import packageJson from '../package.json'
|
||||
import { DialogTitle } from "@radix-ui/react-dialog"
|
||||
import { useTranslations } from "next-intl"
|
||||
import { Logo } from "./Logo"
|
||||
import ChangelogModal from "./ChangelogModal"
|
||||
import { useState } from "react"
|
||||
@@ -15,6 +16,7 @@ interface AboutModalProps {
|
||||
}
|
||||
|
||||
export default function AboutModal({ isOpen, onClose }: AboutModalProps) {
|
||||
const t = useTranslations('AboutModal')
|
||||
const version = packageJson.version
|
||||
const [changelogOpen, setChangelogOpen] = useState(false)
|
||||
|
||||
@@ -22,7 +24,7 @@ export default function AboutModal({ isOpen, onClose }: AboutModalProps) {
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle aria-label="about"></DialogTitle>
|
||||
<DialogTitle aria-label={t('dialogArisLabel')}></DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-6 text-center py-4">
|
||||
<div>
|
||||
@@ -40,14 +42,14 @@ export default function AboutModal({ isOpen, onClose }: AboutModalProps) {
|
||||
onClick={() => setChangelogOpen(true)}
|
||||
>
|
||||
<History className="w-3 h-3 mr-1" />
|
||||
Changelog
|
||||
{t('changelogButton')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="text-sm">
|
||||
Created with ❤️ by{' '}
|
||||
{t('createdByPrefix')}{' '}
|
||||
<a
|
||||
href="https://github.com/dohsimpson"
|
||||
target="_blank"
|
||||
@@ -68,7 +70,7 @@ export default function AboutModal({ isOpen, onClose }: AboutModalProps) {
|
||||
>
|
||||
<Button variant="outline" size="sm">
|
||||
<Star className="w-4 h-4 mr-2" />
|
||||
Star on GitHub
|
||||
{t('starOnGitHubButton')}
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -17,6 +17,7 @@ import Picker from '@emoji-mart/react'
|
||||
import { useAtom } from 'jotai'
|
||||
import { SmilePlus, Zap } from 'lucide-react'
|
||||
import { DateTime } from 'luxon'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { useState } from 'react'
|
||||
import { RRule } from 'rrule'
|
||||
|
||||
@@ -28,6 +29,7 @@ interface AddEditHabitModalProps {
|
||||
}
|
||||
|
||||
export default function AddEditHabitModal({ onClose, onSave, habit, isTask }: AddEditHabitModalProps) {
|
||||
const t = useTranslations('AddEditHabitModal');
|
||||
const [settings] = useAtom(settingsAtom)
|
||||
const [name, setName] = useState(habit?.name || '')
|
||||
const [description, setDescription] = useState(habit?.description || '')
|
||||
@@ -90,13 +92,17 @@ export default function AddEditHabitModal({ onClose, onSave, habit, isTask }: Ad
|
||||
<Dialog open={true} onOpenChange={onClose}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{habit ? `Edit ${isTask ? 'Task' : 'Habit'}` : `Add New ${isTask ? 'Task' : 'Habit'}`}</DialogTitle>
|
||||
<DialogTitle>
|
||||
{habit
|
||||
? t(isTask ? 'editTaskTitle' : 'editHabitTitle')
|
||||
: t(isTask ? 'addNewTaskTitle' : 'addNewHabitTitle')}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="name" className="text-right">
|
||||
Name *
|
||||
{t('nameLabel')}
|
||||
</Label>
|
||||
<div className='flex col-span-3 gap-2'>
|
||||
<Input
|
||||
@@ -136,7 +142,7 @@ export default function AddEditHabitModal({ onClose, onSave, habit, isTask }: Ad
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="description" className="text-right">
|
||||
Description
|
||||
{t('descriptionLabel')}
|
||||
</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
@@ -147,7 +153,7 @@ export default function AddEditHabitModal({ onClose, onSave, habit, isTask }: Ad
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="recurrence" className="text-right">
|
||||
When *
|
||||
{t('whenLabel')}
|
||||
</Label>
|
||||
{/* date input (task) */}
|
||||
<div className="col-span-3 space-y-2">
|
||||
@@ -203,7 +209,7 @@ export default function AddEditHabitModal({ onClose, onSave, habit, isTask }: Ad
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<div className="flex items-center gap-2 justify-end">
|
||||
<Label htmlFor="targetCompletions">
|
||||
Complete
|
||||
{t('completeLabel')}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
@@ -237,7 +243,7 @@ export default function AddEditHabitModal({ onClose, onSave, habit, isTask }: Ad
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
times
|
||||
{t('timesSuffix')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -245,7 +251,7 @@ export default function AddEditHabitModal({ onClose, onSave, habit, isTask }: Ad
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<div className="flex items-center gap-2 justify-end">
|
||||
<Label htmlFor="coinReward">
|
||||
Reward
|
||||
{t('rewardLabel')}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
@@ -276,7 +282,7 @@ export default function AddEditHabitModal({ onClose, onSave, habit, isTask }: Ad
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
coins
|
||||
{t('coinsSuffix')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -284,7 +290,7 @@ export default function AddEditHabitModal({ onClose, onSave, habit, isTask }: Ad
|
||||
{users && users.length > 1 && (
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Label htmlFor="sharing-toggle">Share</Label>
|
||||
<Label htmlFor="sharing-toggle">{t('shareLabel')}</Label>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@@ -315,7 +321,11 @@ export default function AddEditHabitModal({ onClose, onSave, habit, isTask }: Ad
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="submit" disabled={errorMessage !== null}>{habit ? 'Save Changes' : `Add ${isTask ? 'Task' : 'Habit'}`}</Button>
|
||||
<Button type="submit">
|
||||
{habit
|
||||
? t('saveChangesButton')
|
||||
: t(isTask ? 'addTaskButton' : 'addHabitButton')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Input } from '@/components/ui/input'
|
||||
@@ -11,6 +12,7 @@ import data from '@emoji-mart/data'
|
||||
import Picker from '@emoji-mart/react'
|
||||
import { useAtom } from 'jotai'
|
||||
import { SmilePlus } from 'lucide-react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar'
|
||||
|
||||
@@ -31,6 +33,7 @@ export default function AddEditWishlistItemModal({
|
||||
addWishlistItem,
|
||||
editWishlistItem
|
||||
}: AddEditWishlistItemModalProps) {
|
||||
const t = useTranslations('AddEditWishlistItemModal')
|
||||
const [name, setName] = useState(editingItem?.name || '')
|
||||
const [description, setDescription] = useState(editingItem?.description || '')
|
||||
const [coinCost, setCoinCost] = useState(editingItem?.coinCost || 1)
|
||||
@@ -61,16 +64,16 @@ export default function AddEditWishlistItemModal({
|
||||
const validate = () => {
|
||||
const newErrors: { [key: string]: string } = {}
|
||||
if (!name.trim()) {
|
||||
newErrors.name = 'Name is required'
|
||||
newErrors.name = t('errorNameRequired')
|
||||
}
|
||||
if (coinCost < 1) {
|
||||
newErrors.coinCost = 'Coin cost must be at least 1'
|
||||
newErrors.coinCost = t('errorCoinCostMin')
|
||||
}
|
||||
if (targetCompletions !== undefined && targetCompletions < 1) {
|
||||
newErrors.targetCompletions = 'Target completions must be at least 1'
|
||||
newErrors.targetCompletions = t('errorTargetCompletionsMin')
|
||||
}
|
||||
if (link && !isValidUrl(link)) {
|
||||
newErrors.link = 'Please enter a valid URL'
|
||||
newErrors.link = t('errorInvalidUrl')
|
||||
}
|
||||
setErrors(newErrors)
|
||||
return Object.keys(newErrors).length === 0
|
||||
@@ -117,13 +120,13 @@ export default function AddEditWishlistItemModal({
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingItem ? 'Edit Reward' : 'Add New Reward'}</DialogTitle>
|
||||
<DialogTitle>{editingItem ? t('editTitle') : t('addTitle')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSave}>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="name" className="text-right">
|
||||
Name *
|
||||
{t('nameLabel')}
|
||||
</Label>
|
||||
<div className="col-span-3 flex gap-2">
|
||||
<Input
|
||||
@@ -160,7 +163,7 @@ export default function AddEditWishlistItemModal({
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="description" className="text-right">
|
||||
Description
|
||||
{t('descriptionLabel')}
|
||||
</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
@@ -172,7 +175,7 @@ export default function AddEditWishlistItemModal({
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<div className="flex items-center gap-2 justify-end">
|
||||
<Label htmlFor="coinReward">
|
||||
Cost
|
||||
{t('costLabel')}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
@@ -203,7 +206,7 @@ export default function AddEditWishlistItemModal({
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
coins
|
||||
{t('coinsSuffix')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -211,7 +214,7 @@ export default function AddEditWishlistItemModal({
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<div className="flex items-center gap-2 justify-end">
|
||||
<Label htmlFor="targetCompletions">
|
||||
Redeemable
|
||||
{t('redeemableLabel')}
|
||||
</Label>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
@@ -245,7 +248,7 @@ export default function AddEditWishlistItemModal({
|
||||
</button>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
times
|
||||
{t('timesSuffix')}
|
||||
</span>
|
||||
</div>
|
||||
{errors.targetCompletions && (
|
||||
@@ -257,7 +260,7 @@ export default function AddEditWishlistItemModal({
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="link" className="text-right">
|
||||
Link
|
||||
{t('linkLabel')}
|
||||
</Label>
|
||||
<div className="col-span-3">
|
||||
<Input
|
||||
@@ -278,7 +281,7 @@ export default function AddEditWishlistItemModal({
|
||||
{usersData.users && usersData.users.length > 1 && (
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Label htmlFor="sharing-toggle">Share</Label>
|
||||
<Label htmlFor="sharing-toggle">{t('shareLabel')}</Label>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@@ -309,7 +312,7 @@ export default function AddEditWishlistItemModal({
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="submit">{editingItem ? 'Save Changes' : 'Add Reward'}</Button>
|
||||
<Button type="submit">{editingItem ? t('saveButton') : t('addButton')}</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
|
||||
@@ -2,17 +2,19 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Coins } from 'lucide-react'
|
||||
import { FormattedNumber } from '@/components/FormattedNumber'
|
||||
import { useAtom } from 'jotai'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { settingsAtom } from '@/lib/atoms'
|
||||
import dynamic from 'next/dynamic'
|
||||
|
||||
const TodayEarnedCoins = dynamic(() => import('./TodayEarnedCoins'), { ssr: false })
|
||||
|
||||
export default function CoinBalance({ coinBalance }: { coinBalance: number }) {
|
||||
const t = useTranslations('CoinBalance');
|
||||
const [settings] = useAtom(settingsAtom)
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Coin Balance</CardTitle>
|
||||
<CardTitle>{t('coinBalanceTitle')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-center">
|
||||
|
||||
@@ -8,9 +8,11 @@ import { Input } from '@/components/ui/input'
|
||||
import { useCoins } from '@/hooks/useCoins'
|
||||
import { settingsAtom, usersAtom } from '@/lib/atoms'
|
||||
import { useHelpers } from '@/lib/client-helpers'
|
||||
import { TransactionType } from '@/lib/types'
|
||||
import { d2s, t2d } from '@/lib/utils'
|
||||
import { useAtom } from 'jotai'
|
||||
import { History } from 'lucide-react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import Link from 'next/link'
|
||||
import { useSearchParams } from 'next/navigation'; // Import useSearchParams
|
||||
import { useEffect, useRef, useState } from 'react'; // Import useEffect, useRef
|
||||
@@ -18,6 +20,7 @@ import EmptyState from './EmptyState'
|
||||
import { TransactionNoteEditor } from './TransactionNoteEditor'
|
||||
|
||||
export default function CoinsManager() {
|
||||
const t = useTranslations('CoinsManager')
|
||||
const { currentUser } = useHelpers()
|
||||
const [selectedUser, setSelectedUser] = useState<string>()
|
||||
const {
|
||||
@@ -31,7 +34,7 @@ export default function CoinsManager() {
|
||||
totalSpent,
|
||||
coinsSpentToday,
|
||||
transactionsToday
|
||||
} = useCoins({selectedUser})
|
||||
} = useCoins({ selectedUser })
|
||||
const [settings] = useAtom(settingsAtom)
|
||||
const [usersData] = useAtom(usersAtom)
|
||||
const DEFAULT_AMOUNT = '0'
|
||||
@@ -87,10 +90,21 @@ export default function CoinsManager() {
|
||||
}
|
||||
}
|
||||
|
||||
const getTransactionTypeLabel = (type: TransactionType) => {
|
||||
switch (type) {
|
||||
case 'HABIT_COMPLETION': return t('transactionTypeHabitCompletion');
|
||||
case 'TASK_COMPLETION': return t('transactionTypeTaskCompletion');
|
||||
case 'HABIT_UNDO': return t('transactionTypeHabitUndo');
|
||||
case 'TASK_UNDO': return t('transactionTypeTaskUndo');
|
||||
case 'WISH_REDEMPTION': return t('transactionTypeWishRedemption');
|
||||
case 'MANUAL_ADJUSTMENT': return t('transactionTypeManualAdjustment');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h1 className="text-3xl font-bold mr-6">Coins Management</h1>
|
||||
<h1 className="text-3xl font-bold mr-6">{t('title')}</h1>
|
||||
{currentUser?.isAdmin && (
|
||||
<select
|
||||
className="border rounded p-2"
|
||||
@@ -112,8 +126,8 @@ export default function CoinsManager() {
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<span className="text-2xl animate-bounce hover:animate-none cursor-default">💰</span>
|
||||
<div>
|
||||
<div className="text-sm font-normal text-muted-foreground">Current Balance</div>
|
||||
<div className="text-3xl font-bold"><FormattedNumber amount={balance} settings={settings} /> coins</div>
|
||||
<div className="text-sm font-normal text-muted-foreground">{t('currentBalanceLabel')}</div>
|
||||
<div className="text-3xl font-bold"><FormattedNumber amount={balance} settings={settings} /> {t('coinsSuffix')}</div>
|
||||
</div>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -158,7 +172,7 @@ export default function CoinsManager() {
|
||||
variant="default"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{Number(amount) >= 0 ? 'Add Coins' : 'Remove Coins'}
|
||||
{Number(amount) >= 0 ? t('addCoinsButton') : t('removeCoinsButton')}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
@@ -170,27 +184,27 @@ export default function CoinsManager() {
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Statistics</CardTitle>
|
||||
<CardTitle>{t('statisticsTitle')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 xl:grid-cols-3 gap-4">
|
||||
{/* Top Row - Totals */}
|
||||
<div className="p-4 rounded-lg bg-green-100 dark:bg-green-900">
|
||||
<div className="text-sm text-green-800 dark:text-green-100 mb-1">Total Earned</div>
|
||||
<div className="text-sm text-green-800 dark:text-green-100 mb-1">{t('totalEarnedLabel')}</div>
|
||||
<div className="text-2xl font-bold text-green-900 dark:text-green-50">
|
||||
<FormattedNumber amount={totalEarned} settings={settings} /> 🪙
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-lg bg-red-100 dark:bg-red-900">
|
||||
<div className="text-sm text-red-800 dark:text-red-100 mb-1">Total Spent</div>
|
||||
<div className="text-sm text-red-800 dark:text-red-100 mb-1">{t('totalSpentLabel')}</div>
|
||||
<div className="text-2xl font-bold text-red-900 dark:text-red-50">
|
||||
<FormattedNumber amount={totalSpent} settings={settings} /> 💸
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-lg bg-pink-100 dark:bg-pink-900">
|
||||
<div className="text-sm text-pink-800 dark:text-pink-100 mb-1">Total Transactions</div>
|
||||
<div className="text-sm text-pink-800 dark:text-pink-100 mb-1">{t('totalTransactionsLabel')}</div>
|
||||
<div className="text-2xl font-bold text-pink-900 dark:text-pink-50">
|
||||
{transactions.length} 📈
|
||||
</div>
|
||||
@@ -198,21 +212,21 @@ export default function CoinsManager() {
|
||||
|
||||
{/* Bottom Row - Today */}
|
||||
<div className="p-4 rounded-lg bg-blue-100 dark:bg-blue-900">
|
||||
<div className="text-sm text-blue-800 dark:text-blue-100 mb-1">Today's Earned</div>
|
||||
<div className="text-sm text-blue-800 dark:text-blue-100 mb-1">{t('todaysEarnedLabel')}</div>
|
||||
<div className="text-2xl font-bold text-blue-900 dark:text-blue-50">
|
||||
<FormattedNumber amount={coinsEarnedToday} settings={settings} /> 🪙
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-lg bg-purple-100 dark:bg-purple-900">
|
||||
<div className="text-sm text-purple-800 dark:text-purple-100 mb-1">Today's Spent</div>
|
||||
<div className="text-sm text-purple-800 dark:text-purple-100 mb-1">{t('todaysSpentLabel')}</div>
|
||||
<div className="text-2xl font-bold text-purple-900 dark:text-purple-50">
|
||||
<FormattedNumber amount={coinsSpentToday} settings={settings} /> 💸
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4 rounded-lg bg-orange-100 dark:bg-orange-900">
|
||||
<div className="text-sm text-orange-800 dark:text-orange-100 mb-1">Today's Transactions</div>
|
||||
<div className="text-sm text-orange-800 dark:text-orange-100 mb-1">{t('todaysTransactionsLabel')}</div>
|
||||
<div className="text-2xl font-bold text-orange-900 dark:text-orange-50">
|
||||
{transactionsToday} 📊
|
||||
</div>
|
||||
@@ -223,13 +237,13 @@ export default function CoinsManager() {
|
||||
|
||||
<Card className="md:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle>Transaction History</CardTitle>
|
||||
<CardTitle>{t('transactionHistoryTitle')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">Show:</span>
|
||||
<span className="text-sm text-muted-foreground">{t('showLabel')}</span>
|
||||
<select
|
||||
className="border rounded p-1"
|
||||
value={pageSize}
|
||||
@@ -240,18 +254,18 @@ export default function CoinsManager() {
|
||||
>
|
||||
{PAGE_ENTRY_COUNTS.map(n => <option key={n} value={n}>{n}</option>)}
|
||||
</select>
|
||||
<span className="text-sm text-muted-foreground">entries</span>
|
||||
<span className="text-sm text-muted-foreground">{t('entriesSuffix')}</span>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Showing {Math.min((currentPage - 1) * pageSize + 1, transactions.length)} to {Math.min(currentPage * pageSize, transactions.length)} of {transactions.length} entries
|
||||
{t('showingEntries', { from: Math.min((currentPage - 1) * pageSize + 1, transactions.length), to: Math.min(currentPage * pageSize, transactions.length), total: transactions.length })}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{transactions.length === 0 ? (
|
||||
<EmptyState
|
||||
icon={History}
|
||||
title="No transactions yet"
|
||||
description="Your transaction history will appear here once you start earning or spending coins"
|
||||
title={t('noTransactionsTitle')}
|
||||
description={t('noTransactionsDescription')}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
@@ -279,9 +293,8 @@ export default function CoinsManager() {
|
||||
<div
|
||||
key={transaction.id}
|
||||
ref={(el) => { transactionRefs.current[transaction.id] = el; }} // Assign ref correctly
|
||||
className={`flex justify-between items-center p-3 border rounded hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors ${
|
||||
isHighlighted ? 'ring-2 ring-blue-500 bg-blue-50 dark:bg-blue-900/30' : '' // Apply highlight styles
|
||||
}`}
|
||||
className={`flex justify-between items-center p-3 border rounded hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors ${isHighlighted ? 'ring-2 ring-blue-500 bg-blue-50 dark:bg-blue-900/30' : '' // Apply highlight styles
|
||||
}`}
|
||||
>
|
||||
<div className="space-y-1 flex-grow mr-4"> {/* Added flex-grow and margin */}
|
||||
<div className="flex items-center gap-2 flex-wrap"> {/* Added flex-wrap */}
|
||||
@@ -299,7 +312,7 @@ export default function CoinsManager() {
|
||||
<span
|
||||
className={`text-xs px-2 py-1 rounded-full ${getBadgeStyles()}`}
|
||||
>
|
||||
{transaction.type.split('_').join(' ')}
|
||||
{getTransactionTypeLabel(transaction.type as TransactionType)}
|
||||
</span>
|
||||
{transaction.userId && currentUser?.isAdmin && (
|
||||
<Avatar className="h-6 w-6">
|
||||
@@ -357,9 +370,9 @@ export default function CoinsManager() {
|
||||
‹
|
||||
</Button>
|
||||
<div className="flex items-center gap-1 px-4 py-2 rounded-md bg-muted">
|
||||
<span className="text-sm font-medium">Page</span>
|
||||
<span className="text-sm font-medium">{t('pageLabel')}</span>
|
||||
<span className="text-sm font-bold">{currentPage}</span>
|
||||
<span className="text-sm font-medium">of</span>
|
||||
<span className="text-sm font-medium">{t('ofLabel')}</span>
|
||||
<span className="text-sm font-bold">{Math.ceil(transactions.length / pageSize)}</span>
|
||||
</div>
|
||||
<Button
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Badge } from "@/components/ui/badge"
|
||||
import { completedHabitsMapAtom, habitsByDateFamily, settingsAtom } from '@/lib/atoms'
|
||||
import { getTodayInTimezone } from '@/lib/utils'
|
||||
import { useAtom } from 'jotai'
|
||||
import { useTranslations } from 'next-intl'
|
||||
|
||||
interface CompletionCountBadgeProps {
|
||||
type: 'habits' | 'tasks'
|
||||
@@ -12,6 +13,7 @@ export default function CompletionCountBadge({
|
||||
type,
|
||||
date
|
||||
}: CompletionCountBadgeProps) {
|
||||
const t = useTranslations('CompletionCountBadge');
|
||||
const [settings] = useAtom(settingsAtom)
|
||||
const [completedHabitsMap] = useAtom(completedHabitsMapAtom)
|
||||
const targetDate = date || getTodayInTimezone(settings.system.timezone)
|
||||
@@ -27,7 +29,7 @@ export default function CompletionCountBadge({
|
||||
|
||||
return (
|
||||
<Badge variant="secondary">
|
||||
{`${completedCount}/${totalCount} Completed`}
|
||||
{t('countCompleted', { completedCount, totalCount })}
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useTranslations } from 'next-intl'
|
||||
|
||||
interface ConfirmDialogProps {
|
||||
isOpen: boolean
|
||||
@@ -23,9 +24,13 @@ export default function ConfirmDialog({
|
||||
onConfirm,
|
||||
title,
|
||||
message,
|
||||
confirmText = "Confirm",
|
||||
cancelText = "Cancel"
|
||||
confirmText,
|
||||
cancelText,
|
||||
}: ConfirmDialogProps) {
|
||||
const t = useTranslations('ConfirmDialog');
|
||||
const finalConfirmText = confirmText || t('confirmButton');
|
||||
const finalCancelText = cancelText || t('cancelButton');
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent>
|
||||
@@ -37,10 +42,10 @@ export default function ConfirmDialog({
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
{cancelText}
|
||||
{finalCancelText}
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={onConfirm}>
|
||||
{confirmText}
|
||||
{finalConfirmText}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Habit, WishlistItemType } from '@/lib/types'
|
||||
import { cn, d2t, getNow, getTodayInTimezone, isHabitDue, isSameDate, isTaskOverdue, t2d } from '@/lib/utils'
|
||||
import { useAtom } from 'jotai'
|
||||
import { AlertTriangle, ArrowRight, ChevronDown, ChevronUp, Circle, CircleCheck, Coins, Pin, Plus } from 'lucide-react'; // Removed unused icons
|
||||
import { useTranslations } from 'next-intl'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import AddEditHabitModal from './AddEditHabitModal'
|
||||
@@ -50,6 +51,7 @@ const ItemSection = ({
|
||||
viewLink,
|
||||
addNewItem,
|
||||
}: ItemSectionProps) => {
|
||||
const t = useTranslations('DailyOverview');
|
||||
const { completeHabit, undoComplete, saveHabit, deleteHabit, archiveHabit, habitFreqMap } = useHabits();
|
||||
const [_, setPomo] = useAtom(pomodoroAtom);
|
||||
const [browserSettings, setBrowserSettings] = useAtom(browserSettingsAtom);
|
||||
@@ -101,7 +103,7 @@ const ItemSection = ({
|
||||
onClick={addNewItem}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
<span className="sr-only">Add {isTask ? "Task" : "Habit"}</span>
|
||||
<span className="sr-only">{t(isTask ? 'addTaskButtonLabel' : 'addHabitButtonLabel')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-center text-muted-foreground text-sm py-4">
|
||||
@@ -126,7 +128,7 @@ const ItemSection = ({
|
||||
onClick={addNewItem}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
<span className="sr-only">Add {isTask ? "Task" : "Habit"}</span>
|
||||
<span className="sr-only">{t(isTask ? 'addTaskButtonLabel' : 'addHabitButtonLabel')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -229,7 +231,7 @@ const ItemSection = ({
|
||||
<AlertTriangle className="h-4 w-4 flex-shrink-0 text-red-600 dark:text-red-500" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Overdue</p>
|
||||
<p>{t('overdueTooltip')}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
@@ -295,12 +297,12 @@ const ItemSection = ({
|
||||
>
|
||||
{currentExpanded ? (
|
||||
<>
|
||||
Show less
|
||||
{t('showLessButton')}
|
||||
<ChevronUp className="h-3 w-3" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Show all
|
||||
{t('showAllButton')}
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
</>
|
||||
)}
|
||||
@@ -343,6 +345,7 @@ export default function DailyOverview({
|
||||
wishlistItems,
|
||||
coinBalance,
|
||||
}: UpcomingItemsProps) {
|
||||
const t = useTranslations('DailyOverview');
|
||||
const { completeHabit, undoComplete } = useHabits()
|
||||
const [settings] = useAtom(settingsAtom)
|
||||
const [completedHabitsMap] = useAtom(completedHabitsMapAtom)
|
||||
@@ -396,16 +399,16 @@ export default function DailyOverview({
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Today's Overview</CardTitle>
|
||||
<CardTitle>{t('todaysOverviewTitle')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-6">
|
||||
{/* Tasks Section */}
|
||||
{hasTasks && (
|
||||
<ItemSection
|
||||
title="Daily Tasks"
|
||||
title={t('dailyTasksTitle')}
|
||||
items={dailyTasks}
|
||||
emptyMessage="No tasks due today. Add some tasks to get started!"
|
||||
emptyMessage={t('noTasksDueTodayMessage')}
|
||||
isTask={true}
|
||||
viewLink="/habits?view=tasks"
|
||||
addNewItem={() => setModalConfig({ isOpen: true, isTask: true })}
|
||||
@@ -414,9 +417,9 @@ export default function DailyOverview({
|
||||
|
||||
{/* Habits Section */}
|
||||
<ItemSection
|
||||
title="Daily Habits"
|
||||
title={t('dailyHabitsTitle')}
|
||||
items={dailyHabits}
|
||||
emptyMessage="No habits due today. Add some habits to get started!"
|
||||
emptyMessage={t('noHabitsDueTodayMessage')}
|
||||
isTask={false}
|
||||
viewLink="/habits"
|
||||
addNewItem={() => setModalConfig({ isOpen: true, isTask: false })}
|
||||
@@ -424,16 +427,19 @@ export default function DailyOverview({
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 className="font-semibold">Wishlist Goals</h3>
|
||||
<h3 className="font-semibold">{t('wishlistGoalsTitle')}</h3>
|
||||
<Badge variant="secondary">
|
||||
{wishlistItems.filter(item => item.coinCost <= coinBalance).length}/{wishlistItems.length} Redeemable
|
||||
{t('redeemableBadgeLabel', {
|
||||
count: wishlistItems.filter(item => item.coinCost <= coinBalance).length,
|
||||
total: wishlistItems.length
|
||||
})}
|
||||
</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<div className={`space-y-3 transition-all duration-300 ease-in-out ${browserSettings.expandedWishlist ? 'max-h-none' : 'max-h-[200px]'} overflow-hidden`}>
|
||||
{sortedWishlistItems.length === 0 ? (
|
||||
<div className="text-center text-muted-foreground text-sm py-4">
|
||||
No wishlist items yet. Add some goals to work towards!
|
||||
{t('noWishlistItemsMessage')}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
@@ -480,8 +486,8 @@ export default function DailyOverview({
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
{isRedeemable
|
||||
? "Ready to redeem!"
|
||||
: `${item.coinCost - coinBalance} coins to go`
|
||||
? t('readyToRedeemMessage')
|
||||
: t('coinsToGoMessage', { amount: item.coinCost - coinBalance })
|
||||
}
|
||||
</p>
|
||||
</Link>
|
||||
@@ -497,12 +503,12 @@ export default function DailyOverview({
|
||||
>
|
||||
{browserSettings.expandedWishlist ? (
|
||||
<>
|
||||
Show less
|
||||
{t('showLessButton')}
|
||||
<ChevronUp className="h-3 w-3" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Show all
|
||||
{t('showAllButton')}
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
</>
|
||||
)}
|
||||
@@ -511,7 +517,7 @@ export default function DailyOverview({
|
||||
href="/wishlist"
|
||||
className="text-sm text-muted-foreground hover:text-primary flex items-center gap-1"
|
||||
>
|
||||
View
|
||||
{t('viewButton')}
|
||||
<ArrowRight className="h-3 w-3" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
import { useCoins } from '@/hooks/useCoins'
|
||||
import { habitsAtom, wishlistAtom } from '@/lib/atoms'
|
||||
import { useAtom } from 'jotai'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import CoinBalance from './CoinBalance'
|
||||
import DailyOverview from './DailyOverview'
|
||||
import HabitStreak from './HabitStreak'
|
||||
|
||||
export default function Dashboard() {
|
||||
const t = useTranslations('Dashboard');
|
||||
const [habitsData] = useAtom(habitsAtom)
|
||||
const habits = habitsData.habits
|
||||
const { balance } = useCoins()
|
||||
@@ -17,7 +19,7 @@ export default function Dashboard() {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-3xl font-bold">Dashboard</h1>
|
||||
<h1 className="text-3xl font-bold">{t('title')}</h1>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<CoinBalance coinBalance={balance} />
|
||||
|
||||
@@ -10,19 +10,21 @@ import { d2s, getCompletionsForDate, getISODate, getNow, isHabitDue } from '@/li
|
||||
import { useAtom } from 'jotai'
|
||||
import { Circle, CircleCheck } from 'lucide-react'
|
||||
import { DateTime } from 'luxon'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import Linkify from './linkify'
|
||||
|
||||
export default function HabitCalendar() {
|
||||
const t = useTranslations('HabitCalendar')
|
||||
const { completePastHabit } = useHabits()
|
||||
|
||||
const handleCompletePastHabit = useCallback(async (habit: Habit, date: DateTime) => {
|
||||
try {
|
||||
await completePastHabit(habit, date)
|
||||
} catch (error) {
|
||||
console.error('Error completing past habit:', error)
|
||||
console.error(t('errorCompletingPastHabit'), error)
|
||||
}
|
||||
}, [completePastHabit])
|
||||
}, [completePastHabit, t])
|
||||
const [settings] = useAtom(settingsAtom)
|
||||
const [selectedDateTime, setSelectedDateTime] = useState<DateTime>(getNow({ timezone: settings.system.timezone }))
|
||||
const selectedDate = selectedDateTime.toFormat("yyyy-MM-dd")
|
||||
@@ -41,11 +43,11 @@ export default function HabitCalendar() {
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<h1 className="text-3xl font-bold mb-6">Habit Calendar</h1>
|
||||
<h1 className="text-3xl font-bold mb-6">{t('title')}</h1>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Calendar</CardTitle>
|
||||
<CardTitle>{t('calendarCardTitle')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Calendar
|
||||
@@ -74,7 +76,7 @@ export default function HabitCalendar() {
|
||||
{selectedDateTime ? (
|
||||
<>{d2s({ dateTime: selectedDateTime, timezone: settings.system.timezone, format: DateTime.DATE_MED_WITH_WEEKDAY })}</>
|
||||
) : (
|
||||
'Select a date'
|
||||
t('selectDatePrompt')
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -84,7 +86,7 @@ export default function HabitCalendar() {
|
||||
{hasTasks && (
|
||||
<div className="pt-2 border-t">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="font-medium text-sm text-muted-foreground uppercase tracking-wide">Tasks</h3>
|
||||
<h3 className="font-medium text-sm text-muted-foreground uppercase tracking-wide">{t('tasksSectionTitle')}</h3>
|
||||
<CompletionCountBadge type="tasks" date={selectedDate.toString()} />
|
||||
</div>
|
||||
<ul className="space-y-3">
|
||||
@@ -143,7 +145,7 @@ export default function HabitCalendar() {
|
||||
)}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="font-medium text-sm text-muted-foreground uppercase tracking-wide">Habits</h3>
|
||||
<h3 className="font-medium text-sm text-muted-foreground uppercase tracking-wide">{t('habitsSectionTitle')}</h3>
|
||||
<CompletionCountBadge type="habits" date={selectedDate.toString()} />
|
||||
</div>
|
||||
<ul className="space-y-3">
|
||||
|
||||
@@ -7,6 +7,7 @@ import { DropdownMenuItem, DropdownMenuSeparator } from '@/components/ui/dropdow
|
||||
import { ContextMenuItem, ContextMenuSeparator } from '@/components/ui/context-menu';
|
||||
import { Timer, Calendar, Pin, Edit, Archive, ArchiveRestore, Trash2 } from 'lucide-react';
|
||||
import { useHelpers } from '@/lib/client-helpers'; // For permission checks if needed, though useHabits handles most
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
interface HabitContextMenuItemsProps {
|
||||
habit: Habit;
|
||||
@@ -23,6 +24,7 @@ export function HabitContextMenuItems({
|
||||
context = 'habit-item',
|
||||
onClose,
|
||||
}: HabitContextMenuItemsProps) {
|
||||
const t = useTranslations('HabitContextMenuItems');
|
||||
const { saveHabit, archiveHabit, unarchiveHabit } = useHabits();
|
||||
const [settings] = useAtom(settingsAtom);
|
||||
const [, setPomo] = useAtom(pomodoroAtom);
|
||||
@@ -55,7 +57,7 @@ export function HabitContextMenuItems({
|
||||
})}
|
||||
>
|
||||
<Timer className="mr-2 h-4 w-4" />
|
||||
<span>Start Pomodoro</span>
|
||||
<span>{t('startPomodoro')}</span>
|
||||
</MenuItemComponent>
|
||||
)}
|
||||
|
||||
@@ -69,7 +71,7 @@ export function HabitContextMenuItems({
|
||||
})}
|
||||
>
|
||||
<Calendar className="mr-2 h-4 w-4" />
|
||||
<span>Move to Today</span>
|
||||
<span>{t('moveToToday')}</span>
|
||||
</MenuItemComponent>
|
||||
)}
|
||||
|
||||
@@ -83,7 +85,7 @@ export function HabitContextMenuItems({
|
||||
})}
|
||||
>
|
||||
<Calendar className="mr-2 h-4 w-4" />
|
||||
<span>Move to Tomorrow</span>
|
||||
<span>{t('moveToTomorrow')}</span>
|
||||
</MenuItemComponent>
|
||||
)}
|
||||
|
||||
@@ -93,7 +95,7 @@ export function HabitContextMenuItems({
|
||||
onClick={() => handleAction(() => saveHabit({ ...habit, pinned: !habit.pinned }))}
|
||||
>
|
||||
<Pin className="mr-2 h-4 w-4" />
|
||||
<span>{habit.pinned ? 'Unpin' : 'Pin'}</span>
|
||||
<span>{t(habit.pinned ? 'unpin' : 'pin')}</span>
|
||||
</MenuItemComponent>
|
||||
)}
|
||||
|
||||
@@ -104,7 +106,7 @@ export function HabitContextMenuItems({
|
||||
disabled={!canWrite}
|
||||
>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
<span>Edit</span>
|
||||
<span>{t('edit')}</span>
|
||||
</MenuItemComponent>
|
||||
)}
|
||||
|
||||
@@ -114,7 +116,7 @@ export function HabitContextMenuItems({
|
||||
disabled={!canWrite}
|
||||
>
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
<span>Edit</span>
|
||||
<span>{t('edit')}</span>
|
||||
</MenuItemComponent>
|
||||
)}
|
||||
|
||||
@@ -125,7 +127,7 @@ export function HabitContextMenuItems({
|
||||
onClick={() => handleAction(() => archiveHabit(habit.id))}
|
||||
>
|
||||
<Archive className="mr-2 h-4 w-4" />
|
||||
<span>Archive</span>
|
||||
<span>{t('archive')}</span>
|
||||
</MenuItemComponent>
|
||||
)}
|
||||
|
||||
@@ -135,7 +137,7 @@ export function HabitContextMenuItems({
|
||||
onClick={() => handleAction(() => unarchiveHabit(habit.id))}
|
||||
>
|
||||
<ArchiveRestore className="mr-2 h-4 w-4" />
|
||||
<span>Unarchive</span>
|
||||
<span>{t('unarchive')}</span>
|
||||
</MenuItemComponent>
|
||||
)}
|
||||
|
||||
@@ -150,7 +152,7 @@ export function HabitContextMenuItems({
|
||||
disabled={!canWrite} // Assuming delete is a write operation
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
<span>Delete</span>
|
||||
<span>{t('delete')}</span>
|
||||
</MenuItemComponent>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -12,10 +12,11 @@ import { Habit, User } from '@/lib/types'
|
||||
import { convertMachineReadableFrequencyToHumanReadable, getCompletionsForToday, isTaskOverdue } from '@/lib/utils'
|
||||
import { useAtom } from 'jotai'
|
||||
import { Check, Coins, Edit, MoreVertical, Pin, Undo2 } from 'lucide-react'; // Removed unused icons
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { HabitContextMenuItems } from './HabitContextMenuItems'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar'
|
||||
import { usePathname } from 'next/navigation'
|
||||
|
||||
interface HabitItemProps {
|
||||
habit: Habit
|
||||
@@ -50,6 +51,7 @@ export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) {
|
||||
const target = habit.targetCompletions || 1
|
||||
const isCompletedToday = completionsToday >= target
|
||||
const [isHighlighted, setIsHighlighted] = useState(false)
|
||||
const t = useTranslations('HabitItem');
|
||||
const [usersData] = useAtom(usersAtom)
|
||||
const { currentUser, hasPermission } = useHelpers()
|
||||
const canWrite = hasPermission('habit', 'write')
|
||||
@@ -91,7 +93,7 @@ export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) {
|
||||
</div>
|
||||
{isTaskOverdue(habit, settings.system.timezone) && (
|
||||
<span className="ml-2 inline-flex items-center rounded-md bg-red-50 dark:bg-red-900/30 px-2 py-1 text-xs font-medium text-red-700 dark:text-red-400 ring-1 ring-inset ring-red-600/10 dark:ring-red-500/20">
|
||||
Overdue
|
||||
{t('overdue')}
|
||||
</span>
|
||||
)}
|
||||
</CardTitle>
|
||||
@@ -105,15 +107,15 @@ export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) {
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1">
|
||||
<p className={`text-sm ${habit.archived ? 'text-gray-400 dark:text-gray-500' : 'text-gray-500'}`}>
|
||||
When: {convertMachineReadableFrequencyToHumanReadable({
|
||||
{t('whenLabel', { frequency: convertMachineReadableFrequencyToHumanReadable({
|
||||
frequency: habit.frequency,
|
||||
isRecurRule: pathname.includes("habits"),
|
||||
timezone: settings.system.timezone
|
||||
})}
|
||||
})})}
|
||||
</p>
|
||||
<div className="flex items-center mt-2">
|
||||
<Coins className={`h-4 w-4 mr-1 ${habit.archived ? 'text-gray-400 dark:text-gray-500' : 'text-yellow-400'}`} />
|
||||
<span className={`text-sm font-medium ${habit.archived ? 'text-gray-400 dark:text-gray-500' : ''}`}>{habit.coinReward} coins per completion</span>
|
||||
<span className={`text-sm font-medium ${habit.archived ? 'text-gray-400 dark:text-gray-500' : ''}`}>{t('coinsPerCompletion', { count: habit.coinReward })}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-between gap-2">
|
||||
@@ -131,19 +133,19 @@ export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) {
|
||||
{isCompletedToday ? (
|
||||
target > 1 ? (
|
||||
<>
|
||||
<span className="sm:hidden">{completionsToday}/{target}</span>
|
||||
<span className="hidden sm:inline">Completed ({completionsToday}/{target})</span>
|
||||
<span className="sm:hidden">{t('completedStatusCountMobile', { completed: completionsToday, target })}</span>
|
||||
<span className="hidden sm:inline">{t('completedStatusCount', { completed: completionsToday, target })}</span>
|
||||
</>
|
||||
) : (
|
||||
'Completed'
|
||||
t('completedStatus')
|
||||
)
|
||||
) : (
|
||||
target > 1 ? (
|
||||
<>
|
||||
<span className="sm:hidden">{completionsToday}/{target}</span>
|
||||
<span className="hidden sm:inline">Complete ({completionsToday}/{target})</span>
|
||||
<span className="sm:hidden">{t('completeButtonCountMobile', { completed: completionsToday, target })}</span>
|
||||
<span className="hidden sm:inline">{t('completeButtonCount', { completed: completionsToday, target })}</span>
|
||||
</>
|
||||
) : 'Complete'
|
||||
) : t('completeButton')
|
||||
)}
|
||||
</span>
|
||||
{habit.targetCompletions && habit.targetCompletions > 1 && (
|
||||
@@ -165,7 +167,7 @@ export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) {
|
||||
className="w-10 sm:w-auto"
|
||||
>
|
||||
<Undo2 className="h-4 w-4" />
|
||||
<span className="hidden sm:inline ml-2">Undo</span>
|
||||
<span className="hidden sm:inline ml-2">{t('undoButton')}</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -179,7 +181,7 @@ export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) {
|
||||
className="hidden sm:flex"
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
<span className="ml-2">Edit</span>
|
||||
<span className="ml-2">{t('editButton')}</span>
|
||||
</Button>
|
||||
)}
|
||||
<DropdownMenu>
|
||||
|
||||
@@ -12,14 +12,15 @@ import { getHabitFreq } from '@/lib/utils'; // Added
|
||||
import { useAtom } from 'jotai'
|
||||
import { ArrowDownWideNarrow, ArrowUpNarrowWide, Plus, Search } from 'lucide-react'; // Added sort icons, Search icon
|
||||
import { DateTime } from 'luxon'; // Added
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { useEffect, useMemo, useState } from 'react'; // Added useMemo, useEffect
|
||||
import AddEditHabitModal from './AddEditHabitModal'
|
||||
import ConfirmDialog from './ConfirmDialog'
|
||||
import EmptyState from './EmptyState'
|
||||
import HabitItem from './HabitItem'
|
||||
import { ViewToggle } from './ViewToggle'
|
||||
|
||||
export default function HabitList({ isTasksView }: { isTasksView: boolean}) {
|
||||
const t = useTranslations('HabitList');
|
||||
const { saveHabit, deleteHabit } = useHabits()
|
||||
const [habitsData] = useAtom(habitsAtom) // setHabitsData removed as it's not used
|
||||
|
||||
@@ -121,10 +122,10 @@ export default function HabitList({ isTasksView }: { isTasksView: boolean}) {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-3xl font-bold">{`My ${isTasksView ? "Tasks" : "Habits"}`}</h1>
|
||||
<h1 className="text-3xl font-bold">{t(isTasksView ? "myTasks" : "myHabits")}</h1>
|
||||
<span>
|
||||
<Button onClick={() => setModalConfig({ isOpen: true, isTask: isTasksView })}>
|
||||
<Plus className='mr-2 h-4 w-4' />{`Add ${isTasksView ? "Task" : "Habit"}`}
|
||||
<Plus className='mr-2 h-4 w-4' />{isTasksView ? t("addTaskButton") : t("addHabitButton")}
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
@@ -137,28 +138,28 @@ export default function HabitList({ isTasksView }: { isTasksView: boolean}) {
|
||||
</div>
|
||||
<Input
|
||||
type="search"
|
||||
placeholder={`Search ${isTasksView ? 'tasks' : 'habits'}...`}
|
||||
placeholder={t(isTasksView ? 'searchTasksPlaceholder' : 'searchHabitsPlaceholder')}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10 w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 self-start sm:self-center w-full sm:w-auto">
|
||||
<Label htmlFor="sort-by" className="text-sm font-medium whitespace-nowrap sr-only sm:not-sr-only">Sort by:</Label>
|
||||
<Label htmlFor="sort-by" className="text-sm font-medium whitespace-nowrap sr-only sm:not-sr-only">{t('sortByLabel')}</Label>
|
||||
<Select value={sortBy} onValueChange={(value) => setSortBy(value as SortableField)}>
|
||||
<SelectTrigger id="sort-by" className="w-full sm:w-[180px]">
|
||||
<SelectValue placeholder="Sort by" />
|
||||
<SelectValue placeholder={t('sortByLabel')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="name">Name</SelectItem>
|
||||
<SelectItem value="coinReward">Coin Reward</SelectItem>
|
||||
{isTasksView && <SelectItem value="dueDate">Due Date</SelectItem>}
|
||||
{!isTasksView && <SelectItem value="frequency">Frequency</SelectItem>}
|
||||
<SelectItem value="name">{t('sortByName')}</SelectItem>
|
||||
<SelectItem value="coinReward">{t('sortByCoinReward')}</SelectItem>
|
||||
{isTasksView && <SelectItem value="dueDate">{t('sortByDueDate')}</SelectItem>}
|
||||
{!isTasksView && <SelectItem value="frequency">{t('sortByFrequency')}</SelectItem>}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button variant="outline" size="icon" onClick={() => setSortOrder(prev => prev === 'asc' ? 'desc' : 'asc')}>
|
||||
{sortOrder === 'asc' ? <ArrowUpNarrowWide className="h-4 w-4" /> : <ArrowDownWideNarrow className="h-4 w-4" />}
|
||||
<span className="sr-only">Toggle sort order</span>
|
||||
<span className="sr-only">{t('toggleSortOrderAriaLabel')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -166,14 +167,14 @@ export default function HabitList({ isTasksView }: { isTasksView: boolean}) {
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 items-stretch">
|
||||
{activeHabits.length === 0 && searchTerm.trim() ? (
|
||||
<div className="col-span-2 text-center text-muted-foreground py-8">
|
||||
No {isTasksView ? 'tasks' : 'habits'} found matching your search.
|
||||
{t(isTasksView ? 'noTasksFoundMessage' : 'noHabitsFoundMessage')}
|
||||
</div>
|
||||
) : activeHabits.length === 0 ? (
|
||||
<div className="col-span-2">
|
||||
<EmptyState
|
||||
icon={isTasksView ? TaskIcon : HabitIcon}
|
||||
title={isTasksView ? "No tasks yet" : "No habits yet"}
|
||||
description={isTasksView ? "Create your first task to start tracking your progress" : "Create your first habit to start tracking your progress"}
|
||||
title={t(isTasksView ? 'emptyStateTasksTitle' : 'emptyStateHabitsTitle')}
|
||||
description={t(isTasksView ? 'emptyStateTasksDescription' : 'emptyStateHabitsDescription')}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
@@ -194,7 +195,7 @@ export default function HabitList({ isTasksView }: { isTasksView: boolean}) {
|
||||
<>
|
||||
<div className="col-span-1 sm:col-span-2 relative flex items-center my-6">
|
||||
<div className="flex-grow border-t border-gray-300 dark:border-gray-600" />
|
||||
<span className="mx-4 text-sm text-gray-500 dark:text-gray-400">Archived</span>
|
||||
<span className="mx-4 text-sm text-gray-500 dark:text-gray-400">{t('archivedSectionTitle')}</span>
|
||||
<div className="flex-grow border-t border-gray-300 dark:border-gray-600" />
|
||||
</div>
|
||||
{archivedHabits.map((habit: Habit) => (
|
||||
@@ -235,9 +236,9 @@ export default function HabitList({ isTasksView }: { isTasksView: boolean}) {
|
||||
}
|
||||
setDeleteConfirmation({ isOpen: false, habitId: null })
|
||||
}}
|
||||
title={isTasksView ? "Delete Task" : "Delete Habit"}
|
||||
message={isTasksView ? "Are you sure you want to delete this task? This action cannot be undone." : "Are you sure you want to delete this habit? This action cannot be undone."}
|
||||
confirmText="Delete"
|
||||
title={t(isTasksView ? 'deleteTaskDialogTitle' : 'deleteHabitDialogTitle')}
|
||||
message={t(isTasksView ? 'deleteTaskDialogMessage' : 'deleteHabitDialogMessage')}
|
||||
confirmText={t('deleteButton')}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { completedHabitsMapAtom, hasTasksAtom, settingsAtom } from '@/lib/atoms'; // Added completedHabitsMapAtom
|
||||
import { Habit } from '@/lib/types'
|
||||
import { Habit } from '@/lib/types';
|
||||
import { d2s, getNow } from '@/lib/utils'; // Removed getCompletedHabitsForDate
|
||||
import { useAtom } from 'jotai'
|
||||
import { CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'
|
||||
import { useAtom } from 'jotai';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
||||
|
||||
interface HabitStreakProps {
|
||||
habits: Habit[]
|
||||
}
|
||||
|
||||
export default function HabitStreak({ habits }: HabitStreakProps) {
|
||||
const t = useTranslations('HabitStreak');
|
||||
const [settings] = useAtom(settingsAtom)
|
||||
const [hasTasks] = useAtom(hasTasksAtom)
|
||||
const [completedHabitsMap] = useAtom(completedHabitsMapAtom) // Use the atom
|
||||
@@ -40,7 +42,7 @@ export default function HabitStreak({ habits }: HabitStreakProps) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Daily Completion Streak</CardTitle>
|
||||
<CardTitle>{t('dailyCompletionStreakTitle')}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="w-full aspect-[2/1]">
|
||||
@@ -56,11 +58,14 @@ export default function HabitStreak({ habits }: HabitStreakProps) {
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="date" />
|
||||
<YAxis />
|
||||
<Tooltip formatter={(value, name) => [`${value} ${name}`, 'Completed']} />
|
||||
<YAxis allowDecimals={false} />
|
||||
<Tooltip formatter={(value, name) => {
|
||||
const translatedName = name === 'habits' ? t('tooltipHabitsLabel') : t('tooltipTasksLabel');
|
||||
return [`${value} ${translatedName}`, t('tooltipCompletedLabel')];
|
||||
}} />
|
||||
<Line
|
||||
type="monotone"
|
||||
name="habits"
|
||||
name={t('tooltipHabitsLabel')}
|
||||
dataKey="habits"
|
||||
stroke="#14b8a6"
|
||||
strokeWidth={2}
|
||||
@@ -69,7 +74,7 @@ export default function HabitStreak({ habits }: HabitStreakProps) {
|
||||
{hasTasks && (
|
||||
<Line
|
||||
type="monotone"
|
||||
name="tasks"
|
||||
name={t('tooltipTasksLabel')}
|
||||
dataKey="tasks"
|
||||
stroke="#f59e0b"
|
||||
strokeWidth={2}
|
||||
|
||||
@@ -3,32 +3,34 @@
|
||||
import { useHelpers } from '@/lib/client-helpers'
|
||||
import { HabitIcon, TaskIcon } from '@/lib/constants'
|
||||
import { Calendar, Coins, Gift, Home } from 'lucide-react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useEffect, useState } from 'react'
|
||||
import AboutModal from './AboutModal'
|
||||
import { usePathname } from 'next/navigation'
|
||||
|
||||
type ViewPort = 'main' | 'mobile'
|
||||
|
||||
const navItems = () => [
|
||||
{ icon: Home, label: 'Dashboard', href: '/', position: 'main' },
|
||||
{ icon: HabitIcon, label: 'Habits', href: '/habits', position: 'main' },
|
||||
{ icon: TaskIcon, label: 'Tasks', href: '/tasks', position: 'main' },
|
||||
{ icon: Calendar, label: 'Calendar', href: '/calendar', position: 'main' },
|
||||
{ icon: Gift, label: 'Wishlist', href: '/wishlist', position: 'main' },
|
||||
{ icon: Coins, label: 'Coins', href: '/coins', position: 'main' },
|
||||
]
|
||||
|
||||
interface NavigationProps {
|
||||
viewPort: ViewPort
|
||||
}
|
||||
|
||||
export default function Navigation({ viewPort }: NavigationProps) {
|
||||
const t = useTranslations('Navigation')
|
||||
const [showAbout, setShowAbout] = useState(false)
|
||||
const [isMobileView, setIsMobileView] = useState(false)
|
||||
const { isIOS } = useHelpers()
|
||||
const pathname = usePathname();
|
||||
|
||||
const navItems = () => [
|
||||
{ icon: Home, label: t('dashboard'), href: '/', position: 'main' },
|
||||
{ icon: HabitIcon, label: t('habits'), href: '/habits', position: 'main' },
|
||||
{ icon: TaskIcon, label: t('tasks'), href: '/tasks', position: 'main' },
|
||||
{ icon: Calendar, label: t('calendar'), href: '/calendar', position: 'main' },
|
||||
{ icon: Gift, label: t('wishlist'), href: '/wishlist', position: 'main' },
|
||||
{ icon: Coins, label: t('coins'), href: '/coins', position: 'main' },
|
||||
]
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
setIsMobileView(window.innerWidth < 1024)
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useAtom } from 'jotai'
|
||||
import { coinsAtom, habitsAtom, wishlistAtom, usersAtom } from '@/lib/atoms'
|
||||
import { Bell } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import NotificationDropdown from './NotificationDropdown';
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -17,6 +18,7 @@ import { useHelpers } from '@/lib/client-helpers';
|
||||
import { User, CoinTransaction } from '@/lib/types';
|
||||
|
||||
export default function NotificationBell() {
|
||||
const t = useTranslations('NotificationBell');
|
||||
const { currentUser } = useHelpers();
|
||||
const [coinsData] = useAtom(coinsAtom)
|
||||
const [habitsData] = useAtom(habitsAtom)
|
||||
@@ -99,7 +101,7 @@ export default function NotificationBell() {
|
||||
const nowTimestamp = d2t({ dateTime: getNow({}) });
|
||||
await updateLastNotificationReadTimestamp(currentUser.id, nowTimestamp);
|
||||
} catch (error) {
|
||||
console.error("Failed to update notification read timestamp:", error);
|
||||
console.error(t('errorUpdateTimestamp'), error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -11,33 +11,18 @@ import {
|
||||
import { CoinTransaction, HabitsData, User, UserData, WishlistData } from '@/lib/types';
|
||||
import { t2d } from '@/lib/utils';
|
||||
import { Info } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface NotificationDropdownProps {
|
||||
currentUser: User | null;
|
||||
unreadNotifications: CoinTransaction[];
|
||||
displayedReadNotifications: CoinTransaction[];
|
||||
habitsData: HabitsData; // Keep needed props
|
||||
habitsData: HabitsData;
|
||||
wishlistData: WishlistData;
|
||||
usersData: UserData;
|
||||
}
|
||||
|
||||
// Helper function to generate notification message
|
||||
const getNotificationMessage = (tx: CoinTransaction, triggeringUser?: User, relatedItemName?: string): string => {
|
||||
const username = triggeringUser?.username || 'Someone';
|
||||
const itemName = relatedItemName || 'a shared item';
|
||||
switch (tx.type) {
|
||||
case 'HABIT_COMPLETION':
|
||||
case 'TASK_COMPLETION':
|
||||
return `${username} completed ${itemName}.`;
|
||||
case 'WISH_REDEMPTION':
|
||||
return `${username} redeemed ${itemName}.`;
|
||||
// Add other relevant transaction types if needed
|
||||
default:
|
||||
return `Activity related to ${itemName} by ${username}.`; // Fallback message
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to get the name of the related item
|
||||
const getRelatedItemName = (tx: CoinTransaction, habitsData: HabitsData, wishlistData: WishlistData): string | undefined => {
|
||||
if (!tx.relatedItemId) return undefined;
|
||||
@@ -59,19 +44,33 @@ export default function NotificationDropdown({
|
||||
wishlistData,
|
||||
usersData,
|
||||
}: NotificationDropdownProps) {
|
||||
if (!currentUser) {
|
||||
return <div className="p-4 text-sm text-gray-500">Not logged in.</div>;
|
||||
}
|
||||
const t = useTranslations('NotificationDropdown');
|
||||
|
||||
// Removed the useMemo block for calculating notifications
|
||||
// Helper function to generate notification message, now using t
|
||||
const getNotificationMessage = (tx: CoinTransaction, triggeringUser?: User, relatedItemName?: string): string => {
|
||||
const username = triggeringUser?.username || t('defaultUsername');
|
||||
const itemName = relatedItemName || t('defaultItemName');
|
||||
switch (tx.type) {
|
||||
case 'HABIT_COMPLETION':
|
||||
case 'TASK_COMPLETION':
|
||||
return t('userCompletedItem', { username, itemName });
|
||||
case 'WISH_REDEMPTION':
|
||||
return t('userRedeemedItem', { username, itemName });
|
||||
default:
|
||||
return t('activityRelatedToItem', { username, itemName });
|
||||
}
|
||||
};
|
||||
|
||||
if (!currentUser) {
|
||||
return <div className="p-4 text-sm text-gray-500">{t('notLoggedIn')}</div>;
|
||||
}
|
||||
|
||||
const renderNotification = (tx: CoinTransaction, isUnread: boolean) => {
|
||||
const triggeringUser = usersData.users.find(u => u.id === tx.userId);
|
||||
const relatedItemName = getRelatedItemName(tx, habitsData, wishlistData);
|
||||
const message = getNotificationMessage(tx, triggeringUser, relatedItemName);
|
||||
const message = getNotificationMessage(tx, triggeringUser, relatedItemName); // Uses the new t-aware helper
|
||||
const txTimestamp = t2d({ timestamp: tx.timestamp, timezone: 'UTC' });
|
||||
const timeAgo = txTimestamp.toRelative(); // e.g., "2 hours ago"
|
||||
// Add the triggering user's ID to the query params if it exists
|
||||
const timeAgo = txTimestamp.toRelative();
|
||||
const linkHref = `/coins?highlight=${tx.id}${tx.userId ? `&user=${tx.userId}` : ''}`;
|
||||
|
||||
return (
|
||||
@@ -98,21 +97,21 @@ export default function NotificationDropdown({
|
||||
{/* Removed the outer div as width is now set on DropdownMenuContent in NotificationBell */}
|
||||
<>
|
||||
<div className="p-3 border-b border-gray-200 dark:border-gray-700 flex items-center gap-2">
|
||||
<h4 className="text-sm font-medium">Notifications</h4>
|
||||
<h4 className="text-sm font-medium">{t('notificationsTitle')}</h4>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="h-4 w-4 text-muted-foreground cursor-help" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left" className="max-w-xs">
|
||||
<p className="text-xs">
|
||||
Shows completions or redemptions by other users for habits or wishlist that you shared with them (you must be admin)
|
||||
{t('notificationsTooltip')}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<ScrollArea className="h-[400px]">
|
||||
{unreadNotifications.length === 0 && displayedReadNotifications.length === 0 && (
|
||||
<div className="p-4 text-center text-sm text-gray-500">No notifications yet.</div>
|
||||
<div className="p-4 text-center text-sm text-gray-500">{t('noNotificationsYet')}</div>
|
||||
)}
|
||||
|
||||
{unreadNotifications.length > 0 && (
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { toast } from '@/hooks/use-toast';
|
||||
import { User } from '@/lib/types';
|
||||
import { User as UserIcon } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useState } from 'react';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
|
||||
import { Button } from './ui/button';
|
||||
@@ -22,6 +23,7 @@ export default function PasswordEntryForm({
|
||||
onSubmit,
|
||||
error
|
||||
}: PasswordEntryFormProps) {
|
||||
const t = useTranslations('PasswordEntryForm');
|
||||
const hasPassword = !!user.password;
|
||||
const [password, setPassword] = useState('');
|
||||
|
||||
@@ -31,8 +33,8 @@ export default function PasswordEntryForm({
|
||||
await onSubmit(password);
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: err instanceof Error ? err.message : 'Login failed',
|
||||
title: t('loginErrorToastTitle'),
|
||||
description: err instanceof Error ? err.message : t('loginFailedErrorToastDescription'),
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
@@ -58,18 +60,18 @@ export default function PasswordEntryForm({
|
||||
onClick={onCancel}
|
||||
className="text-sm text-blue-500 hover:text-blue-600 mt-1"
|
||||
>
|
||||
Not you?
|
||||
{t('notYouButton')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasPassword && <div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Label htmlFor="password">{t('passwordLabel')}</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="Enter password"
|
||||
placeholder={t('passwordPlaceholder')}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className={error ? 'border-red-500' : ''}
|
||||
@@ -82,10 +84,10 @@ export default function PasswordEntryForm({
|
||||
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button type="button" variant="outline" onClick={onCancel}>
|
||||
Cancel
|
||||
{t('cancelButton')}
|
||||
</Button>
|
||||
<Button type="submit" disabled={hasPassword && !password}>
|
||||
Login
|
||||
{t('loginButton')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { Switch } from './ui/switch';
|
||||
import { Label } from './ui/label';
|
||||
import { Permission } from '@/lib/types';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
interface PermissionSelectorProps {
|
||||
permissions: Permission[];
|
||||
@@ -11,18 +12,20 @@ interface PermissionSelectorProps {
|
||||
onAdminChange: (isAdmin: boolean) => void;
|
||||
}
|
||||
|
||||
const permissionLabels: { [key: string]: string } = {
|
||||
habit: 'Habit / Task',
|
||||
wishlist: 'Wishlist',
|
||||
coins: 'Coins'
|
||||
};
|
||||
|
||||
export function PermissionSelector({
|
||||
permissions,
|
||||
isAdmin,
|
||||
onPermissionsChange,
|
||||
onAdminChange,
|
||||
}: PermissionSelectorProps) {
|
||||
const t = useTranslations('PermissionSelector');
|
||||
|
||||
const permissionLabels: { [key: string]: string } = {
|
||||
habit: t('resourceHabitTask'),
|
||||
wishlist: t('resourceWishlist'),
|
||||
coins: t('resourceCoins')
|
||||
};
|
||||
|
||||
const currentPermissions = isAdmin ?
|
||||
{
|
||||
habit: { write: true, interact: true },
|
||||
@@ -49,11 +52,11 @@ export function PermissionSelector({
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Permissions</Label>
|
||||
<Label>{t('permissionsTitle')}</Label>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<div className="flex items-center justify-between p-3 rounded-lg border bg-muted/50">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="font-medium text-sm">Admin Access</div>
|
||||
<div className="font-medium text-sm">{t('adminAccessLabel')}</div>
|
||||
</div>
|
||||
<Switch
|
||||
id="isAdmin"
|
||||
@@ -65,7 +68,7 @@ export function PermissionSelector({
|
||||
|
||||
{isAdmin ? (
|
||||
<p className="text-xs text-muted-foreground px-3">
|
||||
Admins have full permission to all data for all users
|
||||
{t('adminAccessDescription')}
|
||||
</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
@@ -74,7 +77,7 @@ export function PermissionSelector({
|
||||
<div className="font-medium capitalize text-sm border-b pb-2">{permissionLabels[resource]}</div>
|
||||
<div className="flex flex-col gap-2.5">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2">
|
||||
<Label htmlFor={`${resource}-write`} className="text-xs text-muted-foreground break-words">Write</Label>
|
||||
<Label htmlFor={`${resource}-write`} className="text-xs text-muted-foreground break-words">{t('permissionWrite')}</Label>
|
||||
<Switch
|
||||
id={`${resource}-write`}
|
||||
className="h-4 w-7"
|
||||
@@ -85,7 +88,7 @@ export function PermissionSelector({
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2">
|
||||
<Label htmlFor={`${resource}-interact`} className="text-xs text-muted-foreground break-words">Interact</Label>
|
||||
<Label htmlFor={`${resource}-interact`} className="text-xs text-muted-foreground break-words">{t('permissionInteract')}</Label>
|
||||
<Switch
|
||||
id={`${resource}-interact`}
|
||||
className="h-4 w-7"
|
||||
|
||||
@@ -3,55 +3,41 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import { useHabits } from '@/hooks/useHabits'
|
||||
import { habitsAtom, pomodoroAtom, pomodoroTodayCompletionsAtom, settingsAtom } from '@/lib/atoms'
|
||||
import { habitsAtom, pomodoroAtom, pomodoroTodayCompletionsAtom } from '@/lib/atoms'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useAtom } from 'jotai'
|
||||
import { Clock, Minus, Pause, Play, RotateCw, SkipForward, X } from 'lucide-react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
interface PomoConfig {
|
||||
labels: string[]
|
||||
getLabels: () => string[]
|
||||
duration: number
|
||||
type: 'focus' | 'break'
|
||||
}
|
||||
|
||||
const PomoConfigs: Record<PomoConfig['type'], PomoConfig> = {
|
||||
focus: {
|
||||
labels: [
|
||||
'Stay Focused',
|
||||
'You Got This',
|
||||
'Keep Going',
|
||||
'Crush It',
|
||||
'Make It Happen',
|
||||
'Stay Strong',
|
||||
'Push Through',
|
||||
'One Step at a Time',
|
||||
'You Can Do It',
|
||||
'Focus and Conquer'
|
||||
],
|
||||
duration: 25 * 60,
|
||||
type: 'focus',
|
||||
},
|
||||
break: {
|
||||
labels: [
|
||||
'Take a Break',
|
||||
'Relax and Recharge',
|
||||
'Breathe Deeply',
|
||||
'Stretch It Out',
|
||||
'Refresh Yourself',
|
||||
'You Deserve This',
|
||||
'Recharge Your Energy',
|
||||
'Step Away for a Bit',
|
||||
'Clear Your Mind',
|
||||
'Rest and Rejuvenate'
|
||||
],
|
||||
duration: 5 * 60,
|
||||
type: 'break',
|
||||
},
|
||||
}
|
||||
|
||||
export default function PomodoroTimer() {
|
||||
const [settings] = useAtom(settingsAtom)
|
||||
const t = useTranslations('PomodoroTimer')
|
||||
|
||||
const PomoConfigs: Record<PomoConfig['type'], PomoConfig> = {
|
||||
focus: {
|
||||
getLabels: () => [
|
||||
t('focusLabel1'), t('focusLabel2'), t('focusLabel3'), t('focusLabel4'), t('focusLabel5'),
|
||||
t('focusLabel6'), t('focusLabel7'), t('focusLabel8'), t('focusLabel9'), t('focusLabel10')
|
||||
],
|
||||
duration: 25 * 60,
|
||||
type: 'focus',
|
||||
},
|
||||
break: {
|
||||
getLabels: () => [
|
||||
t('breakLabel1'), t('breakLabel2'), t('breakLabel3'), t('breakLabel4'), t('breakLabel5'),
|
||||
t('breakLabel6'), t('breakLabel7'), t('breakLabel8'), t('breakLabel9'), t('breakLabel10')
|
||||
],
|
||||
duration: 5 * 60,
|
||||
type: 'break',
|
||||
},
|
||||
}
|
||||
|
||||
const [pomo, setPomo] = useAtom(pomodoroAtom)
|
||||
const { show, selectedHabitId, autoStart, minimized } = pomo
|
||||
const [habitsData] = useAtom(habitsAtom)
|
||||
@@ -61,21 +47,23 @@ export default function PomodoroTimer() {
|
||||
const [state, setState] = useState<'started' | 'stopped' | 'paused'>(autoStart ? 'started' : 'stopped')
|
||||
const wakeLock = useRef<WakeLockSentinel | null>(null)
|
||||
const [todayCompletions] = useAtom(pomodoroTodayCompletionsAtom)
|
||||
const currentTimer = useRef<PomoConfig>(PomoConfigs.focus)
|
||||
const [currentLabel, setCurrentLabel] = useState(
|
||||
currentTimer.current.labels[Math.floor(Math.random() * currentTimer.current.labels.length)]
|
||||
)
|
||||
const currentTimerRef = useRef<PomoConfig>(PomoConfigs.focus)
|
||||
const [currentLabel, setCurrentLabel] = useState(() => {
|
||||
const labels = currentTimerRef.current.getLabels();
|
||||
return labels[Math.floor(Math.random() * labels.length)];
|
||||
});
|
||||
|
||||
|
||||
// Handle wake lock
|
||||
useEffect(() => {
|
||||
const requestWakeLock = async () => {
|
||||
try {
|
||||
if (!('wakeLock' in navigator)) {
|
||||
console.debug('Browser does not support wakelock')
|
||||
console.debug(t('wakeLockNotSupported'))
|
||||
return
|
||||
}
|
||||
if (wakeLock.current && !wakeLock.current.released) {
|
||||
console.debug('Wake lock already in use')
|
||||
console.debug(t('wakeLockInUse'))
|
||||
return
|
||||
}
|
||||
if (state === 'started') {
|
||||
@@ -84,7 +72,7 @@ export default function PomodoroTimer() {
|
||||
return
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error requesting wake lock:', err)
|
||||
console.error(t('wakeLockRequestError'), err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +83,7 @@ export default function PomodoroTimer() {
|
||||
wakeLock.current = null
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error releasing wake lock:', err)
|
||||
console.error(t('wakeLockReleaseError'), err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,40 +112,43 @@ export default function PomodoroTimer() {
|
||||
|
||||
// Timer logic
|
||||
useEffect(() => {
|
||||
let interval: ReturnType<typeof setInterval> | null = null
|
||||
let interval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
if (state === 'started') {
|
||||
if (state === "started") {
|
||||
// Calculate the target end time based on current timeLeft
|
||||
const targetEndTime = Date.now() + timeLeft * 1000
|
||||
const targetEndTime = Date.now() + timeLeft * 1000;
|
||||
|
||||
interval = setInterval(() => {
|
||||
const remaining = Math.floor((targetEndTime - Date.now()) / 1000)
|
||||
const remaining = Math.floor((targetEndTime - Date.now()) / 1000);
|
||||
|
||||
if (remaining <= 0) {
|
||||
setState("stopped")
|
||||
const currentTimerType = currentTimer.current.type
|
||||
currentTimer.current = currentTimerType === 'focus' ? PomoConfigs.break : PomoConfigs.focus
|
||||
setTimeLeft(currentTimer.current.duration)
|
||||
setCurrentLabel(
|
||||
currentTimer.current.labels[Math.floor(Math.random() * currentTimer.current.labels.length)]
|
||||
)
|
||||
|
||||
// update habits only after focus sessions
|
||||
if (selectedHabit && currentTimerType === 'focus') {
|
||||
completeHabit(selectedHabit)
|
||||
// The atom will automatically update with the new completions
|
||||
}
|
||||
handleTimerEnd();
|
||||
} else {
|
||||
setTimeLeft(remaining)
|
||||
setTimeLeft(remaining);
|
||||
}
|
||||
}, 1000)
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// return handles any other states
|
||||
return () => {
|
||||
if (interval) clearInterval(interval)
|
||||
if (interval) clearInterval(interval);
|
||||
};
|
||||
}, [state]);
|
||||
|
||||
const handleTimerEnd = async () => {
|
||||
setState("stopped")
|
||||
const currentTimerType = currentTimerRef.current.type
|
||||
currentTimerRef.current = currentTimerType === 'focus' ? PomoConfigs.break : PomoConfigs.focus
|
||||
setTimeLeft(currentTimerRef.current.duration)
|
||||
const newLabels = currentTimerRef.current.getLabels();
|
||||
setCurrentLabel(newLabels[Math.floor(Math.random() * newLabels.length)])
|
||||
|
||||
// update habits only after focus sessions
|
||||
if (selectedHabit && currentTimerType === 'focus') {
|
||||
await completeHabit(selectedHabit)
|
||||
// The atom will automatically update with the new completions
|
||||
}
|
||||
}, [state, timeLeft, completeHabit, selectedHabit])
|
||||
}
|
||||
|
||||
const toggleTimer = () => {
|
||||
setState(prev => prev === 'started' ? 'paused' : 'started')
|
||||
@@ -165,17 +156,16 @@ export default function PomodoroTimer() {
|
||||
|
||||
const resetTimer = () => {
|
||||
setState("stopped")
|
||||
setTimeLeft(currentTimer.current.duration)
|
||||
setTimeLeft(currentTimerRef.current.duration)
|
||||
}
|
||||
|
||||
const skipTimer = () => {
|
||||
currentTimer.current = currentTimer.current.type === 'focus'
|
||||
currentTimerRef.current = currentTimerRef.current.type === 'focus'
|
||||
? PomoConfigs.break
|
||||
: PomoConfigs.focus
|
||||
resetTimer()
|
||||
setCurrentLabel(
|
||||
currentTimer.current.labels[Math.floor(Math.random() * currentTimer.current.labels.length)]
|
||||
)
|
||||
resetTimer() // This will also reset timeLeft to the new timer's duration
|
||||
const newLabels = currentTimerRef.current.getLabels();
|
||||
setCurrentLabel(newLabels[Math.floor(Math.random() * newLabels.length)])
|
||||
}
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
@@ -184,7 +174,7 @@ export default function PomodoroTimer() {
|
||||
return `${minutes}:${secs < 10 ? '0' : ''}${secs}`
|
||||
}
|
||||
|
||||
const progress = (timeLeft / currentTimer.current.duration) * 100
|
||||
const progress = (timeLeft / currentTimerRef.current.duration) * 100
|
||||
|
||||
if (!show) return null
|
||||
|
||||
@@ -237,11 +227,11 @@ export default function PomodoroTimer() {
|
||||
<div className={cn(
|
||||
'w-2 h-2 rounded-full flex-none',
|
||||
// order matters here
|
||||
currentTimer.current.type === 'focus' && 'bg-green-500',
|
||||
currentTimerRef.current.type === 'focus' && 'bg-green-500',
|
||||
state === 'started' && 'animate-pulse',
|
||||
state === 'paused' && 'bg-yellow-500',
|
||||
state === 'stopped' && 'bg-red-500',
|
||||
currentTimer.current.type === 'break' && 'bg-blue-500',
|
||||
currentTimerRef.current.type === 'break' && 'bg-blue-500',
|
||||
)} />
|
||||
<div className="font-bold text-foreground">
|
||||
{selectedHabit.name}
|
||||
@@ -249,7 +239,9 @@ export default function PomodoroTimer() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<span>{currentTimer.current.type.charAt(0).toUpperCase() + currentTimer.current.type.slice(1)}: {currentLabel}</span>
|
||||
<span>
|
||||
{currentTimerRef.current.type === 'focus' ? t('focusType') : t('breakType')}: {currentLabel}
|
||||
</span>
|
||||
{selectedHabit && selectedHabit.targetCompletions && selectedHabit.targetCompletions > 1 && (
|
||||
<div className="flex justify-center gap-1 mt-2">
|
||||
{(() => {
|
||||
@@ -288,12 +280,12 @@ export default function PomodoroTimer() {
|
||||
{state === "started" ? (
|
||||
<>
|
||||
<Pause className="h-4 w-4 sm:mr-2" />
|
||||
<span className="hidden sm:inline">Pause</span>
|
||||
<span className="hidden sm:inline">{t('pauseButton')}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play className="h-4 w-4 sm:mr-2" />
|
||||
<span className="hidden sm:inline">Start</span>
|
||||
<span className="hidden sm:inline">{t('startButton')}</span>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -304,7 +296,7 @@ export default function PomodoroTimer() {
|
||||
className="sm:px-4"
|
||||
>
|
||||
<RotateCw className="h-4 w-4 sm:mr-2" />
|
||||
<span className="hidden sm:inline">Reset</span>
|
||||
<span className="hidden sm:inline">{t('resetButton')}</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -313,7 +305,7 @@ export default function PomodoroTimer() {
|
||||
className="sm:px-4"
|
||||
>
|
||||
<SkipForward className="h-4 w-4 sm:mr-2" />
|
||||
<span className="hidden sm:inline">Skip</span>
|
||||
<span className="hidden sm:inline">{t('skipButton')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { settingsAtom, userSelectAtom } from "@/lib/atoms"
|
||||
import { useHelpers } from "@/lib/client-helpers"
|
||||
import { useAtom } from "jotai"
|
||||
import { ArrowRightLeft, Crown, Info, LogOut, Moon, Palette, Settings, Sun, User } from "lucide-react"
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { useTheme } from "next-themes"
|
||||
import Link from "next/link"
|
||||
import { useState } from "react"
|
||||
@@ -17,6 +18,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from './ui/dialog'
|
||||
import UserForm from './UserForm'
|
||||
|
||||
export function Profile() {
|
||||
const t = useTranslations('Profile');
|
||||
const [settings] = useAtom(settingsAtom)
|
||||
const [userSelect, setUserSelect] = useAtom(userSelectAtom)
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
@@ -29,14 +31,14 @@ export function Profile() {
|
||||
try {
|
||||
await signOut()
|
||||
toast({
|
||||
title: "Signed out successfully",
|
||||
description: "You have been logged out of your account",
|
||||
title: t('signOutSuccessTitle'),
|
||||
description: t('signOutSuccessDescription'),
|
||||
})
|
||||
setTimeout(() => window.location.reload(), 300);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to sign out",
|
||||
title: t('signOutErrorTitle'),
|
||||
description: t('signOutErrorDescription'),
|
||||
variant: "destructive",
|
||||
})
|
||||
}
|
||||
@@ -66,7 +68,7 @@ export function Profile() {
|
||||
</Avatar>
|
||||
<div className="flex flex-col mr-4">
|
||||
<span className="text-sm font-semibold flex items-center gap-1">
|
||||
{user?.username || "Guest"}
|
||||
{user?.username || t('guestUsername')}
|
||||
{user?.isAdmin && <Crown className="h-3 w-3 text-yellow-500" />}
|
||||
</span>
|
||||
{user && (
|
||||
@@ -78,7 +80,7 @@ export function Profile() {
|
||||
}}
|
||||
className="text-xs text-muted-foreground hover:text-primary transition-colors text-left"
|
||||
>
|
||||
Edit profile
|
||||
{t('editProfileButton')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -104,18 +106,18 @@ export function Profile() {
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
<ArrowRightLeft className="h-4 w-4" />
|
||||
<span>Switch user</span>
|
||||
<span>{t('switchUserButton')}</span>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="cursor-pointer px-2 py-1.5" asChild>
|
||||
<Link
|
||||
href="/settings"
|
||||
aria-label='settings'
|
||||
aria-label={t('settingsLink')}
|
||||
className="flex items-center w-full gap-3"
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
<span>Settings</span>
|
||||
<span>{t('settingsLink')}</span>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="cursor-pointer px-2 py-1.5" asChild>
|
||||
@@ -124,14 +126,14 @@ export function Profile() {
|
||||
className="flex items-center w-full gap-3"
|
||||
>
|
||||
<Info className="h-4 w-4" />
|
||||
<span>About</span>
|
||||
<span>{t('aboutButton')}</span>
|
||||
</button>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="cursor-pointer px-2 py-1.5">
|
||||
<div className="flex items-center justify-between w-full gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Palette className="h-4 w-4" />
|
||||
<span>Theme</span>
|
||||
<span>{t('themeLabel')}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
@@ -174,7 +176,7 @@ export function Profile() {
|
||||
<Dialog open={isEditing} onOpenChange={() => setIsEditing(false)}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Profile</DialogTitle>
|
||||
<DialogTitle>{t('editProfileModalTitle')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<UserForm
|
||||
userId={user.id}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useAtom } from 'jotai'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { settingsAtom } from '@/lib/atoms'
|
||||
import { useCoins } from '@/hooks/useCoins'
|
||||
import { FormattedNumber } from '@/components/FormattedNumber'
|
||||
|
||||
export default function TodayEarnedCoins({ longFormat }: { longFormat?: boolean }) {
|
||||
const t = useTranslations('TodayEarnedCoins')
|
||||
const [settings] = useAtom(settingsAtom)
|
||||
const { coinsEarnedToday } = useCoins()
|
||||
|
||||
@@ -14,7 +16,7 @@ export default function TodayEarnedCoins({ longFormat }: { longFormat?: boolean
|
||||
{"+"}
|
||||
<FormattedNumber amount={coinsEarnedToday} settings={settings} />
|
||||
{longFormat ?
|
||||
<span className="text-sm text-muted-foreground"> today</span>
|
||||
<span className="text-sm text-muted-foreground"> {t('todaySuffix')}</span>
|
||||
: null}
|
||||
</span>
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Check, Loader2, Pencil, Trash2, X } from 'lucide-react'
|
||||
import { toast } from '@/hooks/use-toast'
|
||||
import { useTranslations } from 'next-intl'
|
||||
|
||||
interface TransactionNoteEditorProps {
|
||||
transactionId: string
|
||||
@@ -19,6 +20,7 @@ export function TransactionNoteEditor({
|
||||
onSave,
|
||||
onDelete
|
||||
}: TransactionNoteEditorProps) {
|
||||
const t = useTranslations('TransactionNoteEditor');
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [noteText, setNoteText] = useState(initialNote)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
@@ -27,8 +29,8 @@ export function TransactionNoteEditor({
|
||||
const trimmedNote = noteText.trim()
|
||||
if (trimmedNote.length > 200) {
|
||||
toast({
|
||||
title: 'Note too long',
|
||||
description: 'Notes must be less than 200 characters',
|
||||
title: t('noteTooLongTitle'),
|
||||
description: t('noteTooLongDescription'),
|
||||
variant: 'destructive'
|
||||
})
|
||||
return
|
||||
@@ -40,8 +42,8 @@ export function TransactionNoteEditor({
|
||||
setIsEditing(false)
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Error saving note',
|
||||
description: 'Please try again',
|
||||
title: t('errorSavingNoteTitle'),
|
||||
description: t('pleaseTryAgainDescription'),
|
||||
variant: 'destructive'
|
||||
})
|
||||
// Revert to initial value on error
|
||||
@@ -59,8 +61,8 @@ export function TransactionNoteEditor({
|
||||
setIsEditing(false)
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Error deleting note',
|
||||
description: 'Please try again',
|
||||
title: t('errorDeletingNoteTitle'),
|
||||
description: t('pleaseTryAgainDescription'),
|
||||
variant: 'destructive'
|
||||
})
|
||||
} finally {
|
||||
@@ -74,7 +76,7 @@ export function TransactionNoteEditor({
|
||||
<Input
|
||||
value={noteText}
|
||||
onChange={(e) => setNoteText(e.target.value)}
|
||||
placeholder="Add a note..."
|
||||
placeholder={t('addNotePlaceholder')}
|
||||
className="w-64"
|
||||
maxLength={200}
|
||||
/>
|
||||
@@ -85,7 +87,7 @@ export function TransactionNoteEditor({
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
className="text-green-600 dark:text-green-500 hover:text-green-700 dark:hover:text-green-400 transition-colors"
|
||||
title="Save note"
|
||||
title={t('saveNoteTitle')}
|
||||
>
|
||||
{isSaving ? <Loader2 className="h-4 w-4 animate-spin" /> : <Check className="h-4 w-4" />}
|
||||
</Button>
|
||||
@@ -98,7 +100,7 @@ export function TransactionNoteEditor({
|
||||
}}
|
||||
disabled={isSaving}
|
||||
className="text-red-600 dark:text-red-500 hover:text-red-700 dark:hover:text-red-400 transition-colors"
|
||||
title="Cancel"
|
||||
title={t('cancelButtonTitle')}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -109,7 +111,7 @@ export function TransactionNoteEditor({
|
||||
onClick={handleDelete}
|
||||
disabled={isSaving}
|
||||
className="text-gray-600 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-400 transition-colors"
|
||||
title="Delete note"
|
||||
title={t('deleteNoteTitle')}
|
||||
>
|
||||
{isSaving ? <Loader2 className="h-4 w-4 animate-spin" /> : <Trash2 className="h-4 w-4" />}
|
||||
</Button>
|
||||
@@ -129,7 +131,7 @@ export function TransactionNoteEditor({
|
||||
<button
|
||||
onClick={() => setIsEditing(true)}
|
||||
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
aria-label="Edit note"
|
||||
aria-label={t('editNoteAriaLabel')}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { passwordSchema, usernameSchema } from '@/lib/zod';
|
||||
import { useAtom, useAtomValue } from 'jotai';
|
||||
import _ from 'lodash';
|
||||
import { User as UserIcon } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useState } from 'react';
|
||||
import { PermissionSelector } from './PermissionSelector';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
|
||||
@@ -24,6 +25,7 @@ interface UserFormProps {
|
||||
}
|
||||
|
||||
export default function UserForm({ userId, onCancel, onSuccess }: UserFormProps) {
|
||||
const t = useTranslations('UserForm');
|
||||
const [users, setUsersData] = useAtom(usersAtom);
|
||||
const serverSettings = useAtomValue(serverSettingsAtom)
|
||||
const user = userId ? users.users.find(u => u.id === userId) : undefined;
|
||||
@@ -103,8 +105,8 @@ export default function UserForm({ userId, onCancel, onSuccess }: UserFormProps)
|
||||
}));
|
||||
|
||||
toast({
|
||||
title: "User updated",
|
||||
description: `Successfully updated user ${username}`,
|
||||
title: t('toastUserUpdatedTitle'),
|
||||
description: t('toastUserUpdatedDescription', { username }),
|
||||
variant: 'default'
|
||||
});
|
||||
} else {
|
||||
@@ -127,8 +129,8 @@ export default function UserForm({ userId, onCancel, onSuccess }: UserFormProps)
|
||||
}));
|
||||
|
||||
toast({
|
||||
title: "User created",
|
||||
description: `Successfully created user ${username}`,
|
||||
title: t('toastUserCreatedTitle'),
|
||||
description: t('toastUserCreatedDescription', { username }),
|
||||
variant: 'default'
|
||||
});
|
||||
}
|
||||
@@ -137,15 +139,16 @@ export default function UserForm({ userId, onCancel, onSuccess }: UserFormProps)
|
||||
setError('');
|
||||
onSuccess();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : `Failed to ${isEditing ? 'update' : 'create'} user`);
|
||||
const action = isEditing ? t('actionUpdate') : t('actionCreate');
|
||||
setError(err instanceof Error ? err.message : t('errorFailedUserAction', { action }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleAvatarChange = async (file: File) => {
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
if (file.size > 5 * 1024 * 1024) { // 5MB
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "File size must be less than 5MB",
|
||||
title: t('errorTitle'),
|
||||
description: t('errorFileSizeLimit'),
|
||||
variant: 'destructive'
|
||||
});
|
||||
return;
|
||||
@@ -159,14 +162,14 @@ export default function UserForm({ userId, onCancel, onSuccess }: UserFormProps)
|
||||
setAvatarPath(path);
|
||||
setAvatarFile(null); // Clear the file since we've uploaded it
|
||||
toast({
|
||||
title: "Avatar uploaded",
|
||||
description: "Successfully uploaded avatar",
|
||||
title: t('toastAvatarUploadedTitle'),
|
||||
description: t('toastAvatarUploadedDescription'),
|
||||
variant: 'default'
|
||||
});
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to upload avatar",
|
||||
title: t('errorTitle'),
|
||||
description: t('errorFailedAvatarUpload'),
|
||||
variant: 'destructive'
|
||||
});
|
||||
}
|
||||
@@ -208,18 +211,18 @@ export default function UserForm({ userId, onCancel, onSuccess }: UserFormProps)
|
||||
}}
|
||||
className="w-full"
|
||||
>
|
||||
{isEditing ? 'Change Avatar' : 'Upload Avatar'}
|
||||
{isEditing ? t('changeAvatarButton') : t('uploadAvatarButton')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="username">Username</Label>
|
||||
<Label htmlFor="username">{t('usernameLabel')}</Label>
|
||||
<Input
|
||||
id="username"
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
placeholder={t('usernamePlaceholder')}
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className={error ? 'border-red-500' : ''}
|
||||
@@ -229,19 +232,19 @@ export default function UserForm({ userId, onCancel, onSuccess }: UserFormProps)
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">
|
||||
{isEditing ? 'New Password' : 'Password'}
|
||||
{isEditing ? t('newPasswordLabel') : t('passwordLabel')}
|
||||
</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder={isEditing ? "Leave blank to keep current" : "Enter password"}
|
||||
placeholder={isEditing ? t('passwordPlaceholderEdit') : t('passwordPlaceholderCreate')}
|
||||
value={password || ''}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className={error ? 'border-red-500' : ''}
|
||||
disabled={disablePassword}
|
||||
/>
|
||||
{serverSettings.isDemo && (
|
||||
<p className="text-sm text-red-500">Password is automatically disabled in demo instance</p>
|
||||
<p className="text-sm text-red-500">{t('demoPasswordDisabledMessage')}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -252,7 +255,7 @@ export default function UserForm({ userId, onCancel, onSuccess }: UserFormProps)
|
||||
onCheckedChange={setDisablePassword}
|
||||
disabled={serverSettings.isDemo}
|
||||
/>
|
||||
<Label htmlFor="disable-password">Disable password</Label>
|
||||
<Label htmlFor="disable-password">{t('disablePasswordLabel')}</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -276,10 +279,10 @@ export default function UserForm({ userId, onCancel, onSuccess }: UserFormProps)
|
||||
variant="outline"
|
||||
onClick={onCancel}
|
||||
>
|
||||
Cancel
|
||||
{t('cancelButton')}
|
||||
</Button>
|
||||
<Button type="submit" disabled={!username}>
|
||||
{isEditing ? 'Save Changes' : 'Create User'}
|
||||
{isEditing ? t('saveChangesButton') : t('createUserButton')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -9,14 +9,15 @@ import { cn } from '@/lib/utils';
|
||||
import { Description } from '@radix-ui/react-dialog';
|
||||
import { useAtom } from 'jotai';
|
||||
import { Crown, Plus, User as UserIcon, UserRoundPen } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useState } from 'react';
|
||||
import PasswordEntryForm from './PasswordEntryForm';
|
||||
import UserForm from './UserForm';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from './ui/dialog';
|
||||
|
||||
function UserCard({
|
||||
user,
|
||||
function UserCard({
|
||||
user,
|
||||
onSelect,
|
||||
onEdit,
|
||||
showEdit,
|
||||
@@ -38,9 +39,9 @@ function UserCard({
|
||||
)}
|
||||
>
|
||||
<Avatar className="h-16 w-16">
|
||||
<AvatarImage
|
||||
<AvatarImage
|
||||
src={user.avatarPath && `/api/avatars/${user.avatarPath.split('/').pop()}`}
|
||||
alt={user.username}
|
||||
alt={user.username}
|
||||
/>
|
||||
<AvatarFallback>
|
||||
<UserIcon className="h-8 w-8" />
|
||||
@@ -67,6 +68,7 @@ function UserCard({
|
||||
}
|
||||
|
||||
function AddUserButton({ onClick }: { onClick: () => void }) {
|
||||
const t = useTranslations('UserSelectModal');
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
@@ -77,7 +79,7 @@ function AddUserButton({ onClick }: { onClick: () => void }) {
|
||||
<Plus className="h-8 w-8" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="text-sm font-medium">Add User</span>
|
||||
<span className="text-sm font-medium">{t('addUserButton')}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -87,13 +89,13 @@ function UserSelectionView({
|
||||
currentUser,
|
||||
onUserSelect,
|
||||
onEditUser,
|
||||
onCreateUser
|
||||
onCreateUser,
|
||||
}: {
|
||||
users: User[],
|
||||
currentUser?: SafeUser,
|
||||
onUserSelect: (userId: string) => void,
|
||||
onEditUser: (userId: string) => void,
|
||||
onCreateUser: () => void
|
||||
onCreateUser: () => void,
|
||||
}) {
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-4 p-2 max-h-80 overflow-y-auto">
|
||||
@@ -108,20 +110,21 @@ function UserSelectionView({
|
||||
showEdit={!!currentUser?.isAdmin}
|
||||
isCurrentUser={false}
|
||||
/>
|
||||
))}
|
||||
))}
|
||||
{currentUser?.isAdmin && <AddUserButton onClick={onCreateUser} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function UserSelectModal({ onClose }: { onClose: () => void }) {
|
||||
const t = useTranslations('UserSelectModal');
|
||||
const [selectedUser, setSelectedUser] = useState<string>();
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [usersData] = useAtom(usersAtom);
|
||||
const users = usersData.users;
|
||||
const {currentUser} = useHelpers();
|
||||
const { currentUser } = useHelpers();
|
||||
|
||||
const handleUserSelect = (userId: string) => {
|
||||
setSelectedUser(userId);
|
||||
@@ -156,7 +159,7 @@ const {currentUser} = useHelpers();
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<Description></Description>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{isCreating ? 'Create New User' : 'Select User'}</DialogTitle>
|
||||
<DialogTitle>{isCreating ? t('createNewUserTitle') : t('selectUserTitle')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
@@ -184,19 +187,19 @@ const {currentUser} = useHelpers();
|
||||
const user = users.find(u => u.id === selectedUser);
|
||||
if (!user) throw new Error("User not found");
|
||||
await signIn(user.username, password);
|
||||
|
||||
|
||||
setError('');
|
||||
onClose();
|
||||
|
||||
toast({
|
||||
title: "Signed in successfully",
|
||||
description: `Welcome back, ${user.username}!`,
|
||||
title: t('signInSuccessTitle'),
|
||||
description: t('signInSuccessDescription', { username: user.username }),
|
||||
variant: "default"
|
||||
});
|
||||
|
||||
setTimeout(() => window.location.reload(), 300);
|
||||
} catch (err) {
|
||||
setError('invalid password');
|
||||
setError(t('errorInvalidPassword'));
|
||||
throw err;
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -4,8 +4,9 @@ import { browserSettingsAtom, habitsAtom, settingsAtom } from '@/lib/atoms'
|
||||
import { HabitIcon, TaskIcon } from '@/lib/constants'
|
||||
import { cn, isHabitDueToday } from '@/lib/utils'
|
||||
import { useAtom } from 'jotai'
|
||||
import { NotificationBadge } from './ui/notification-badge'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import { NotificationBadge } from './ui/notification-badge'
|
||||
|
||||
interface ViewToggleProps {
|
||||
className?: string
|
||||
@@ -14,6 +15,7 @@ interface ViewToggleProps {
|
||||
export function ViewToggle({
|
||||
className
|
||||
}: ViewToggleProps) {
|
||||
const t = useTranslations('ViewToggle')
|
||||
const [browserSettings, setBrowserSettings] = useAtom(browserSettingsAtom)
|
||||
const [habits] = useAtom(habitsAtom)
|
||||
const [settings] = useAtom(settingsAtom)
|
||||
@@ -40,9 +42,9 @@ export function ViewToggle({
|
||||
)}
|
||||
>
|
||||
<HabitIcon className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Habits</span>
|
||||
<span className="hidden sm:inline">{t('habitsLabel')}</span>
|
||||
</button>
|
||||
<NotificationBadge
|
||||
<NotificationBadge
|
||||
label={dueTasksCount}
|
||||
show={dueTasksCount > 0}
|
||||
variant={pathname.includes('tasks') ? 'secondary' : 'default'}
|
||||
@@ -56,7 +58,7 @@ export function ViewToggle({
|
||||
)}
|
||||
>
|
||||
<TaskIcon className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Tasks</span>
|
||||
<span className="hidden sm:inline">{t('tasksLabel')}</span>
|
||||
</button>
|
||||
</NotificationBadge>
|
||||
<div
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useHelpers } from '@/lib/client-helpers'
|
||||
import { User, WishlistItemType } from '@/lib/types'
|
||||
import { useAtom } from 'jotai'
|
||||
import { Archive, ArchiveRestore, Coins, Edit, Gift, MoreVertical, Trash2 } from 'lucide-react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar'
|
||||
|
||||
interface WishlistItemProps {
|
||||
@@ -29,7 +30,7 @@ interface WishlistItemProps {
|
||||
|
||||
const renderUserAvatars = (item: WishlistItemType, currentUser: User | null, usersData: { users: User[] }) => {
|
||||
if (!item.userIds || item.userIds.length <= 1) return null;
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex -space-x-2 ml-2 flex-shrink-0">
|
||||
{item.userIds?.filter((u) => u !== currentUser?.id).map(userId => {
|
||||
@@ -57,11 +58,13 @@ export default function WishlistItem({
|
||||
isHighlighted,
|
||||
isRecentlyRedeemed
|
||||
}: WishlistItemProps) {
|
||||
const t = useTranslations('WishlistItem')
|
||||
const { currentUser, hasPermission } = useHelpers()
|
||||
const canWrite = hasPermission('wishlist', 'write')
|
||||
const canInteract = hasPermission('wishlist', 'interact')
|
||||
const [usersData] = useAtom(usersAtom)
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Card
|
||||
id={`wishlist-${item.id}`}
|
||||
@@ -76,7 +79,7 @@ export default function WishlistItem({
|
||||
</CardTitle>
|
||||
{item.targetCompletions && (
|
||||
<span className="text-sm text-gray-500 dark:text-gray-400">
|
||||
({item.targetCompletions} {item.targetCompletions === 1 ? 'use' : 'uses'} left)
|
||||
({item.targetCompletions === 1 ? t('usesLeftSingular') : t('usesLeftPlural', { count: item.targetCompletions })})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -95,7 +98,7 @@ export default function WishlistItem({
|
||||
<div className="flex items-center gap-2">
|
||||
<Coins className={`h-4 w-4 ${item.archived ? 'text-gray-400 dark:text-gray-500' : 'text-yellow-400'}`} />
|
||||
<span className={`text-sm font-medium ${item.archived ? 'text-gray-400 dark:text-gray-500' : ''}`}>
|
||||
{item.coinCost} coins
|
||||
{item.coinCost} {t('coinsSuffix')}
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -112,13 +115,13 @@ export default function WishlistItem({
|
||||
<span>
|
||||
{isRecentlyRedeemed ? (
|
||||
<>
|
||||
<span className="sm:hidden">Done</span>
|
||||
<span className="hidden sm:inline">Redeemed!</span>
|
||||
<span className="sm:hidden">{t('redeemedDone')}</span>
|
||||
<span className="hidden sm:inline">{t('redeemedExclamation')}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="sm:hidden">Redeem</span>
|
||||
<span className="hidden sm:inline">Redeem</span>
|
||||
<span className="sm:hidden">{t('redeem')}</span>
|
||||
<span className="hidden sm:inline">{t('redeem')}</span>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
@@ -134,7 +137,7 @@ export default function WishlistItem({
|
||||
className="hidden sm:flex"
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
<span className="ml-2">Edit</span>
|
||||
<span className="ml-2">{t('editButton')}</span>
|
||||
</Button>
|
||||
)}
|
||||
<DropdownMenu>
|
||||
@@ -147,18 +150,18 @@ export default function WishlistItem({
|
||||
{!item.archived && (
|
||||
<DropdownMenuItem disabled={!canWrite} onClick={onArchive}>
|
||||
<Archive className="mr-2 h-4 w-4" />
|
||||
<span>Archive</span>
|
||||
<span>{t('archiveButton')}</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{item.archived && (
|
||||
<DropdownMenuItem disabled={!canWrite} onClick={onUnarchive}>
|
||||
<ArchiveRestore className="mr-2 h-4 w-4" />
|
||||
<span>Unarchive</span>
|
||||
<span>{t('unarchiveButton')}</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem onClick={onEdit} className="sm:hidden">
|
||||
<Edit className="mr-2 h-4 w-4" />
|
||||
Edit
|
||||
{t('editButton')}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator className="sm:hidden" />
|
||||
<DropdownMenuItem
|
||||
@@ -167,7 +170,7 @@ export default function WishlistItem({
|
||||
disabled={!canWrite}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
Delete
|
||||
{t('deleteButton')}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useWishlist } from '@/hooks/useWishlist'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { Plus, Gift } from 'lucide-react'
|
||||
import EmptyState from './EmptyState'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -13,6 +14,7 @@ import { openWindow } from '@/lib/utils'
|
||||
import { toast } from '@/hooks/use-toast'
|
||||
|
||||
export default function WishlistManager() {
|
||||
const t = useTranslations('WishlistManager')
|
||||
const {
|
||||
addWishlistItem,
|
||||
editWishlistItem,
|
||||
@@ -70,8 +72,8 @@ export default function WishlistManager() {
|
||||
const opened = openWindow(item.link!)
|
||||
if (!opened) {
|
||||
toast({
|
||||
title: "Popup Blocked",
|
||||
description: "Please allow popups to open the link",
|
||||
title: t('popupBlockedTitle'),
|
||||
description: t('popupBlockedDescription'),
|
||||
variant: "destructive"
|
||||
})
|
||||
}
|
||||
@@ -83,9 +85,9 @@ export default function WishlistManager() {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h1 className="text-3xl font-bold">My Wishlist</h1>
|
||||
<h1 className="text-3xl font-bold">{t('title')}</h1>
|
||||
<Button onClick={() => setIsModalOpen(true)}>
|
||||
<Plus className="mr-2 h-4 w-4" /> Add Reward
|
||||
<Plus className="mr-2 h-4 w-4" /> {t('addRewardButton')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 items-stretch">
|
||||
@@ -93,8 +95,8 @@ export default function WishlistManager() {
|
||||
<div className="col-span-1 lg:col-span-2">
|
||||
<EmptyState
|
||||
icon={Gift}
|
||||
title="Your wishlist is empty"
|
||||
description="Add rewards that you'd like to earn with your coins"
|
||||
title={t('emptyStateTitle')}
|
||||
description={t('emptyStateDescription')}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
@@ -129,7 +131,7 @@ export default function WishlistManager() {
|
||||
<>
|
||||
<div className="col-span-1 lg:col-span-2 relative flex items-center my-6">
|
||||
<div className="flex-grow border-t border-gray-300 dark:border-gray-600" />
|
||||
<span className="mx-4 text-sm text-gray-500 dark:text-gray-400">Archived</span>
|
||||
<span className="mx-4 text-sm text-gray-500 dark:text-gray-400">{t('archivedSectionTitle')}</span>
|
||||
<div className="flex-grow border-t border-gray-300 dark:border-gray-600" />
|
||||
</div>
|
||||
{archivedItems.map((item) => (
|
||||
@@ -167,9 +169,9 @@ export default function WishlistManager() {
|
||||
}
|
||||
setDeleteConfirmation({ isOpen: false, itemId: null })
|
||||
}}
|
||||
title="Delete Reward"
|
||||
message="Are you sure you want to delete this reward? This action cannot be undone."
|
||||
confirmText="Delete"
|
||||
title={t('deleteDialogTitle')}
|
||||
message={t('deleteDialogMessage')}
|
||||
confirmText={t('deleteButton')}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useAtom } from 'jotai'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { calculateCoinsEarnedToday, calculateCoinsSpentToday, calculateTotalEarned, calculateTotalSpent, calculateTransactionsToday, checkPermission } from '@/lib/utils'
|
||||
import {
|
||||
coinsAtom,
|
||||
@@ -19,12 +20,13 @@ import { useHelpers } from '@/lib/client-helpers'
|
||||
function handlePermissionCheck(
|
||||
user: User | undefined,
|
||||
resource: 'habit' | 'wishlist' | 'coins',
|
||||
action: 'write' | 'interact'
|
||||
action: 'write' | 'interact',
|
||||
tCommon: (key: string, values?: Record<string, any>) => string
|
||||
): boolean {
|
||||
if (!user) {
|
||||
toast({
|
||||
title: "Authentication Required",
|
||||
description: "Please sign in to continue.",
|
||||
title: tCommon("authenticationRequiredTitle"),
|
||||
description: tCommon("authenticationRequiredDescription"),
|
||||
variant: "destructive",
|
||||
})
|
||||
return false
|
||||
@@ -32,8 +34,8 @@ function handlePermissionCheck(
|
||||
|
||||
if (!user.isAdmin && !checkPermission(user.permissions, resource, action)) {
|
||||
toast({
|
||||
title: "Permission Denied",
|
||||
description: `You don't have ${action} permission for ${resource}s.`,
|
||||
title: tCommon("permissionDeniedTitle"),
|
||||
description: tCommon("permissionDeniedDescription", { action, resource }),
|
||||
variant: "destructive",
|
||||
})
|
||||
return false
|
||||
@@ -43,6 +45,8 @@ function handlePermissionCheck(
|
||||
}
|
||||
|
||||
export function useCoins(options?: { selectedUser?: string }) {
|
||||
const t = useTranslations('useCoins');
|
||||
const tCommon = useTranslations('Common');
|
||||
const [coins, setCoins] = useAtom(coinsAtom)
|
||||
const [settings] = useAtom(settingsAtom)
|
||||
const [users] = useAtom(usersAtom)
|
||||
@@ -65,11 +69,11 @@ export function useCoins(options?: { selectedUser?: string }) {
|
||||
const [transactionsToday] = useAtom(transactionsTodayAtom)
|
||||
|
||||
const add = async (amount: number, description: string, note?: string) => {
|
||||
if (!handlePermissionCheck(currentUser, 'coins', 'write')) return null
|
||||
if (!handlePermissionCheck(currentUser, 'coins', 'write', tCommon)) return null
|
||||
if (isNaN(amount) || amount <= 0) {
|
||||
toast({
|
||||
title: "Invalid amount",
|
||||
description: "Please enter a valid positive number"
|
||||
title: t("invalidAmountTitle"),
|
||||
description: t("invalidAmountDescription")
|
||||
})
|
||||
return null
|
||||
}
|
||||
@@ -82,17 +86,17 @@ export function useCoins(options?: { selectedUser?: string }) {
|
||||
userId: user?.id
|
||||
})
|
||||
setCoins(data)
|
||||
toast({ title: "Success", description: `Added ${amount} coins` })
|
||||
toast({ title: t("successTitle"), description: t("addedCoinsDescription", { amount }) })
|
||||
return data
|
||||
}
|
||||
|
||||
const remove = async (amount: number, description: string, note?: string) => {
|
||||
if (!handlePermissionCheck(currentUser, 'coins', 'write')) return null
|
||||
if (!handlePermissionCheck(currentUser, 'coins', 'write', tCommon)) return null
|
||||
const numAmount = Math.abs(amount)
|
||||
if (isNaN(numAmount) || numAmount <= 0) {
|
||||
toast({
|
||||
title: "Invalid amount",
|
||||
description: "Please enter a valid positive number"
|
||||
title: t("invalidAmountTitle"),
|
||||
description: t("invalidAmountDescription")
|
||||
})
|
||||
return null
|
||||
}
|
||||
@@ -105,17 +109,17 @@ export function useCoins(options?: { selectedUser?: string }) {
|
||||
userId: user?.id
|
||||
})
|
||||
setCoins(data)
|
||||
toast({ title: "Success", description: `Removed ${numAmount} coins` })
|
||||
toast({ title: t("successTitle"), description: t("removedCoinsDescription", { amount: numAmount }) })
|
||||
return data
|
||||
}
|
||||
|
||||
const updateNote = async (transactionId: string, note: string) => {
|
||||
if (!handlePermissionCheck(currentUser, 'coins', 'write')) return null
|
||||
if (!handlePermissionCheck(currentUser, 'coins', 'write', tCommon)) return null
|
||||
const transaction = coins.transactions.find(t => t.id === transactionId)
|
||||
if (!transaction) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Transaction not found"
|
||||
title: tCommon("errorTitle"),
|
||||
description: t("transactionNotFoundDescription")
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useAtom, atom } from 'jotai'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { habitsAtom, coinsAtom, settingsAtom, usersAtom, habitFreqMapAtom } from '@/lib/atoms'
|
||||
import { addCoins, removeCoins, saveHabitsData } from '@/app/actions/data'
|
||||
import { Habit, Permission, SafeUser, User } from '@/lib/types'
|
||||
@@ -24,12 +25,13 @@ import { useHelpers } from '@/lib/client-helpers'
|
||||
function handlePermissionCheck(
|
||||
user: SafeUser | undefined,
|
||||
resource: 'habit' | 'wishlist' | 'coins',
|
||||
action: 'write' | 'interact'
|
||||
action: 'write' | 'interact',
|
||||
tCommon: (key: string, values?: Record<string, any>) => string
|
||||
): boolean {
|
||||
if (!user) {
|
||||
toast({
|
||||
title: "Authentication Required",
|
||||
description: "Please sign in to continue.",
|
||||
title: tCommon("authenticationRequiredTitle"),
|
||||
description: tCommon("authenticationRequiredDescription"),
|
||||
variant: "destructive",
|
||||
})
|
||||
return false
|
||||
@@ -37,8 +39,8 @@ function handlePermissionCheck(
|
||||
|
||||
if (!user.isAdmin && !checkPermission(user.permissions, resource, action)) {
|
||||
toast({
|
||||
title: "Permission Denied",
|
||||
description: `You don't have ${action} permission for ${resource}s.`,
|
||||
title: tCommon("permissionDeniedTitle"),
|
||||
description: tCommon("permissionDeniedDescription", { action, resource }),
|
||||
variant: "destructive",
|
||||
})
|
||||
return false
|
||||
@@ -49,6 +51,8 @@ function handlePermissionCheck(
|
||||
|
||||
|
||||
export function useHabits() {
|
||||
const t = useTranslations('useHabits');
|
||||
const tCommon = useTranslations('Common');
|
||||
const [usersData] = useAtom(usersAtom)
|
||||
const { currentUser } = useHelpers()
|
||||
const [habitsData, setHabitsData] = useAtom(habitsAtom)
|
||||
@@ -57,7 +61,7 @@ export function useHabits() {
|
||||
const [habitFreqMap] = useAtom(habitFreqMapAtom)
|
||||
|
||||
const completeHabit = async (habit: Habit) => {
|
||||
if (!handlePermissionCheck(currentUser, 'habit', 'interact')) return
|
||||
if (!handlePermissionCheck(currentUser, 'habit', 'interact', tCommon)) return
|
||||
const timezone = settings.system.timezone
|
||||
const today = getTodayInTimezone(timezone)
|
||||
|
||||
@@ -72,8 +76,8 @@ export function useHabits() {
|
||||
// Check if already completed
|
||||
if (completionsToday >= target) {
|
||||
toast({
|
||||
title: "Already completed",
|
||||
description: `You've already completed this habit today.`,
|
||||
title: t("alreadyCompletedTitle"),
|
||||
description: t("alreadyCompletedDescription"),
|
||||
variant: "destructive",
|
||||
})
|
||||
return
|
||||
@@ -104,19 +108,19 @@ export function useHabits() {
|
||||
})
|
||||
isTargetReached && playSound()
|
||||
toast({
|
||||
title: "Completed!",
|
||||
description: `You earned ${habit.coinReward} coins.`,
|
||||
action: <ToastAction altText="Undo" className="gap-2" onClick={() => undoComplete(updatedHabit)}>
|
||||
<Undo2 className="h-4 w-4" />Undo
|
||||
title: t("completedTitle"),
|
||||
description: t("earnedCoinsDescription", { coinReward: habit.coinReward }),
|
||||
action: <ToastAction altText={tCommon('undoButton')} className="gap-2" onClick={() => undoComplete(updatedHabit)}>
|
||||
<Undo2 className="h-4 w-4" />{tCommon('undoButton')}
|
||||
</ToastAction>
|
||||
})
|
||||
setCoins(updatedCoins)
|
||||
} else {
|
||||
toast({
|
||||
title: "Progress!",
|
||||
description: `You've completed ${completionsToday + 1}/${target} times today.`,
|
||||
action: <ToastAction altText="Undo" className="gap-2" onClick={() => undoComplete(updatedHabit)}>
|
||||
<Undo2 className="h-4 w-4" />Undo
|
||||
title: t("progressTitle"),
|
||||
description: t("progressDescription", { count: completionsToday + 1, target }),
|
||||
action: <ToastAction altText={tCommon('undoButton')} className="gap-2" onClick={() => undoComplete(updatedHabit)}>
|
||||
<Undo2 className="h-4 w-4" />{tCommon('undoButton')}
|
||||
</ToastAction>
|
||||
})
|
||||
}
|
||||
@@ -131,7 +135,7 @@ export function useHabits() {
|
||||
}
|
||||
|
||||
const undoComplete = async (habit: Habit) => {
|
||||
if (!handlePermissionCheck(currentUser, 'habit', 'interact')) return
|
||||
if (!handlePermissionCheck(currentUser, 'habit', 'interact', tCommon)) return
|
||||
const timezone = settings.system.timezone
|
||||
const today = t2d({ timestamp: getTodayInTimezone(timezone), timezone })
|
||||
|
||||
@@ -170,14 +174,17 @@ export function useHabits() {
|
||||
}
|
||||
|
||||
toast({
|
||||
title: "Completion undone",
|
||||
description: `You have ${getCompletionsForDate({
|
||||
habit: updatedHabit,
|
||||
date: today,
|
||||
timezone
|
||||
})}/${target} completions today.`,
|
||||
action: <ToastAction altText="Redo" onClick={() => completeHabit(updatedHabit)}>
|
||||
<Undo2 className="h-4 w-4" />Redo
|
||||
title: t("completionUndoneTitle"),
|
||||
description: t("completionUndoneDescription", {
|
||||
count: getCompletionsForDate({
|
||||
habit: updatedHabit,
|
||||
date: today,
|
||||
timezone
|
||||
}),
|
||||
target
|
||||
}),
|
||||
action: <ToastAction altText={tCommon('redoButton')} onClick={() => completeHabit(updatedHabit)}>
|
||||
<Undo2 className="h-4 w-4" />{tCommon('redoButton')}
|
||||
</ToastAction>
|
||||
})
|
||||
|
||||
@@ -188,8 +195,8 @@ export function useHabits() {
|
||||
}
|
||||
} else {
|
||||
toast({
|
||||
title: "No completions to undo",
|
||||
description: "This habit hasn't been completed today.",
|
||||
title: t("noCompletionsToUndoTitle"),
|
||||
description: t("noCompletionsToUndoDescription"),
|
||||
variant: "destructive",
|
||||
})
|
||||
return
|
||||
@@ -197,7 +204,7 @@ export function useHabits() {
|
||||
}
|
||||
|
||||
const saveHabit = async (habit: Omit<Habit, 'id'> & { id?: string }) => {
|
||||
if (!handlePermissionCheck(currentUser, 'habit', 'write')) return
|
||||
if (!handlePermissionCheck(currentUser, 'habit', 'write', tCommon)) return
|
||||
const newHabit = {
|
||||
...habit,
|
||||
id: habit.id || getNowInMilliseconds().toString()
|
||||
@@ -212,7 +219,7 @@ export function useHabits() {
|
||||
}
|
||||
|
||||
const deleteHabit = async (id: string) => {
|
||||
if (!handlePermissionCheck(currentUser, 'habit', 'write')) return
|
||||
if (!handlePermissionCheck(currentUser, 'habit', 'write', tCommon)) return
|
||||
const updatedHabits = habitsData.habits.filter(h => h.id !== id)
|
||||
await saveHabitsData({ habits: updatedHabits })
|
||||
setHabitsData({ habits: updatedHabits })
|
||||
@@ -220,7 +227,7 @@ export function useHabits() {
|
||||
}
|
||||
|
||||
const completePastHabit = async (habit: Habit, date: DateTime) => {
|
||||
if (!handlePermissionCheck(currentUser, 'habit', 'interact')) return
|
||||
if (!handlePermissionCheck(currentUser, 'habit', 'interact', tCommon)) return
|
||||
const timezone = settings.system.timezone
|
||||
const dateKey = getISODate({ dateTime: date, timezone })
|
||||
|
||||
@@ -232,8 +239,8 @@ export function useHabits() {
|
||||
|
||||
if (completionsOnDate >= target) {
|
||||
toast({
|
||||
title: "Already completed",
|
||||
description: `This habit was already completed on ${d2s({ dateTime: date, timezone, format: 'yyyy-MM-dd' })}.`,
|
||||
title: t("alreadyCompletedPastDateTitle"),
|
||||
description: t("alreadyCompletedPastDateDescription", { dateKey: d2s({ dateTime: date, timezone, format: 'yyyy-MM-dd' }) }),
|
||||
variant: "destructive",
|
||||
})
|
||||
return
|
||||
@@ -273,12 +280,12 @@ export function useHabits() {
|
||||
}
|
||||
|
||||
toast({
|
||||
title: isTargetReached ? "Completed!" : "Progress!",
|
||||
title: isTargetReached ? t("completedTitle") : t("progressTitle"),
|
||||
description: isTargetReached
|
||||
? `You earned ${habit.coinReward} coins for ${dateKey}.`
|
||||
: `You've completed ${completionsOnDate + 1}/${target} times on ${dateKey}.`,
|
||||
action: <ToastAction altText="Undo" className="gap-2" onClick={() => undoComplete(updatedHabit)}>
|
||||
<Undo2 className="h-4 w-4" />Undo
|
||||
? t("earnedCoinsPastDateDescription", { coinReward: habit.coinReward, dateKey })
|
||||
: t("progressPastDateDescription", { count: completionsOnDate + 1, target, dateKey }),
|
||||
action: <ToastAction altText={tCommon('undoButton')} className="gap-2" onClick={() => undoComplete(updatedHabit)}>
|
||||
<Undo2 className="h-4 w-4" />{tCommon('undoButton')}
|
||||
</ToastAction>
|
||||
})
|
||||
|
||||
@@ -290,7 +297,7 @@ export function useHabits() {
|
||||
}
|
||||
|
||||
const archiveHabit = async (id: string) => {
|
||||
if (!handlePermissionCheck(currentUser, 'habit', 'write')) return
|
||||
if (!handlePermissionCheck(currentUser, 'habit', 'write', tCommon)) return
|
||||
const updatedHabits = habitsData.habits.map(h =>
|
||||
h.id === id ? { ...h, archived: true } : h
|
||||
)
|
||||
@@ -299,7 +306,7 @@ export function useHabits() {
|
||||
}
|
||||
|
||||
const unarchiveHabit = async (id: string) => {
|
||||
if (!handlePermissionCheck(currentUser, 'habit', 'write')) return
|
||||
if (!handlePermissionCheck(currentUser, 'habit', 'write', tCommon)) return
|
||||
const updatedHabits = habitsData.habits.map(h =>
|
||||
h.id === id ? { ...h, archived: false } : h
|
||||
)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useAtom } from 'jotai'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { wishlistAtom, coinsAtom } from '@/lib/atoms'
|
||||
import { saveWishlistItems, removeCoins } from '@/app/actions/data'
|
||||
import { toast } from '@/hooks/use-toast'
|
||||
@@ -9,14 +10,15 @@ import { useHelpers } from '@/lib/client-helpers'
|
||||
import { useCoins } from './useCoins'
|
||||
|
||||
function handlePermissionCheck(
|
||||
user: any,
|
||||
user: any, // Consider using a more specific type like SafeUser | User | undefined
|
||||
resource: 'habit' | 'wishlist' | 'coins',
|
||||
action: 'write' | 'interact'
|
||||
action: 'write' | 'interact',
|
||||
tCommon: (key: string, values?: Record<string, any>) => string
|
||||
): boolean {
|
||||
if (!user) {
|
||||
toast({
|
||||
title: "Authentication Required",
|
||||
description: "Please sign in to continue.",
|
||||
title: tCommon("authenticationRequiredTitle"),
|
||||
description: tCommon("authenticationRequiredDescription"),
|
||||
variant: "destructive",
|
||||
})
|
||||
return false
|
||||
@@ -24,8 +26,8 @@ function handlePermissionCheck(
|
||||
|
||||
if (!user.isAdmin && !checkPermission(user.permissions, resource, action)) {
|
||||
toast({
|
||||
title: "Permission Denied",
|
||||
description: `You don't have ${action} permission for ${resource}s.`,
|
||||
title: tCommon("permissionDeniedTitle"),
|
||||
description: tCommon("permissionDeniedDescription", { action, resource }),
|
||||
variant: "destructive",
|
||||
})
|
||||
return false
|
||||
@@ -35,13 +37,15 @@ function handlePermissionCheck(
|
||||
}
|
||||
|
||||
export function useWishlist() {
|
||||
const t = useTranslations('useWishlist');
|
||||
const tCommon = useTranslations('Common');
|
||||
const { currentUser: user } = useHelpers()
|
||||
const [wishlist, setWishlist] = useAtom(wishlistAtom)
|
||||
const [coins, setCoins] = useAtom(coinsAtom)
|
||||
const { balance } = useCoins()
|
||||
|
||||
const addWishlistItem = async (item: Omit<WishlistItemType, 'id'>) => {
|
||||
if (!handlePermissionCheck(user, 'wishlist', 'write')) return
|
||||
if (!handlePermissionCheck(user, 'wishlist', 'write', tCommon)) return
|
||||
const newItem = { ...item, id: Date.now().toString() }
|
||||
const newItems = [...wishlist.items, newItem]
|
||||
const newWishListData = { items: newItems }
|
||||
@@ -50,7 +54,7 @@ export function useWishlist() {
|
||||
}
|
||||
|
||||
const editWishlistItem = async (updatedItem: WishlistItemType) => {
|
||||
if (!handlePermissionCheck(user, 'wishlist', 'write')) return
|
||||
if (!handlePermissionCheck(user, 'wishlist', 'write', tCommon)) return
|
||||
const newItems = wishlist.items.map(item =>
|
||||
item.id === updatedItem.id ? updatedItem : item
|
||||
)
|
||||
@@ -60,7 +64,7 @@ export function useWishlist() {
|
||||
}
|
||||
|
||||
const deleteWishlistItem = async (id: string) => {
|
||||
if (!handlePermissionCheck(user, 'wishlist', 'write')) return
|
||||
if (!handlePermissionCheck(user, 'wishlist', 'write', tCommon)) return
|
||||
const newItems = wishlist.items.filter(item => item.id !== id)
|
||||
const newWishListData = { items: newItems }
|
||||
setWishlist(newWishListData)
|
||||
@@ -68,13 +72,13 @@ export function useWishlist() {
|
||||
}
|
||||
|
||||
const redeemWishlistItem = async (item: WishlistItemType) => {
|
||||
if (!handlePermissionCheck(user, 'wishlist', 'interact')) return false
|
||||
if (!handlePermissionCheck(user, 'wishlist', 'interact', tCommon)) return false
|
||||
if (balance >= item.coinCost) {
|
||||
// Check if item has target completions and if we've reached the limit
|
||||
if (item.targetCompletions && item.targetCompletions <= 0) {
|
||||
toast({
|
||||
title: "Redemption limit reached",
|
||||
description: `You've reached the maximum redemptions for "${item.name}".`,
|
||||
title: t("redemptionLimitReachedTitle"),
|
||||
description: t("redemptionLimitReachedDescription", { itemName: item.name }),
|
||||
variant: "destructive",
|
||||
})
|
||||
return false
|
||||
@@ -121,15 +125,15 @@ export function useWishlist() {
|
||||
randomEffect()
|
||||
|
||||
toast({
|
||||
title: "🎉 Reward Redeemed!",
|
||||
description: `You've redeemed "${item.name}" for ${item.coinCost} coins.`,
|
||||
title: t("rewardRedeemedTitle"),
|
||||
description: t("rewardRedeemedDescription", { itemName: item.name, itemCoinCost: item.coinCost }),
|
||||
})
|
||||
|
||||
return true
|
||||
} else {
|
||||
toast({
|
||||
title: "Not enough coins",
|
||||
description: `You need ${item.coinCost - balance} more coins to redeem this reward.`,
|
||||
title: t("notEnoughCoinsTitle"),
|
||||
description: t("notEnoughCoinsDescription", { coinsNeeded: item.coinCost - balance }),
|
||||
variant: "destructive",
|
||||
})
|
||||
return false
|
||||
@@ -139,7 +143,7 @@ export function useWishlist() {
|
||||
const canRedeem = (cost: number) => balance >= cost
|
||||
|
||||
const archiveWishlistItem = async (id: string) => {
|
||||
if (!handlePermissionCheck(user, 'wishlist', 'write')) return
|
||||
if (!handlePermissionCheck(user, 'wishlist', 'write', tCommon)) return
|
||||
const newItems = wishlist.items.map(item =>
|
||||
item.id === id ? { ...item, archived: true } : item
|
||||
)
|
||||
@@ -149,7 +153,7 @@ export function useWishlist() {
|
||||
}
|
||||
|
||||
const unarchiveWishlistItem = async (id: string) => {
|
||||
if (!handlePermissionCheck(user, 'wishlist', 'write')) return
|
||||
if (!handlePermissionCheck(user, 'wishlist', 'write', tCommon)) return
|
||||
const newItems = wishlist.items.map(item =>
|
||||
item.id === id ? { ...item, archived: false } : item
|
||||
)
|
||||
|
||||
13
i18n/request.ts
Normal file
13
i18n/request.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { getRequestConfig } from 'next-intl/server';
|
||||
import { loadSettings } from '@/app/actions/data'; // Adjust path as necessary
|
||||
|
||||
export default getRequestConfig(async () => {
|
||||
// Load settings to get the user's preferred language
|
||||
const settings = await loadSettings();
|
||||
const locale = settings.system.language || 'en'; // Fallback to 'en' if not set
|
||||
|
||||
return {
|
||||
locale,
|
||||
messages: (await import(`../messages/${locale}.json`)).default
|
||||
};
|
||||
});
|
||||
@@ -132,6 +132,7 @@ export const getDefaultSettings = (): Settings => ({
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
weekStartDay: 1, // Monday
|
||||
autoBackupEnabled: true, // Add this line (default to true)
|
||||
language: 'en', // Default language
|
||||
},
|
||||
profile: {}
|
||||
});
|
||||
@@ -163,6 +164,7 @@ export interface SystemSettings {
|
||||
timezone: string;
|
||||
weekStartDay: WeekDay;
|
||||
autoBackupEnabled: boolean; // Add this line
|
||||
language: string; // Add this line for language preference
|
||||
}
|
||||
|
||||
export interface ProfileSettings {
|
||||
|
||||
407
messages/de.json
Normal file
407
messages/de.json
Normal file
@@ -0,0 +1,407 @@
|
||||
{
|
||||
"Dashboard": {
|
||||
"title": "Dashboard"
|
||||
},
|
||||
"HabitList": {
|
||||
"myTasks": "Meine Aufgaben",
|
||||
"myHabits": "Meine Gewohnheiten",
|
||||
"addTaskButton": "Aufgabe hinzufügen",
|
||||
"addHabitButton": "Gewohnheit hinzufügen",
|
||||
"searchTasksPlaceholder": "Aufgaben suchen...",
|
||||
"searchHabitsPlaceholder": "Gewohnheiten suchen...",
|
||||
"sortByLabel": "Sortieren nach:",
|
||||
"sortByName": "Name",
|
||||
"sortByCoinReward": "Münzbelohnung",
|
||||
"sortByDueDate": "Fälligkeitsdatum",
|
||||
"sortByFrequency": "Häufigkeit",
|
||||
"toggleSortOrderAriaLabel": "Sortierreihenfolge umkehren",
|
||||
"noTasksFoundMessage": "Keine Aufgaben gefunden, die Ihrer Suche entsprechen.",
|
||||
"noHabitsFoundMessage": "Keine Gewohnheiten gefunden, die Ihrer Suche entsprechen.",
|
||||
"emptyStateTasksTitle": "Noch keine Aufgaben",
|
||||
"emptyStateHabitsTitle": "Noch keine Gewohnheiten",
|
||||
"emptyStateTasksDescription": "Erstellen Sie Ihre erste Aufgabe, um Ihren Fortschritt zu verfolgen",
|
||||
"emptyStateHabitsDescription": "Erstellen Sie Ihre erste Gewohnheit, um Ihren Fortschritt zu verfolgen",
|
||||
"archivedSectionTitle": "Archiviert",
|
||||
"deleteTaskDialogTitle": "Aufgabe löschen",
|
||||
"deleteHabitDialogTitle": "Gewohnheit löschen",
|
||||
"deleteTaskDialogMessage": "Sind Sie sicher, dass Sie diese Aufgabe löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"deleteHabitDialogMessage": "Sind Sie sicher, dass Sie diese Gewohnheit löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"deleteButton": "Löschen"
|
||||
},
|
||||
"DailyOverview": {
|
||||
"addTaskButtonLabel": "Aufgabe hinzufügen",
|
||||
"addHabitButtonLabel": "Gewohnheit hinzufügen",
|
||||
"todaysOverviewTitle": "Heutige Übersicht",
|
||||
"dailyTasksTitle": "Tägliche Aufgaben",
|
||||
"noTasksDueTodayMessage": "Heute keine Aufgaben fällig. Fügen Sie Aufgaben hinzu, um zu beginnen!",
|
||||
"dailyHabitsTitle": "Tägliche Gewohnheiten",
|
||||
"noHabitsDueTodayMessage": "Heute keine Gewohnheiten fällig. Fügen Sie Gewohnheiten hinzu, um zu beginnen!",
|
||||
"wishlistGoalsTitle": "Wunschlisten-Ziele",
|
||||
"redeemableBadgeLabel": "{count}/{total} einlösbar",
|
||||
"noWishlistItemsMessage": "Noch keine Elemente auf der Wunschliste. Fügen Sie Ziele hinzu, auf die Sie hinarbeiten können!",
|
||||
"readyToRedeemMessage": "Bereit zum Einlösen!",
|
||||
"coinsToGoMessage": "Noch {amount} Münzen benötigt",
|
||||
"showLessButton": "Weniger anzeigen",
|
||||
"showAllButton": "Alles anzeigen",
|
||||
"viewButton": "Anzeigen",
|
||||
"deleteTaskDialogTitle": "Aufgabe löschen",
|
||||
"deleteHabitDialogTitle": "Gewohnheit löschen",
|
||||
"confirmDeleteDialogMessage": "Sind Sie sicher, dass Sie \"{name}\" löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"deleteButton": "Löschen",
|
||||
"overdueTooltip": "Überfällig"
|
||||
},
|
||||
"HabitContextMenuItems": {
|
||||
"startPomodoro": "Pomodoro starten",
|
||||
"moveToToday": "Auf heute verschieben",
|
||||
"moveToTomorrow": "Auf morgen verschieben",
|
||||
"unpin": "Abheften",
|
||||
"pin": "Anheften",
|
||||
"edit": "Bearbeiten",
|
||||
"archive": "Archivieren",
|
||||
"unarchive": "Dearchivieren",
|
||||
"delete": "Löschen"
|
||||
},
|
||||
"HabitStreak": {
|
||||
"dailyCompletionStreakTitle": "Tägliche Abschluss-Serie",
|
||||
"tooltipHabitsLabel": "Gewohnheiten",
|
||||
"tooltipTasksLabel": "Aufgaben",
|
||||
"tooltipCompletedLabel": "Abgeschlossen"
|
||||
},
|
||||
"CoinBalance": {
|
||||
"coinBalanceTitle": "Münzguthaben"
|
||||
},
|
||||
"AddEditHabitModal": {
|
||||
"editTaskTitle": "Aufgabe bearbeiten",
|
||||
"editHabitTitle": "Gewohnheit bearbeiten",
|
||||
"addNewTaskTitle": "Neue Aufgabe hinzufügen",
|
||||
"addNewHabitTitle": "Neue Gewohnheit hinzufügen",
|
||||
"nameLabel": "Name *",
|
||||
"descriptionLabel": "Beschreibung",
|
||||
"whenLabel": "Wann *",
|
||||
"completeLabel": "Abschließen",
|
||||
"timesSuffix": "mal",
|
||||
"rewardLabel": "Belohnung",
|
||||
"coinsSuffix": "Münzen",
|
||||
"shareLabel": "Teilen",
|
||||
"saveChangesButton": "Änderungen speichern",
|
||||
"addTaskButton": "Aufgabe hinzufügen",
|
||||
"addHabitButton": "Gewohnheit hinzufügen"
|
||||
},
|
||||
"ConfirmDialog": {
|
||||
"confirmButton": "Bestätigen",
|
||||
"cancelButton": "Abbrechen"
|
||||
},
|
||||
"AddEditWishlistItemModal": {
|
||||
"editTitle": "Belohnung bearbeiten",
|
||||
"addTitle": "Neue Belohnung hinzufügen",
|
||||
"nameLabel": "Name *",
|
||||
"descriptionLabel": "Beschreibung",
|
||||
"costLabel": "Kosten",
|
||||
"coinsSuffix": "Münzen",
|
||||
"redeemableLabel": "Einlösbar",
|
||||
"timesSuffix": "mal",
|
||||
"errorNameRequired": "Name ist erforderlich",
|
||||
"errorCoinCostMin": "Münzkosten müssen mindestens 1 sein",
|
||||
"errorTargetCompletionsMin": "Zielabschlüsse müssen mindestens 1 sein",
|
||||
"errorInvalidUrl": "Bitte geben Sie eine gültige URL ein",
|
||||
"linkLabel": "Link",
|
||||
"shareLabel": "Teilen",
|
||||
"saveButton": "Änderungen speichern",
|
||||
"addButton": "Belohnung hinzufügen"
|
||||
},
|
||||
"Navigation": {
|
||||
"dashboard": "Dashboard",
|
||||
"tasks": "Aufgaben",
|
||||
"habits": "Gewohnheiten",
|
||||
"calendar": "Kalender",
|
||||
"wishlist": "Wunschliste",
|
||||
"coins": "Münzen"
|
||||
},
|
||||
"TodayEarnedCoins": {
|
||||
"todaySuffix": "heute"
|
||||
},
|
||||
"WishlistItem": {
|
||||
"usesLeftSingular": "Verwendung übrig",
|
||||
"usesLeftPlural": "Verwendungen übrig",
|
||||
"coinsSuffix": "Münzen",
|
||||
"redeem": "Einlösen",
|
||||
"redeemedDone": "Erledigt",
|
||||
"redeemedExclamation": "Eingelöst!",
|
||||
"editButton": "Bearbeiten",
|
||||
"archiveButton": "Archivieren",
|
||||
"unarchiveButton": "Dearchivieren",
|
||||
"deleteButton": "Löschen"
|
||||
},
|
||||
"WishlistManager": {
|
||||
"title": "Meine Wunschliste",
|
||||
"addRewardButton": "Belohnung hinzufügen",
|
||||
"emptyStateTitle": "Ihre Wunschliste ist leer",
|
||||
"emptyStateDescription": "Fügen Sie Belohnungen hinzu, die Sie mit Ihren Münzen verdienen möchten",
|
||||
"archivedSectionTitle": "Archiviert",
|
||||
"popupBlockedTitle": "Popup blockiert",
|
||||
"popupBlockedDescription": "Bitte erlauben Sie Popups, um den Link zu öffnen",
|
||||
"deleteDialogTitle": "Belohnung löschen",
|
||||
"deleteDialogMessage": "Sind Sie sicher, dass Sie diese Belohnung löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"deleteButton": "Löschen"
|
||||
},
|
||||
"UserSelectModal": {
|
||||
"addUserButton": "Benutzer hinzufügen",
|
||||
"createNewUserTitle": "Neuen Benutzer erstellen",
|
||||
"selectUserTitle": "Benutzer auswählen",
|
||||
"signInSuccessTitle": "Erfolgreich angemeldet",
|
||||
"signInSuccessDescription": "Willkommen zurück, {username}!",
|
||||
"errorInvalidPassword": "Ungültiges Passwort"
|
||||
},
|
||||
"CoinsManager": {
|
||||
"title": "Münzverwaltung",
|
||||
"currentBalanceLabel": "Aktuelles Guthaben",
|
||||
"coinsSuffix": "Münzen",
|
||||
"addCoinsButton": "Münzen hinzufügen",
|
||||
"removeCoinsButton": "Münzen entfernen",
|
||||
"statisticsTitle": "Statistiken",
|
||||
"totalEarnedLabel": "Gesamt verdient",
|
||||
"totalSpentLabel": "Gesamt ausgegeben",
|
||||
"totalTransactionsLabel": "Gesamt Transaktionen",
|
||||
"todaysEarnedLabel": "Heute verdient",
|
||||
"todaysSpentLabel": "Heute ausgegeben",
|
||||
"todaysTransactionsLabel": "Heutige Transaktionen",
|
||||
"transactionHistoryTitle": "Transaktionsverlauf",
|
||||
"showLabel": "Anzeigen:",
|
||||
"entriesSuffix": "Einträge",
|
||||
"showingEntries": "Zeige {from} bis {to} von {total} Einträgen",
|
||||
"noTransactionsTitle": "Noch keine Transaktionen",
|
||||
"noTransactionsDescription": "Ihr Transaktionsverlauf wird hier angezeigt, sobald Sie beginnen, Münzen zu verdienen oder auszugeben",
|
||||
"pageLabel": "Seite",
|
||||
"ofLabel": "von",
|
||||
"transactionTypeHabitCompletion": "Gewohnheitsabschluss",
|
||||
"transactionTypeTaskCompletion": "Aufgabenabschluss",
|
||||
"transactionTypeHabitUndo": "Gewohnheitsrückgängig",
|
||||
"transactionTypeTaskUndo": "Aufgabenrückgängig",
|
||||
"transactionTypeWishRedemption": "Wunscherfüllung",
|
||||
"transactionTypeManualAdjustment": "Manuelle Anpassung",
|
||||
"transactionTypeCoinReset": "Münzrücksetzung",
|
||||
"transactionTypeInitialBalance": "Anfangsguthaben"
|
||||
},
|
||||
"NotificationBell": {
|
||||
"errorUpdateTimestamp": "Fehler beim Aktualisieren des gelesenen Benachrichtigungszeitstempels:"
|
||||
},
|
||||
"PomodoroTimer": {
|
||||
"focusLabel1": "Bleiben Sie konzentriert",
|
||||
"focusLabel2": "Sie schaffen das",
|
||||
"focusLabel3": "Machen Sie weiter",
|
||||
"focusLabel4": "Zerquetschen Sie es",
|
||||
"focusLabel5": "Lassen Sie es geschehen",
|
||||
"focusLabel6": "Bleiben Sie stark",
|
||||
"focusLabel7": "Durchhalten",
|
||||
"focusLabel8": "Ein Schritt nach dem anderen",
|
||||
"focusLabel9": "Sie können es schaffen",
|
||||
"focusLabel10": "Konzentrieren und Erobern",
|
||||
"breakLabel1": "Machen Sie eine Pause",
|
||||
"breakLabel2": "Entspannen und Aufladen",
|
||||
"breakLabel3": "Tief durchatmen",
|
||||
"breakLabel4": "Dehnen Sie sich",
|
||||
"breakLabel5": "Erfrischen Sie sich",
|
||||
"breakLabel6": "Sie verdienen dies",
|
||||
"breakLabel7": "Laden Sie Ihre Energie auf",
|
||||
"breakLabel8": "Gehen Sie kurz weg",
|
||||
"breakLabel9": "Klaren Sie Ihren Geist",
|
||||
"breakLabel10": "Ruhen Sie sich aus und erfrischen Sie sich",
|
||||
"focusType": "Konzentration",
|
||||
"breakType": "Pause",
|
||||
"pauseButton": "Pause",
|
||||
"startButton": "Start",
|
||||
"resetButton": "Zurücksetzen",
|
||||
"skipButton": "Überspringen",
|
||||
"wakeLockNotSupported": "Browser unterstützt WakeLock nicht",
|
||||
"wakeLockInUse": "WakeLock bereits in Benutzung",
|
||||
"wakeLockRequestError": "Fehler beim Anfordern des WakeLock:",
|
||||
"wakeLockReleaseError": "Fehler beim Freigeben des WakeLock:"
|
||||
},
|
||||
"HabitCalendar": {
|
||||
"title": "Gewohnheitskalender",
|
||||
"calendarCardTitle": "Kalender",
|
||||
"selectDatePrompt": "Wählen Sie ein Datum",
|
||||
"tasksSectionTitle": "Aufgaben",
|
||||
"habitsSectionTitle": "Gewohnheiten",
|
||||
"errorCompletingPastHabit": "Fehler beim Abschließen vergangener Gewohnheit:"
|
||||
},
|
||||
"NotificationDropdown": {
|
||||
"notLoggedIn": "Nicht eingeloggt.",
|
||||
"userCompletedItem": "{username} hat {itemName} abgeschlossen.",
|
||||
"userRedeemedItem": "{username} hat {itemName} eingelöst.",
|
||||
"activityRelatedToItem": "Aktivität bezüglich {itemName} von {username}.",
|
||||
"defaultUsername": "Jemand",
|
||||
"defaultItemName": "ein geteilter Gegenstand",
|
||||
"notificationsTitle": "Benachrichtungen",
|
||||
"notificationsTooltip": "Zeigt Abschlüsse oder Einlösungen von anderen Benutzern für Gewohnheiten oder Wunschlisten, die Sie mit ihnen geteilt haben (Sie müssen Admin sein)",
|
||||
"noNotificationsYet": "Noch keine Benachrichtigkeiten"
|
||||
},
|
||||
"AboutModal": {
|
||||
"dialogArisLabel": "über",
|
||||
"changelogButton": "Änderungsprotokoll",
|
||||
"createdByPrefix": "Erstellt mit ❤️ von",
|
||||
"starOnGitHubButton": "Auf GitHub bewerten"
|
||||
},
|
||||
"PermissionSelector": {
|
||||
"permissionsTitle": "Berechtigungen",
|
||||
"adminAccessLabel": "Admin-Zugriff",
|
||||
"adminAccessDescription": "Admins haben uneingeschränkte Berechtigungen für alle Daten aller Benutzer",
|
||||
"resourceHabitTask": "Gewohnheit / Aufgabe",
|
||||
"resourceWishlist": "Wunschliste",
|
||||
"resourceCoins": "Münzen",
|
||||
"permissionWrite": "Schreiben",
|
||||
"permissionInteract": "Interagieren"
|
||||
},
|
||||
"UserForm": {
|
||||
"toastUserUpdatedTitle": "Benutzer aktualisiert",
|
||||
"toastUserUpdatedDescription": "Benutzer {username} erfolgreich aktualisiert",
|
||||
"toastUserCreatedTitle": "Benutzer erstellt",
|
||||
"toastUserCreatedDescription": "Benutzer {username} erfolgreich erstellt",
|
||||
"actionUpdate": "aktualisieren",
|
||||
"actionCreate": "erstellen",
|
||||
"errorFailedUserAction": "Fehler beim {action} des Benutzers",
|
||||
"errorTitle": "Fehler",
|
||||
"errorFileSizeLimit": "Die Dateigröße muss kleiner als 5MB sein",
|
||||
"toastAvatarUploadedTitle": "Avatar hochgeladen",
|
||||
"toastAvatarUploadedDescription": "Avatar erfolgreich hochgeladen",
|
||||
"errorFailedAvatarUpload": "Fehler beim Hochladen des Avatars",
|
||||
"changeAvatarButton": "Avatar ändern",
|
||||
"uploadAvatarButton": "Avatar hochladen",
|
||||
"usernameLabel": "Benutzername",
|
||||
"usernamePlaceholder": "Benutzername",
|
||||
"newPasswordLabel": "Neues Passwort",
|
||||
"passwordLabel": "Passwort",
|
||||
"passwordPlaceholderEdit": "Leerlassen, um das aktuelle beizubehalten",
|
||||
"passwordPlaceholderCreate": "Passwort eingeben",
|
||||
"demoPasswordDisabledMessage": "Passwort ist in der Demo-Instanz automatisch deaktiviert",
|
||||
"disablePasswordLabel": "Passwort deaktivieren",
|
||||
"cancelButton": "Abbrechen",
|
||||
"saveChangesButton": "Änderungen speichern",
|
||||
"createUserButton": "Benutzer erstellen"
|
||||
},
|
||||
"ViewToggle": {
|
||||
"habitsLabel": "Gewohnheiten",
|
||||
"tasksLabel": "Aufgaben"
|
||||
},
|
||||
"HabitItem": {
|
||||
"overdue": "Überfällig",
|
||||
"whenLabel": "Wann: {frequency}",
|
||||
"coinsPerCompletion": "{count} Münzen pro Abschluss",
|
||||
"completedStatus": "Abgeschlossen",
|
||||
"completedStatusCount": "Abgeschlossen ({completed}/{target})",
|
||||
"completedStatusCountMobile": "{completed}/{target}",
|
||||
"completeButton": "Abschließen",
|
||||
"completeButtonCount": "Abschließen ({completed}/{target})",
|
||||
"completeButtonCountMobile": "{completed}/{target}",
|
||||
"undoButton": "Rückgängig",
|
||||
"editButton": "Bearbeiten"
|
||||
},
|
||||
"TransactionNoteEditor": {
|
||||
"noteTooLongTitle": "Notiz zu lang",
|
||||
"noteTooLongDescription": "Notizen müssen weniger als 200 Zeichen haben",
|
||||
"errorSavingNoteTitle": "Fehler beim Speichern der Notiz",
|
||||
"errorDeletingNoteTitle": "Fehler beim Löschen der Notiz",
|
||||
"pleaseTryAgainDescription": "Bitte versuchen Sie es erneut",
|
||||
"addNotePlaceholder": "Notiz hinzufügen...",
|
||||
"saveNoteTitle": "Notiz speichern",
|
||||
"cancelButtonTitle": "Abbrechen",
|
||||
"deleteNoteTitle": "Notiz löschen",
|
||||
"editNoteAriaLabel": "Notiz bearbeiten"
|
||||
},
|
||||
"Profile": {
|
||||
"guestUsername": "Gast",
|
||||
"editProfileButton": "Profil bearbeiten",
|
||||
"signOutSuccessTitle": "Erfolgreich abgemeldet",
|
||||
"signOutSuccessDescription": "Sie wurden von Ihrem Konto abgemeldet",
|
||||
"signOutErrorTitle": "Abmeldefehler",
|
||||
"signOutErrorDescription": "Abmeldung fehlgeschlagen",
|
||||
"switchUserButton": "Benutzer wechseln",
|
||||
"settingsLink": "Einstellungen",
|
||||
"aboutButton": "Über",
|
||||
"themeLabel": "Thema",
|
||||
"editProfileModalTitle": "Profil bearbeiten"
|
||||
},
|
||||
"PasswordEntryForm": {
|
||||
"notYouButton": "Sind Sie es nicht?",
|
||||
"passwordLabel": "Passwort",
|
||||
"passwordPlaceholder": "Passwort eingeben",
|
||||
"loginErrorToastTitle": "Fehler",
|
||||
"loginFailedErrorToastDescription": "Anmeldung fehlgeschlagen",
|
||||
"cancelButton": "Abbrechen",
|
||||
"loginButton": "Anmelden"
|
||||
},
|
||||
"CompletionCountBadge": {
|
||||
"countCompleted": "{completedCount}/{totalCount} abgeschlossen"
|
||||
},
|
||||
"SettingsPage": {
|
||||
"title": "Einstellungen",
|
||||
"uiSettingsTitle": "UI-Einstellungen",
|
||||
"numberFormattingLabel": "Zahlenformatierung",
|
||||
"numberFormattingDescription": "Große Zahlen formatieren (z.B. 1K, 1M, 1B)",
|
||||
"numberGroupingLabel": "Zahlengruppierung",
|
||||
"numberGroupingDescription": "Tausendertrennzeichen verwenden (z.B. 1,000 vs 1000)",
|
||||
"systemSettingsTitle": "Systemeinstellungen",
|
||||
"timezoneLabel": "Zeitzone",
|
||||
"timezoneDescription": "Wählen Sie Ihre Zeitzone für eine genaue Datumsverfolgung",
|
||||
"weekStartDayLabel": "Wochenstarttag",
|
||||
"weekStartDayDescription": "Wählen Sie Ihren bevorzugten ersten Tag der Woche",
|
||||
"weekdays": {
|
||||
"sunday": "Sonntag",
|
||||
"monday": "Montag",
|
||||
"tuesday": "Dienstag",
|
||||
"wednesday": "Mittwoch",
|
||||
"thursday": "Donnerstag",
|
||||
"friday": "Freitag",
|
||||
"saturday": "Samstag"
|
||||
},
|
||||
"autoBackupLabel": "Automatische Sicherung",
|
||||
"autoBackupTooltip": "Wenn aktiviert, werden die Anwendungsdaten (Gewohnheiten, Münzen, Einstellungen usw.) täglich um 2 Uhr morgens Serverzeit automatisch gesichert. Backups werden als ZIP-Dateien im Verzeichnis `backups/` im Projektstamm gespeichert. Nur die letzten 7 Backups werden gespeichert; ältere werden automatisch gelöscht.",
|
||||
"autoBackupDescription": "Daten automatisch täglich sichern",
|
||||
"languageLabel": "Sprache",
|
||||
"languageDescription": "Wählen Sie Ihre bevorzugte Anzeigesprache für die Anwendung.",
|
||||
"languageChangedTitle": "Sprache geändert",
|
||||
"languageChangedDescription": "Bitte aktualisieren Sie die Seite, um die Änderungen zu sehen",
|
||||
"languageDisabledInDemoTooltip": "Das Ändern der Sprache ist in der Demoversion deaktiviert."
|
||||
},
|
||||
"Common": {
|
||||
"authenticationRequiredTitle": "Authentifizierung erforderlich",
|
||||
"authenticationRequiredDescription": "Bitte melden Sie sich an, um fortzufahren.",
|
||||
"permissionDeniedTitle": "Berechtigung verweigert",
|
||||
"permissionDeniedDescription": "Sie haben keine {action}-Berechtigung für {resource}.",
|
||||
"undoButton": "Rückgängig",
|
||||
"redoButton": "Wiederholen",
|
||||
"errorTitle": "Fehler"
|
||||
},
|
||||
"useHabits": {
|
||||
"alreadyCompletedTitle": "Schon abgeschlossen",
|
||||
"alreadyCompletedDescription": "Sie haben diese Gewohnheit heute bereits abgeschlossen.",
|
||||
"completedTitle": "Abgeschlossen!",
|
||||
"earnedCoinsDescription": "Sie haben {coinReward} Münzen verdient.",
|
||||
"progressTitle": "Fortschritt!",
|
||||
"progressDescription": "Sie haben {count}/{target} mal heute abgeschlossen.",
|
||||
"completionUndoneTitle": "Abschluss rückgängig gemacht",
|
||||
"completionUndounDescription": "Sie haben {count}/{target} Abschlüsse heute.",
|
||||
"noCompletionsToUndoTitle": "Keine Abschlüsse zum Rückgängigmachen",
|
||||
"noCompletionsToUndoDescription": "Diese Gewohnheit wurde heute nicht abgeschlossen.",
|
||||
"alreadyCompletedPastDateTitle": "Schon abgeschlossen",
|
||||
"alreadyCompletedPastDateDescription": "Diese Gewohnheit wurde bereits am {dateKey} abgeschlossen.",
|
||||
"earnedCoinsPastDateDescription": "Sie haben {coinReward} Münzen für {dateKey} verdient.",
|
||||
"progressPastDateDescription": "Sie haben {count}/{target} mal am {dateKey} abgeschlossen."
|
||||
},
|
||||
"useWishlist": {
|
||||
"redemptionLimitReachedTitle": "Einlösungslimit erreicht",
|
||||
"redemptionLimitReachedDescription": "Sie haben das maximale Einlösungslimit für \"{itemName}\" erreicht.",
|
||||
"rewardRedeemedTitle": "🎉 Belohnung eingelöst!",
|
||||
"rewardRedeemedDescription": "Sie haben \"{itemName}\" für {itemCoinCost} Münzen eingelöst.",
|
||||
"notEnoughCoinsTitle": "Nicht genug Münzen",
|
||||
"notEnoughCoinsDescription": "Sie benötigen {coinsNeeded} Münzen mehr, um diese Belohnung einzulösen."
|
||||
},
|
||||
"useCoins": {
|
||||
"invalidAmountTitle": "Ungültiger Betrag",
|
||||
"invalidAmountDescription": "Bitte geben Sie eine gültige positive Zahl ein",
|
||||
"successTitle": "Erfolg",
|
||||
"addedCoinsDescription": "{amount} Münzen hinzugefügt",
|
||||
"removedCoinsDescription": "{amount} Münzen entfernt",
|
||||
"transactionNotFoundDescription": "Transaktion nicht gefunden"
|
||||
}
|
||||
}
|
||||
407
messages/en.json
Normal file
407
messages/en.json
Normal file
@@ -0,0 +1,407 @@
|
||||
{
|
||||
"Dashboard": {
|
||||
"title": "Dashboard"
|
||||
},
|
||||
"HabitList": {
|
||||
"myTasks": "My Tasks",
|
||||
"myHabits": "My Habits",
|
||||
"addTaskButton": "Add Task",
|
||||
"addHabitButton": "Add Habit",
|
||||
"searchTasksPlaceholder": "Search tasks...",
|
||||
"searchHabitsPlaceholder": "Search habits...",
|
||||
"sortByLabel": "Sort by:",
|
||||
"sortByName": "Name",
|
||||
"sortByCoinReward": "Coin Reward",
|
||||
"sortByDueDate": "Due Date",
|
||||
"sortByFrequency": "Frequency",
|
||||
"toggleSortOrderAriaLabel": "Toggle sort order",
|
||||
"noTasksFoundMessage": "No tasks found matching your search.",
|
||||
"noHabitsFoundMessage": "No habits found matching your search.",
|
||||
"emptyStateTasksTitle": "No tasks yet",
|
||||
"emptyStateHabitsTitle": "No habits yet",
|
||||
"emptyStateTasksDescription": "Create your first task to start tracking your progress",
|
||||
"emptyStateHabitsDescription": "Create your first habit to start tracking your progress",
|
||||
"archivedSectionTitle": "Archived",
|
||||
"deleteTaskDialogTitle": "Delete Task",
|
||||
"deleteHabitDialogTitle": "Delete Habit",
|
||||
"deleteTaskDialogMessage": "Are you sure you want to delete this task? This action cannot be undone.",
|
||||
"deleteHabitDialogMessage": "Are you sure you want to delete this habit? This action cannot be undone.",
|
||||
"deleteButton": "Delete"
|
||||
},
|
||||
"DailyOverview": {
|
||||
"addTaskButtonLabel": "Add Task",
|
||||
"addHabitButtonLabel": "Add Habit",
|
||||
"todaysOverviewTitle": "Today's Overview",
|
||||
"dailyTasksTitle": "Daily Tasks",
|
||||
"noTasksDueTodayMessage": "No tasks due today. Add some tasks to get started!",
|
||||
"dailyHabitsTitle": "Daily Habits",
|
||||
"noHabitsDueTodayMessage": "No habits due today. Add some habits to get started!",
|
||||
"wishlistGoalsTitle": "Wishlist Goals",
|
||||
"redeemableBadgeLabel": "{count}/{total} Redeemable",
|
||||
"noWishlistItemsMessage": "No wishlist items yet. Add some goals to work towards!",
|
||||
"readyToRedeemMessage": "Ready to redeem!",
|
||||
"coinsToGoMessage": "{amount} coins to go",
|
||||
"showLessButton": "Show less",
|
||||
"showAllButton": "Show all",
|
||||
"viewButton": "View",
|
||||
"deleteTaskDialogTitle": "Delete Task",
|
||||
"deleteHabitDialogTitle": "Delete Habit",
|
||||
"confirmDeleteDialogMessage": "Are you sure you want to delete \"{name}\"? This action cannot be undone.",
|
||||
"deleteButton": "Delete",
|
||||
"overdueTooltip": "Overdue"
|
||||
},
|
||||
"HabitContextMenuItems": {
|
||||
"startPomodoro": "Start Pomodoro",
|
||||
"moveToToday": "Move to Today",
|
||||
"moveToTomorrow": "Move to Tomorrow",
|
||||
"unpin": "Unpin",
|
||||
"pin": "Pin",
|
||||
"edit": "Edit",
|
||||
"archive": "Archive",
|
||||
"unarchive": "Unarchive",
|
||||
"delete": "Delete"
|
||||
},
|
||||
"HabitStreak": {
|
||||
"dailyCompletionStreakTitle": "Daily Completion Streak",
|
||||
"tooltipHabitsLabel": "habits",
|
||||
"tooltipTasksLabel": "tasks",
|
||||
"tooltipCompletedLabel": "Completed"
|
||||
},
|
||||
"CoinBalance": {
|
||||
"coinBalanceTitle": "Coin Balance"
|
||||
},
|
||||
"AddEditHabitModal": {
|
||||
"editTaskTitle": "Edit Task",
|
||||
"editHabitTitle": "Edit Habit",
|
||||
"addNewTaskTitle": "Add New Task",
|
||||
"addNewHabitTitle": "Add New Habit",
|
||||
"nameLabel": "Name *",
|
||||
"descriptionLabel": "Description",
|
||||
"whenLabel": "When *",
|
||||
"completeLabel": "Complete",
|
||||
"timesSuffix": "times",
|
||||
"rewardLabel": "Reward",
|
||||
"coinsSuffix": "coins",
|
||||
"shareLabel": "Share",
|
||||
"saveChangesButton": "Save Changes",
|
||||
"addTaskButton": "Add Task",
|
||||
"addHabitButton": "Add Habit"
|
||||
},
|
||||
"ConfirmDialog": {
|
||||
"confirmButton": "Confirm",
|
||||
"cancelButton": "Cancel"
|
||||
},
|
||||
"AddEditWishlistItemModal": {
|
||||
"editTitle": "Edit Reward",
|
||||
"addTitle": "Add New Reward",
|
||||
"nameLabel": "Name *",
|
||||
"descriptionLabel": "Description",
|
||||
"costLabel": "Cost",
|
||||
"coinsSuffix": "coins",
|
||||
"redeemableLabel": "Redeemable",
|
||||
"timesSuffix": "times",
|
||||
"errorNameRequired": "Name is required",
|
||||
"errorCoinCostMin": "Coin cost must be at least 1",
|
||||
"errorTargetCompletionsMin": "Target completions must be at least 1",
|
||||
"errorInvalidUrl": "Please enter a valid URL",
|
||||
"linkLabel": "Link",
|
||||
"shareLabel": "Share",
|
||||
"saveButton": "Save Changes",
|
||||
"addButton": "Add Reward"
|
||||
},
|
||||
"Navigation": {
|
||||
"dashboard": "Dashboard",
|
||||
"tasks": "Tasks",
|
||||
"habits": "Habits",
|
||||
"calendar": "Calendar",
|
||||
"wishlist": "Wishlist",
|
||||
"coins": "Coins"
|
||||
},
|
||||
"TodayEarnedCoins": {
|
||||
"todaySuffix": "today"
|
||||
},
|
||||
"WishlistItem": {
|
||||
"usesLeftSingular": "use left",
|
||||
"usesLeftPlural": "uses left",
|
||||
"coinsSuffix": "coins",
|
||||
"redeem": "Redeem",
|
||||
"redeemedDone": "Done",
|
||||
"redeemedExclamation": "Redeemed!",
|
||||
"editButton": "Edit",
|
||||
"archiveButton": "Archive",
|
||||
"unarchiveButton": "Unarchive",
|
||||
"deleteButton": "Delete"
|
||||
},
|
||||
"WishlistManager": {
|
||||
"title": "My Wishlist",
|
||||
"addRewardButton": "Add Reward",
|
||||
"emptyStateTitle": "Your wishlist is empty",
|
||||
"emptyStateDescription": "Add rewards that you'd like to earn with your coins",
|
||||
"archivedSectionTitle": "Archived",
|
||||
"popupBlockedTitle": "Popup Blocked",
|
||||
"popupBlockedDescription": "Please allow popups to open the link",
|
||||
"deleteDialogTitle": "Delete Reward",
|
||||
"deleteDialogMessage": "Are you sure you want to delete this reward? This action cannot be undone.",
|
||||
"deleteButton": "Delete"
|
||||
},
|
||||
"UserSelectModal": {
|
||||
"addUserButton": "Add User",
|
||||
"createNewUserTitle": "Create New User",
|
||||
"selectUserTitle": "Select User",
|
||||
"signInSuccessTitle": "Signed in successfully",
|
||||
"signInSuccessDescription": "Welcome back, {username}!",
|
||||
"errorInvalidPassword": "invalid password"
|
||||
},
|
||||
"CoinsManager": {
|
||||
"title": "Coins Management",
|
||||
"currentBalanceLabel": "Current Balance",
|
||||
"coinsSuffix": "coins",
|
||||
"addCoinsButton": "Add Coins",
|
||||
"removeCoinsButton": "Remove Coins",
|
||||
"statisticsTitle": "Statistics",
|
||||
"totalEarnedLabel": "Total Earned",
|
||||
"totalSpentLabel": "Total Spent",
|
||||
"totalTransactionsLabel": "Total Transactions",
|
||||
"todaysEarnedLabel": "Today's Earned",
|
||||
"todaysSpentLabel": "Today's Spent",
|
||||
"todaysTransactionsLabel": "Today's Transactions",
|
||||
"transactionHistoryTitle": "Transaction History",
|
||||
"showLabel": "Show:",
|
||||
"entriesSuffix": "entries",
|
||||
"showingEntries": "Showing {from} to {to} of {total} entries",
|
||||
"noTransactionsTitle": "No transactions yet",
|
||||
"noTransactionsDescription": "Your transaction history will appear here once you start earning or spending coins",
|
||||
"pageLabel": "Page",
|
||||
"ofLabel": "of",
|
||||
"transactionTypeHabitCompletion": "Habit Completion",
|
||||
"transactionTypeTaskCompletion": "Task Completion",
|
||||
"transactionTypeHabitUndo": "Habit Undo",
|
||||
"transactionTypeTaskUndo": "Task Undo",
|
||||
"transactionTypeWishRedemption": "Wish Redemption",
|
||||
"transactionTypeManualAdjustment": "Manual Adjustment",
|
||||
"transactionTypeCoinReset": "Coin Reset",
|
||||
"transactionTypeInitialBalance": "Initial Balance"
|
||||
},
|
||||
"NotificationBell": {
|
||||
"errorUpdateTimestamp": "Failed to update notification read timestamp:"
|
||||
},
|
||||
"PomodoroTimer": {
|
||||
"focusLabel1": "Stay Focused",
|
||||
"focusLabel2": "You Got This",
|
||||
"focusLabel3": "Keep Going",
|
||||
"focusLabel4": "Crush It",
|
||||
"focusLabel5": "Make It Happen",
|
||||
"focusLabel6": "Stay Strong",
|
||||
"focusLabel7": "Push Through",
|
||||
"focusLabel8": "One Step at a Time",
|
||||
"focusLabel9": "You Can Do It",
|
||||
"focusLabel10": "Focus and Conquer",
|
||||
"breakLabel1": "Take a Break",
|
||||
"breakLabel2": "Relax and Recharge",
|
||||
"breakLabel3": "Breathe Deeply",
|
||||
"breakLabel4": "Stretch It Out",
|
||||
"breakLabel5": "Refresh Yourself",
|
||||
"breakLabel6": "You Deserve This",
|
||||
"breakLabel7": "Recharge Your Energy",
|
||||
"breakLabel8": "Step Away for a Bit",
|
||||
"breakLabel9": "Clear Your Mind",
|
||||
"breakLabel10": "Rest and Rejuvenate",
|
||||
"focusType": "Focus",
|
||||
"breakType": "Break",
|
||||
"pauseButton": "Pause",
|
||||
"startButton": "Start",
|
||||
"resetButton": "Reset",
|
||||
"skipButton": "Skip",
|
||||
"wakeLockNotSupported": "Browser does not support wakelock",
|
||||
"wakeLockInUse": "Wake lock already in use",
|
||||
"wakeLockRequestError": "Error requesting wake lock:",
|
||||
"wakeLockReleaseError": "Error releasing wake lock:"
|
||||
},
|
||||
"HabitCalendar": {
|
||||
"title": "Habit Calendar",
|
||||
"calendarCardTitle": "Calendar",
|
||||
"selectDatePrompt": "Select a date",
|
||||
"tasksSectionTitle": "Tasks",
|
||||
"habitsSectionTitle": "Habits",
|
||||
"errorCompletingPastHabit": "Error completing past habit:"
|
||||
},
|
||||
"NotificationDropdown": {
|
||||
"notLoggedIn": "Not logged in.",
|
||||
"userCompletedItem": "{username} completed {itemName}.",
|
||||
"userRedeemedItem": "{username} redeemed {itemName}.",
|
||||
"activityRelatedToItem": "Activity related to {itemName} by {username}.",
|
||||
"defaultUsername": "Someone",
|
||||
"defaultItemName": "a shared item",
|
||||
"notificationsTitle": "Notifications",
|
||||
"notificationsTooltip": "Shows completions or redemptions by other users for habits or wishlist that you shared with them (you must be admin)",
|
||||
"noNotificationsYet": "No notifications yet."
|
||||
},
|
||||
"AboutModal": {
|
||||
"dialogArisLabel": "about",
|
||||
"changelogButton": "Changelog",
|
||||
"createdByPrefix": "Created with ❤️ by",
|
||||
"starOnGitHubButton": "Star on GitHub"
|
||||
},
|
||||
"PermissionSelector": {
|
||||
"permissionsTitle": "Permissions",
|
||||
"adminAccessLabel": "Admin Access",
|
||||
"adminAccessDescription": "Admins have full permission to all data for all users",
|
||||
"resourceHabitTask": "Habit / Task",
|
||||
"resourceWishlist": "Wishlist",
|
||||
"resourceCoins": "Coins",
|
||||
"permissionWrite": "Write",
|
||||
"permissionInteract": "Interact"
|
||||
},
|
||||
"UserForm": {
|
||||
"toastUserUpdatedTitle": "User updated",
|
||||
"toastUserUpdatedDescription": "Successfully updated user {username}",
|
||||
"toastUserCreatedTitle": "User created",
|
||||
"toastUserCreatedDescription": "Successfully created user {username}",
|
||||
"actionUpdate": "update",
|
||||
"actionCreate": "create",
|
||||
"errorFailedUserAction": "Failed to {action} user",
|
||||
"errorTitle": "Error",
|
||||
"errorFileSizeLimit": "File size must be less than 5MB",
|
||||
"toastAvatarUploadedTitle": "Avatar uploaded",
|
||||
"toastAvatarUploadedDescription": "Successfully uploaded avatar",
|
||||
"errorFailedAvatarUpload": "Failed to upload avatar",
|
||||
"changeAvatarButton": "Change Avatar",
|
||||
"uploadAvatarButton": "Upload Avatar",
|
||||
"usernameLabel": "Username",
|
||||
"usernamePlaceholder": "Username",
|
||||
"newPasswordLabel": "New Password",
|
||||
"passwordLabel": "Password",
|
||||
"passwordPlaceholderEdit": "Leave blank to keep current",
|
||||
"passwordPlaceholderCreate": "Enter password",
|
||||
"demoPasswordDisabledMessage": "Password is automatically disabled in demo instance",
|
||||
"disablePasswordLabel": "Disable password",
|
||||
"cancelButton": "Cancel",
|
||||
"saveChangesButton": "Save Changes",
|
||||
"createUserButton": "Create User"
|
||||
},
|
||||
"ViewToggle": {
|
||||
"habitsLabel": "Habits",
|
||||
"tasksLabel": "Tasks"
|
||||
},
|
||||
"HabitItem": {
|
||||
"overdue": "Overdue",
|
||||
"whenLabel": "When: {frequency}",
|
||||
"coinsPerCompletion": "{count} coins per completion",
|
||||
"completedStatus": "Completed",
|
||||
"completedStatusCount": "Completed ({completed}/{target})",
|
||||
"completedStatusCountMobile": "{completed}/{target}",
|
||||
"completeButton": "Complete",
|
||||
"completeButtonCount": "Complete ({completed}/{target})",
|
||||
"completeButtonCountMobile": "{completed}/{target}",
|
||||
"undoButton": "Undo",
|
||||
"editButton": "Edit"
|
||||
},
|
||||
"TransactionNoteEditor": {
|
||||
"noteTooLongTitle": "Note too long",
|
||||
"noteTooLongDescription": "Notes must be less than 200 characters",
|
||||
"errorSavingNoteTitle": "Error saving note",
|
||||
"errorDeletingNoteTitle": "Error deleting note",
|
||||
"pleaseTryAgainDescription": "Please try again",
|
||||
"addNotePlaceholder": "Add a note...",
|
||||
"saveNoteTitle": "Save note",
|
||||
"cancelButtonTitle": "Cancel",
|
||||
"deleteNoteTitle": "Delete note",
|
||||
"editNoteAriaLabel": "Edit note"
|
||||
},
|
||||
"Profile": {
|
||||
"guestUsername": "Guest",
|
||||
"editProfileButton": "Edit profile",
|
||||
"signOutSuccessTitle": "Signed out successfully",
|
||||
"signOutSuccessDescription": "You have been logged out of your account",
|
||||
"signOutErrorTitle": "Sign Out Error",
|
||||
"signOutErrorDescription": "Failed to sign out",
|
||||
"switchUserButton": "Switch user",
|
||||
"settingsLink": "Settings",
|
||||
"aboutButton": "About",
|
||||
"themeLabel": "Theme",
|
||||
"editProfileModalTitle": "Edit Profile"
|
||||
},
|
||||
"PasswordEntryForm": {
|
||||
"notYouButton": "Not you?",
|
||||
"passwordLabel": "Password",
|
||||
"passwordPlaceholder": "Enter password",
|
||||
"loginErrorToastTitle": "Error",
|
||||
"loginFailedErrorToastDescription": "Login failed",
|
||||
"cancelButton": "Cancel",
|
||||
"loginButton": "Login"
|
||||
},
|
||||
"CompletionCountBadge": {
|
||||
"countCompleted": "{completedCount}/{totalCount} completed"
|
||||
},
|
||||
"SettingsPage": {
|
||||
"title": "Settings",
|
||||
"uiSettingsTitle": "UI Settings",
|
||||
"numberFormattingLabel": "Number Formatting",
|
||||
"numberFormattingDescription": "Format large numbers (e.g., 1K, 1M, 1B)",
|
||||
"numberGroupingLabel": "Number Grouping",
|
||||
"numberGroupingDescription": "Use thousand separators (e.g., 1,000 vs 1000)",
|
||||
"systemSettingsTitle": "System Settings",
|
||||
"timezoneLabel": "Timezone",
|
||||
"timezoneDescription": "Select your timezone for accurate date tracking",
|
||||
"weekStartDayLabel": "Week Start Day",
|
||||
"weekStartDayDescription": "Select your preferred first day of the week",
|
||||
"weekdays": {
|
||||
"sunday": "Sunday",
|
||||
"monday": "Monday",
|
||||
"tuesday": "Tuesday",
|
||||
"wednesday": "Wednesday",
|
||||
"thursday": "Thursday",
|
||||
"friday": "Friday",
|
||||
"saturday": "Saturday"
|
||||
},
|
||||
"autoBackupLabel": "Auto Backup",
|
||||
"autoBackupTooltip": "When enabled, the application data (habits, coins, settings, etc.) will be automatically backed up daily around 2 AM server time. Backups are stored as ZIP files in the `backups/` directory at the project root. Only the last 7 backups are kept; older ones are automatically deleted.",
|
||||
"autoBackupDescription": "Automatically back up data daily",
|
||||
"languageLabel": "Language",
|
||||
"languageDescription": "Choose your preferred display language for the application.",
|
||||
"languageChangedTitle": "Language Changed",
|
||||
"languageChangedDescription": "Please refresh the page to see the changes",
|
||||
"languageDisabledInDemoTooltip": "Changing the language is disabled in the demo version."
|
||||
},
|
||||
"Common": {
|
||||
"authenticationRequiredTitle": "Authentication Required",
|
||||
"authenticationRequiredDescription": "Please sign in to continue.",
|
||||
"permissionDeniedTitle": "Permission Denied",
|
||||
"permissionDeniedDescription": "You don't have {action} permission for {resource}s.",
|
||||
"undoButton": "Undo",
|
||||
"redoButton": "Redo",
|
||||
"errorTitle": "Error"
|
||||
},
|
||||
"useHabits": {
|
||||
"alreadyCompletedTitle": "Already completed",
|
||||
"alreadyCompletedDescription": "You've already completed this habit today.",
|
||||
"completedTitle": "Completed!",
|
||||
"earnedCoinsDescription": "You earned {coinReward} coins.",
|
||||
"progressTitle": "Progress!",
|
||||
"progressDescription": "You've completed {count}/{target} times today.",
|
||||
"completionUndoneTitle": "Completion undone",
|
||||
"completionUndoneDescription": "You have {count}/{target} completions today.",
|
||||
"noCompletionsToUndoTitle": "No completions to undo",
|
||||
"noCompletionsToUndoDescription": "This habit hasn't been completed today.",
|
||||
"alreadyCompletedPastDateTitle": "Already completed",
|
||||
"alreadyCompletedPastDateDescription": "This habit was already completed on {dateKey}.",
|
||||
"earnedCoinsPastDateDescription": "You earned {coinReward} coins for {dateKey}.",
|
||||
"progressPastDateDescription": "You've completed {count}/{target} times on {dateKey}."
|
||||
},
|
||||
"useWishlist": {
|
||||
"redemptionLimitReachedTitle": "Redemption limit reached",
|
||||
"redemptionLimitReachedDescription": "You've reached the maximum redemptions for \"{itemName}\".",
|
||||
"rewardRedeemedTitle": "🎉 Reward Redeemed!",
|
||||
"rewardRedeemedDescription": "You've redeemed \"{itemName}\" for {itemCoinCost} coins.",
|
||||
"notEnoughCoinsTitle": "Not enough coins",
|
||||
"notEnoughCoinsDescription": "You need {coinsNeeded} more coins to redeem this reward."
|
||||
},
|
||||
"useCoins": {
|
||||
"invalidAmountTitle": "Invalid amount",
|
||||
"invalidAmountDescription": "Please enter a valid positive number",
|
||||
"successTitle": "Success",
|
||||
"addedCoinsDescription": "Added {amount} coins",
|
||||
"removedCoinsDescription": "Removed {amount} coins",
|
||||
"transactionNotFoundDescription": "Transaction not found"
|
||||
}
|
||||
}
|
||||
407
messages/es.json
Normal file
407
messages/es.json
Normal file
@@ -0,0 +1,407 @@
|
||||
{
|
||||
"Dashboard": {
|
||||
"title": "Tablero"
|
||||
},
|
||||
"HabitList": {
|
||||
"myTasks": "Mis tareas",
|
||||
"myHabits": "Mis hábitos",
|
||||
"addTaskButton": "Añadir tarea",
|
||||
"addHabitButton": "Añadir hábito",
|
||||
"searchTasksPlaceholder": "Buscar tareas...",
|
||||
"searchHabitsPlaceholder": "Buscar hábitos...",
|
||||
"sortByLabel": "Ordenar por:",
|
||||
"sortByName": "Nombre",
|
||||
"sortByCoinReward": "Recompensa de monedas",
|
||||
"sortByDueDate": "Fecha límite",
|
||||
"sortByFrequency": "Frecuencia",
|
||||
"toggleSortOrderAriaLabel": "Cambiar orden de clasificación",
|
||||
"noTasksFoundMessage": "No se encontraron tareas que coincidan con tu búsqueda.",
|
||||
"noHabitsFoundMessage": "No se encontraron hábitos que coincidan con tu búsqueda.",
|
||||
"emptyStateTasksTitle": "Aún no hay tareas",
|
||||
"emptyStateHabitsTitle": "Aún no hay hábitos",
|
||||
"emptyStateTasksDescription": "Crea tu primera tarea para empezar a seguir tu progreso",
|
||||
"emptyStateHabitsDescription": "Crea tu primer hábito para empezar a seguir tu progreso",
|
||||
"archivedSectionTitle": "Archivado",
|
||||
"deleteTaskDialogTitle": "Eliminar tarea",
|
||||
"deleteHabitDialogTitle": "Eliminar hábito",
|
||||
"deleteTaskDialogMessage": "¿Estás seguro de que quieres eliminar esta tarea? Esta acción no se puede deshacer.",
|
||||
"deleteHabitDialogMessage": "¿Estás seguro de que quieres eliminar este hábito? Esta acción no se puede deshacer.",
|
||||
"deleteButton": "Eliminar"
|
||||
},
|
||||
"DailyOverview": {
|
||||
"addTaskButtonLabel": "Añadir tarea",
|
||||
"addHabitButtonLabel": "Añadir hábito",
|
||||
"todaysOverviewTitle": "Resumen de hoy",
|
||||
"dailyTasksTitle": "Tareas diarias",
|
||||
"noTasksDueTodayMessage": "No hay tareas para hoy. ¡Añade algunas para empezar!",
|
||||
"dailyHabitsTitle": "Hábitos diarios",
|
||||
"noHabitsDueTodayMessage": "No hay hábitos para hoy. ¡Añade algunos para empezar!",
|
||||
"wishlistGoalsTitle": "Objetivos de lista de deseos",
|
||||
"redeemableBadgeLabel": "{count}/{total} canjeable",
|
||||
"noWishlistItemsMessage": "Aún no hay items en la lista de deseos. ¡Añade algunas metas para trabajar!",
|
||||
"readyToRedeemMessage": "¡Listo para canjear!",
|
||||
"coinsToGoMessage": "Faltan {amount} monedas",
|
||||
"showLessButton": "Mostrar menos",
|
||||
"showAllButton": "Mostrar todo",
|
||||
"viewButton": "Ver",
|
||||
"deleteTaskDialogTitle": "Eliminar tarea",
|
||||
"deleteHabitDialogTitle": "Eliminar hábito",
|
||||
"confirmDeleteDialogMessage": "¿Estás seguro de que quieres eliminar \"{name}\"? Esta acción no se puede deshacer.",
|
||||
"deleteButton": "Eliminar",
|
||||
"overdueTooltip": "Vencido"
|
||||
},
|
||||
"HabitContextMenuItems": {
|
||||
"startPomodoro": "Iniciar Pomodoro",
|
||||
"moveToToday": "Mover a hoy",
|
||||
"moveToTomorrow": "Mover a mañana",
|
||||
"unpin": "Desanclar",
|
||||
"pin": "Anclar",
|
||||
"edit": "Editar",
|
||||
"archive": "Archivar",
|
||||
"unarchive": "Desarchivar",
|
||||
"delete": "Eliminar"
|
||||
},
|
||||
"HabitStreak": {
|
||||
"dailyCompletionStreakTitle": "Racha de finalización diaria",
|
||||
"tooltipHabitsLabel": "hábitos",
|
||||
"tooltipTasksLabel": "tareas",
|
||||
"tooltipCompletedLabel": "Completado"
|
||||
},
|
||||
"CoinBalance": {
|
||||
"coinBalanceTitle": "Saldo de monedas"
|
||||
},
|
||||
"AddEditHabitModal": {
|
||||
"editTaskTitle": "Editar tarea",
|
||||
"editHabitTitle": "Editar hábito",
|
||||
"addNewTaskTitle": "Añadir nueva tarea",
|
||||
"addNewHabitTitle": "Añadir nuevo hábito",
|
||||
"nameLabel": "Nombre *",
|
||||
"descriptionLabel": "Descripción",
|
||||
"whenLabel": "Cuándo *",
|
||||
"completeLabel": "Completar",
|
||||
"timesSuffix": "veces",
|
||||
"rewardLabel": "Recompensa",
|
||||
"coinsSuffix": "monedas",
|
||||
"shareLabel": "Compartir",
|
||||
"saveChangesButton": "Guardar cambios",
|
||||
"addTaskButton": "Añadir tarea",
|
||||
"addHabitButton": "Añadir hábito"
|
||||
},
|
||||
"ConfirmDialog": {
|
||||
"confirmButton": "Confirmar",
|
||||
"cancelButton": "Cancelar"
|
||||
},
|
||||
"AddEditWishlistItemModal": {
|
||||
"editTitle": "Editar recompensa",
|
||||
"addTitle": "Añadir nueva recompensa",
|
||||
"nameLabel": "Nombre *",
|
||||
"descriptionLabel": "Descripción",
|
||||
"costLabel": "Costo",
|
||||
"coinsSuffix": "monedas",
|
||||
"redeemableLabel": "Canjeable",
|
||||
"timesSuffix": "veces",
|
||||
"errorNameRequired": "El nombre es requerido",
|
||||
"errorCoinCostMin": "El costo en monedas debe ser al menos 1",
|
||||
"errorTargetCompletionsMin": "El número de finalizaciones objetivo debe ser al menos 1",
|
||||
"errorInvalidUrl": "Por favor ingresa una URL válida",
|
||||
"linkLabel": "Enlace",
|
||||
"shareLabel": "Compartir",
|
||||
"saveButton": "Guardar cambios",
|
||||
"addButton": "Añadir recompensa"
|
||||
},
|
||||
"Navigation": {
|
||||
"dashboard": "Tablero",
|
||||
"tasks": "Tareas",
|
||||
"habits": "Hábitos",
|
||||
"calendar": "Calendario",
|
||||
"wishlist": "Lista de deseos",
|
||||
"coins": "Monedas"
|
||||
},
|
||||
"TodayEarnedCoins": {
|
||||
"todaySuffix": "hoy"
|
||||
},
|
||||
"WishlistItem": {
|
||||
"usesLeftSingular": "uso restante",
|
||||
"usesLeftPlural": "usos restantes",
|
||||
"coinsSuffix": "monedas",
|
||||
"redeem": "Canjear",
|
||||
"redeemedDone": "Hecho",
|
||||
"redeemedExclamation": "¡Canjeado!",
|
||||
"editButton": "Editar",
|
||||
"archiveButton": "Archivar",
|
||||
"unarchiveButton": "Desarchivar",
|
||||
"deleteButton": "Eliminar"
|
||||
},
|
||||
"WishlistManager": {
|
||||
"title": "Mi lista de deseos",
|
||||
"addRewardButton": "Añadir recompensa",
|
||||
"emptyStateTitle": "Tu lista de deseos está vacía",
|
||||
"emptyStateDescription": "Añade recompensas que te gustaría ganar con tus monedas",
|
||||
"archivedSectionTitle": "Archivado",
|
||||
"popupBlockedTitle": "Popup bloqueado",
|
||||
"popupBlockedDescription": "Por favor permite los popups para abrir el enlace",
|
||||
"deleteDialogTitle": "Eliminar recompensa",
|
||||
"deleteDialogMessage": "¿Estás seguro de que quieres eliminar esta recompensa? Esta acción no se puede deshacer.",
|
||||
"deleteButton": "Eliminar"
|
||||
},
|
||||
"UserSelectModal": {
|
||||
"addUserButton": "Añadir usuario",
|
||||
"createNewUserTitle": "Crear nuevo usuario",
|
||||
"selectUserTitle": "Seleccionar usuario",
|
||||
"signInSuccessTitle": "Inicio de sesión exitoso",
|
||||
"signInSuccessDescription": "¡Bienvenido de nuevo, {username}!",
|
||||
"errorInvalidPassword": "contraseña inválida"
|
||||
},
|
||||
"CoinsManager": {
|
||||
"title": "Gestión de monedas",
|
||||
"currentBalanceLabel": "Saldo actual",
|
||||
"coinsSuffix": "monedas",
|
||||
"addCoinsButton": "Añadir monedas",
|
||||
"removeCoinsButton": "Quitar monedas",
|
||||
"statisticsTitle": "Estadísticas",
|
||||
"totalEarnedLabel": "Total ganado",
|
||||
"totalSpentLabel": "Total gastado",
|
||||
"totalTransactionsLabel": "Transacciones totales",
|
||||
"todaysEarnedLabel": "Ganado hoy",
|
||||
"todaysSpentLabel": "Gastado hoy",
|
||||
"todaysTransactionsLabel": "Transacciones hoy",
|
||||
"transactionHistoryTitle": "Historial de transacciones",
|
||||
"showLabel": "Mostrar:",
|
||||
"entriesSuffix": "entradas",
|
||||
"showingEntries": "Mostrando {from} a {to} de {total} entradas",
|
||||
"noTransactionsTitle": "Aún no hay transacciones",
|
||||
"noTransactionsDescription": "Tu historial de transacciones aparecerá aquí una vez que empieces a ganar o gastar monedas",
|
||||
"pageLabel": "Página",
|
||||
"ofLabel": "de",
|
||||
"transactionTypeHabitCompletion": "Finalización de hábito",
|
||||
"transactionTypeTaskCompletion": "Finalización de tarea",
|
||||
"transactionTypeHabitUndo": "Deshacer hábito",
|
||||
"transactionTypeTaskUndo": "Deshacer tarea",
|
||||
"transactionTypeWishRedemption": "Canje de deseo",
|
||||
"transactionTypeManualAdjustment": "Ajuste manual",
|
||||
"transactionTypeCoinReset": "Reinicio de monedas",
|
||||
"transactionTypeInitialBalance": "Saldo inicial"
|
||||
},
|
||||
"NotificationBell": {
|
||||
"errorUpdateTimestamp": "Error al actualizar la marca de tiempo de notificación leída:"
|
||||
},
|
||||
"PomodoroTimer": {
|
||||
"focusLabel1": "Mantente enfocado",
|
||||
"focusLabel2": "Tú puedes",
|
||||
"focusLabel3": "Sigue adelante",
|
||||
"focusLabel4": "Hazlo",
|
||||
"focusLabel5": "Haz que suceda",
|
||||
"focusLabel6": "Mantente fuerte",
|
||||
"focusLabel7": "Esfuérzate",
|
||||
"focusLabel8": "Un paso a la vez",
|
||||
"focusLabel9": "Tú puedes hacerlo",
|
||||
"focusLabel10": "Enfócate y conquista",
|
||||
"breakLabel1": "Toma un descanso",
|
||||
"breakLabel2": "Relájate y recarga",
|
||||
"breakLabel3": "Respira profundamente",
|
||||
"breakLabel4": "Estírate",
|
||||
"breakLabel5": "Refréscate",
|
||||
"breakLabel6": "Te lo mereces",
|
||||
"breakLabel7": "Recarga tu energía",
|
||||
"breakLabel8": "Aléjate un momento",
|
||||
"breakLabel9": "Despeja tu mente",
|
||||
"breakLabel10": "Descansa y recupérate",
|
||||
"focusType": "Enfoque",
|
||||
"breakType": "Descanso",
|
||||
"pauseButton": "Pausar",
|
||||
"startButton": "Iniciar",
|
||||
"resetButton": "Reiniciar",
|
||||
"skipButton": "Saltar",
|
||||
"wakeLockNotSupported": "El navegador no soporta wake lock",
|
||||
"wakeLockInUse": "Wake lock ya está en uso",
|
||||
"wakeLockRequestError": "Error al solicitar wake lock:",
|
||||
"wakeLockReleaseError": "Error al liberar wake lock:"
|
||||
},
|
||||
"HabitCalendar": {
|
||||
"title": "Calendario de hábitos",
|
||||
"calendarCardTitle": "Calendario",
|
||||
"selectDatePrompt": "Selecciona una fecha",
|
||||
"tasksSectionTitle": "Tareas",
|
||||
"habitsSectionTitle": "Hábitos",
|
||||
"errorCompletingPastHabit": "Error al completar hábito pasado:"
|
||||
},
|
||||
"NotificationDropdown": {
|
||||
"notLoggedIn": "No has iniciado sesión.",
|
||||
"userCompletedItem": "{username} completó {itemName}.",
|
||||
"userRedeemedItem": "{username} canjeó {itemName}.",
|
||||
"activityRelatedToItem": "Actividad relacionada con {itemName} por {username}.",
|
||||
"defaultUsername": "Alguien",
|
||||
"defaultItemName": "un item compartido",
|
||||
"notificationsTitle": "Notificaciones",
|
||||
"notificationsTooltip": "Muestra finalizaciones o canjes de otros usuarios para hábitos o lista de deseos que compartiste con ellos (debes ser admin)",
|
||||
"noNotificationsYet": "Aún no hay notificaciones."
|
||||
},
|
||||
"AboutModal": {
|
||||
"dialogArisLabel": "acerca de",
|
||||
"changelogButton": "Registro de cambios",
|
||||
"createdByPrefix": "Creado con ❤️ por",
|
||||
"starOnGitHubButton": "Dar estrella en GitHub"
|
||||
},
|
||||
"PermissionSelector": {
|
||||
"permissionsTitle": "Permisos",
|
||||
"adminAccessLabel": "Acceso de administrador",
|
||||
"adminAccessDescription": "Los administradores tienen permiso completo sobre todos los datos de todos los usuarios",
|
||||
"resourceHabitTask": "Hábito / Tarea",
|
||||
"resourceWishlist": "Lista de deseos",
|
||||
"resourceCoins": "Monedas",
|
||||
"permissionWrite": "Escritura",
|
||||
"permissionInteract": "Interactuar"
|
||||
},
|
||||
"UserForm": {
|
||||
"toastUserUpdatedTitle": "Usuario actualizado",
|
||||
"toastUserUpdatedDescription": "Usuario {username} actualizado con éxito",
|
||||
"toastUserCreatedTitle": "Usuario creado",
|
||||
"toastUserCreatedDescription": "Usuario {username} creado con éxito",
|
||||
"actionUpdate": "actualizar",
|
||||
"actionCreate": "crear",
|
||||
"errorFailedUserAction": "Error al {action} usuario",
|
||||
"errorTitle": "Error",
|
||||
"errorFileSizeLimit": "El tamaño del archivo debe ser menor a 5MB",
|
||||
"toastAvatarUploadedTitle": "Avatar subido",
|
||||
"toastAvatarUploadedDescription": "Avatar subido con éxito",
|
||||
"errorFailedAvatarUpload": "Error al subir avatar",
|
||||
"changeAvatarButton": "Cambiar avatar",
|
||||
"uploadAvatarButton": "Subir avatar",
|
||||
"usernameLabel": "Nombre de usuario",
|
||||
"usernamePlaceholder": "Nombre de usuario",
|
||||
"newPasswordLabel": "Nueva contraseña",
|
||||
"passwordLabel": "Contraseña",
|
||||
"passwordPlaceholderEdit": "Dejar en blanco para mantener la actual",
|
||||
"passwordPlaceholderCreate": "Ingresar contraseña",
|
||||
"demoPasswordDisabledMessage": "La contraseña está automáticamente desactivada en la instancia demo",
|
||||
"disablePasswordLabel": "Desactivar contraseña",
|
||||
"cancelButton": "Cancelar",
|
||||
"saveChangesButton": "Guardar cambios",
|
||||
"createUserButton": "Crear usuario"
|
||||
},
|
||||
"ViewToggle": {
|
||||
"habitsLabel": "Hábitos",
|
||||
"tasksLabel": "Tareas"
|
||||
},
|
||||
"HabitItem": {
|
||||
"overdue": "Vencido",
|
||||
"whenLabel": "Cuándo: {frequency}",
|
||||
"coinsPerCompletion": "{count} monedas por finalización",
|
||||
"completedStatus": "Completado",
|
||||
"completedStatusCount": "Completado ({completed}/{target})",
|
||||
"completedStatusCountMobile": "{completed}/{target}",
|
||||
"completeButton": "Completar",
|
||||
"completeButtonCount": "Completar ({completed}/{target})",
|
||||
"completeButtonCountMobile": "{completed}/{target}",
|
||||
"undoButton": "Deshacer",
|
||||
"editButton": "Editar"
|
||||
},
|
||||
"TransactionNoteEditor": {
|
||||
"noteTooLongTitle": "Nota demasiado larga",
|
||||
"noteTooLongDescription": "Las notas deben tener menos de 200 caracteres",
|
||||
"errorSavingNoteTitle": "Error al guardar nota",
|
||||
"errorDeletingNoteTitle": "Error al eliminar nota",
|
||||
"pleaseTryAgainDescription": "Por favor inténtalo de nuevo",
|
||||
"addNotePlaceholder": "Añadir nota...",
|
||||
"saveNoteTitle": "Guardar nota",
|
||||
"cancelButtonTitle": "Cancelar",
|
||||
"deleteNoteTitle": "Eliminar nota",
|
||||
"editNoteAriaLabel": "Editar nota"
|
||||
},
|
||||
"Profile": {
|
||||
"guestUsername": "Invitado",
|
||||
"editProfileButton": "Editar perfil",
|
||||
"signOutSuccessTitle": "Cierre de sesión exitoso",
|
||||
"signOutSuccessDescription": "Has cerrado sesión en tu cuenta",
|
||||
"signOutErrorTitle": "Error al cerrar sesión",
|
||||
"signOutErrorDescription": "Error al cerrar sesión",
|
||||
"switchUserButton": "Cambiar usuario",
|
||||
"settingsLink": "Configuración",
|
||||
"aboutButton": "Acerca de",
|
||||
"themeLabel": "Tema",
|
||||
"editProfileModalTitle": "Editar perfil"
|
||||
},
|
||||
"PasswordEntryForm": {
|
||||
"notYouButton": "¿No eres tú?",
|
||||
"passwordLabel": "Contraseña",
|
||||
"passwordPlaceholder": "Ingresar contraseña",
|
||||
"loginErrorToastTitle": "Error",
|
||||
"loginFailedErrorToastDescription": "Error al iniciar sesión",
|
||||
"cancelButton": "Cancelar",
|
||||
"loginButton": "Iniciar sesión"
|
||||
},
|
||||
"CompletionCountBadge": {
|
||||
"countCompleted": "{completedCount}/{totalCount} completado"
|
||||
},
|
||||
"SettingsPage": {
|
||||
"title": "Configuración",
|
||||
"uiSettingsTitle": "Configuración de interfaz",
|
||||
"numberFormattingLabel": "Formato numérico",
|
||||
"numberFormattingDescription": "Formatear números grandes (ej: 1K, 1M, 1B)",
|
||||
"numberGroupingLabel": "Agrupación numérica",
|
||||
"numberGroupingDescription": "Usar separadores de miles (ej: 1,000 vs 1000)",
|
||||
"systemSettingsTitle": "Configuración del sistema",
|
||||
"timezoneLabel": "Zona horaria",
|
||||
"timezoneDescription": "Selecciona tu zona horaria para un seguimiento preciso de fechas",
|
||||
"weekStartDayLabel": "Día de inicio de semana",
|
||||
"weekStartDayDescription": "Selecciona tu día preferido para iniciar la semana",
|
||||
"weekdays": {
|
||||
"sunday": "Domingo",
|
||||
"monday": "Lunes",
|
||||
"tuesday": "Martes",
|
||||
"wednesday": "Miércoles",
|
||||
"thursday": "Jueves",
|
||||
"friday": "Viernes",
|
||||
"saturday": "Sábado"
|
||||
},
|
||||
"autoBackupLabel": "Copia de seguridad automática",
|
||||
"autoBackupTooltip": "Cuando está habilitado, los datos de la aplicación (hábitos, monedas, configuraciones, etc.) se respaldan automáticamente diariamente alrededor de las 2 AM hora del servidor. Las copias de seguridad se almacenan como archivos ZIP en el directorio `backups/` en la raíz del proyecto. Solo se conservan las últimas 7 copias de seguridad; las más antiguas se eliminan automáticamente.",
|
||||
"autoBackupDescription": "Realizar copia de seguridad automática diaria",
|
||||
"languageLabel": "Idioma",
|
||||
"languageDescription": "Elige tu idioma preferido para mostrar en la aplicación.",
|
||||
"languageChangedTitle": "Idioma cambiado",
|
||||
"languageChangedDescription": "Por favor actualiza la página para ver los cambios",
|
||||
"languageDisabledInDemoTooltip": "Cambiar el idioma está deshabilitado en la versión de demostración."
|
||||
},
|
||||
"Common": {
|
||||
"authenticationRequiredTitle": "Autenticación requerida",
|
||||
"authenticationRequiredDescription": "Por favor inicia sesión para continuar.",
|
||||
"permissionDeniedTitle": "Permiso denegado",
|
||||
"permissionDeniedDescription": "No tienes permiso de {action} para {resource}.",
|
||||
"undoButton": "Deshacer",
|
||||
"redoButton": "Rehacer",
|
||||
"errorTitle": "Error"
|
||||
},
|
||||
"useHabits": {
|
||||
"alreadyCompletedTitle": "Ya completado",
|
||||
"alreadyCompletedDescription": "Ya has completado este hábito hoy.",
|
||||
"completedTitle": "¡Completado!",
|
||||
"earnedCoinsDescription": "Ganaste {coinReward} monedas.",
|
||||
"progressTitle": "¡Progreso!",
|
||||
"progressDescription": "Has completado {count}/{target} veces hoy.",
|
||||
"completionUndoneTitle": "Finalización deshecha",
|
||||
"completionUndoneDescription": "Tienes {count}/{target} finalizaciones hoy.",
|
||||
"noCompletionsToUndoTitle": "No hay finalizaciones para deshacer",
|
||||
"noCompletionsToUndoDescription": "Este hábito no ha sido completado hoy.",
|
||||
"alreadyCompletedPastDateTitle": "Ya completado",
|
||||
"alreadyCompletedPastDateDescription": "Este hábito ya fue completado el {dateKey}.",
|
||||
"earnedCoinsPastDateDescription": "Ganaste {coinReward} monedas por {dateKey}.",
|
||||
"progressPastDateDescription": "Has completado {count}/{target} veces el {dateKey}."
|
||||
},
|
||||
"useWishlist": {
|
||||
"redemptionLimitReachedTitle": "Límite de canjes alcanzado",
|
||||
"redemptionLimitReachedDescription": "Has alcanzado el máximo de canjes para \"{itemName}\".",
|
||||
"rewardRedeemedTitle": "🎉 ¡Recompensa canjeada!",
|
||||
"rewardRedeemedDescription": "Has canjeado \"{itemName}\" por {itemCoinCost} monedas.",
|
||||
"notEnoughCoinsTitle": "No hay suficientes monedas",
|
||||
"notEnoughCoinsDescription": "Necesitas {coinsNeeded} monedas más para canjear esta recompensa."
|
||||
},
|
||||
"useCoins": {
|
||||
"invalidAmountTitle": "Cantidad inválida",
|
||||
"invalidAmountDescription": "Por favor ingresa un número positivo válido",
|
||||
"successTitle": "Éxito",
|
||||
"addedCoinsDescription": "Añadidas {amount} monedas",
|
||||
"removedCoinsDescription": "Quitadas {amount} monedas",
|
||||
"transactionNotFoundDescription": "Transacción no encontrada"
|
||||
}
|
||||
}
|
||||
407
messages/fr.json
Normal file
407
messages/fr.json
Normal file
@@ -0,0 +1,407 @@
|
||||
{
|
||||
"Dashboard": {
|
||||
"title": "Tableau de bord"
|
||||
},
|
||||
"HabitList": {
|
||||
"myTasks": "Mes tâches",
|
||||
"myHabits": "Mes habitudes",
|
||||
"addTaskButton": "Ajouter une tâche",
|
||||
"addHabitButton": "Ajouter une habitude",
|
||||
"searchTasksPlaceholder": "Rechercher des tâches...",
|
||||
"searchHabitsPlaceholder": "Rechercher des habitudes...",
|
||||
"sortByLabel": "Trier par :",
|
||||
"sortByName": "Nom",
|
||||
"sortByCoinReward": "Récompense en pièces",
|
||||
"sortByDueDate": "Date d'échéance",
|
||||
"sortByFrequency": "Fréquence",
|
||||
"toggleSortOrderAriaLabel": "Changer l'ordre de tri",
|
||||
"noTasksFoundMessage": "Aucune tâche ne correspond à votre recherche.",
|
||||
"noHabitsFoundMessage": "Aucune habitude ne correspond à votre recherche.",
|
||||
"emptyStateTasksTitle": "Aucune tâche pour l'instant",
|
||||
"emptyStateHabitsTitle": "Aucune habitude pour l'instant",
|
||||
"emptyStateTasksDescription": "Créez votre première tâche pour commencer à suivre vos progrès",
|
||||
"emptyStateHabitsDescription": "Créez votre première habitude pour commencer à suivre vos progrès",
|
||||
"archivedSectionTitle": "Archivé",
|
||||
"deleteTaskDialogTitle": "Supprimer la tâche",
|
||||
"deleteHabitDialogTitle": "Supprimer l'habitude",
|
||||
"deleteTaskDialogMessage": "Êtes-vous sûr de vouloir supprimer cette tâche ? Cette action est irréversible.",
|
||||
"deleteHabitDialogMessage": "Êtes-vous sûr de vouloir supprimer cette habitude ? Cette action est irréversible.",
|
||||
"deleteButton": "Supprimer"
|
||||
},
|
||||
"DailyOverview": {
|
||||
"addTaskButtonLabel": "Ajouter une tâche",
|
||||
"addHabitButtonLabel": "Ajouter une habitude",
|
||||
"todaysOverviewTitle": "Aperçu du jour",
|
||||
"dailyTasksTitle": "Tâches quotidiennes",
|
||||
"noTasksDueTodayMessage": "Aucune tâche pour aujourd'hui. Ajoutez des tâches pour commencer !",
|
||||
"dailyHabitsTitle": "Habitudes quotidiennes",
|
||||
"noHishabitsDueTodayMessage": "Aucune habitude pour aujourd'hui. Ajoutez des habitudes pour commencer !",
|
||||
"wishlistGoalsTitle": "Objectifs de la liste de souhaits",
|
||||
"redeemableBadgeLabel": "{count}/{total} échangeable",
|
||||
"noWishlistItemsMessage": "Aucun élément dans la liste de souhaits. Ajoutez des objectifs à atteindre !",
|
||||
"readyToRedeemMessage": "Prêt à échanger !",
|
||||
"coinsToGoMessage": "Il manque {amount} pièces",
|
||||
"showLessButton": "Afficher moins",
|
||||
"showAllButton": "Afficher tout",
|
||||
"viewButton": "Voir",
|
||||
"deleteTaskDialogTitle": "Supprimer la tâche",
|
||||
"deleteHabitDialogTitle": "Supprimer l'habitude",
|
||||
"confirmDeleteDialogMessage": "Êtes-vous sûr de vouloir supprimer \"{name}\" ? Cette action est irréversible.",
|
||||
"deleteButton": "Supprimer",
|
||||
"overdueTooltip": "En retard"
|
||||
},
|
||||
"HabitContextMenuItems": {
|
||||
"startPomodoro": "Démarrer Pomodoro",
|
||||
"moveToToday": "Déplacer à aujourd'hui",
|
||||
"moveToTomorrow": "Déplacer à demain",
|
||||
"unpin": "Détacher",
|
||||
"pin": "Attacher",
|
||||
"edit": "Modifier",
|
||||
"archive": "Archiver",
|
||||
"unarchive": "Désarchiver",
|
||||
"delete": "Supprimer"
|
||||
},
|
||||
"HabitStreak": {
|
||||
"dailyCompletionStreakTitle": "Série de complétions quotidiennes",
|
||||
"tooltipHabitsLabel": "habitudes",
|
||||
"tooltipTasksLabel": "tâches",
|
||||
"tooltipCompletedLabel": "Complété"
|
||||
},
|
||||
"CoinBalance": {
|
||||
"coinBalanceTitle": "Solde de pièces"
|
||||
},
|
||||
"AddEditHabitModal": {
|
||||
"editTaskTitle": "Modifier une tâche",
|
||||
"editHabitTitle": "Modifier une habitude",
|
||||
"addNewTaskTitle": "Ajouter une nouvelle tâche",
|
||||
"AddNewHabitTitle": "Ajouter une nouvelle habitude",
|
||||
"nameLabel": "Nom *",
|
||||
"descriptionLabel": "Description",
|
||||
"whenLabel": "Quand *",
|
||||
"completeLabel": "Compléter",
|
||||
"timesSuffix": "fois",
|
||||
"rewardLabel": "Récompense",
|
||||
"coinsSuffix": "pièces",
|
||||
"shareLabel": "Partager",
|
||||
"saveChangesButton": "Sauvegarder les modifications",
|
||||
"addTaskButton": "Ajouter une tâche",
|
||||
"addHabitButton": "Ajouter une habitude"
|
||||
},
|
||||
"ConfirmDialog": {
|
||||
"confirmButton": "Confirmer",
|
||||
"cancelButton": "Annuler"
|
||||
},
|
||||
"AddEditWishlistItemModal": {
|
||||
"editTitle": "Modifier la récompense",
|
||||
"addTitle": "Ajouter une nouvelle récompense",
|
||||
"nameLabel": "Nom *",
|
||||
"descriptionLabel": "Description",
|
||||
"costLabel": "Coût",
|
||||
"coinsSuffix": "pièces",
|
||||
"redeemableLabel": "Échangeable",
|
||||
"timesSuffix": "fois",
|
||||
"errorNameRequired": "Le nom est requis",
|
||||
"errorCoinCostMin": "Le coût en pièces doit être d'au moins 1",
|
||||
"errorTargetCompletionsMin": "Les complétions cibles doivent être d'au moins 1",
|
||||
"errorInvalidUrl": "Veuillez entrer une URL valide",
|
||||
"linkLabel": "Lien",
|
||||
"shareLabel": "Partager",
|
||||
"saveButton": "Sauvegarder les modifications",
|
||||
"addButton": "Ajouter une récompense"
|
||||
},
|
||||
"Navigation": {
|
||||
"dashboard": "Tableau de bord",
|
||||
"tasks": "Tâches",
|
||||
"habits": "Habitudes",
|
||||
"calendar": "Calendrier",
|
||||
"wishlist": "Liste de souhaits",
|
||||
"coins": "Pièces"
|
||||
},
|
||||
"TodayEarnedCoins": {
|
||||
"todaySuffix": "aujourd'hui"
|
||||
},
|
||||
"WishlistItem": {
|
||||
"usesLeftSingular": "utilisation restante",
|
||||
"usesLeftPlural": "utilisations restantes",
|
||||
"coinsSuffix": "pièces",
|
||||
"redeem": "Échanger",
|
||||
"redeemedDone": "Fait",
|
||||
"redeemedExclamation": "Échangé !",
|
||||
"editButton": "Modifier",
|
||||
"archiveButton": "Archiver",
|
||||
"unarchiveButton": "Désarchiver",
|
||||
"deleteButton": "Supprimer"
|
||||
},
|
||||
"WishlistManager": {
|
||||
"title": "Ma liste de souhaits",
|
||||
"addRewardButton": "Ajouter une récompense",
|
||||
"emptyStateTitle": "Votre liste de souhaits est vide",
|
||||
"emptyStateDescription": "Ajoutez des récompenses que vous aimeriez gagner avec vos pièces",
|
||||
"archivedSectionTitle": "Archivé",
|
||||
"popupBlockedTitle": "Popup bloqué",
|
||||
"popupBlockedDescription": "Veuillez autoriser les popups pour ouvrir le lien",
|
||||
"deleteDialogTitle": "Supprimer la récompense",
|
||||
"deleteDialogMessage": "Êtes-vous sûr de vouloir supprimer cette récompense ? Cette action est irréversible.",
|
||||
"deleteButton": "Supprimer"
|
||||
},
|
||||
"UserSelectModal": {
|
||||
"addUserButton": "Ajouter un utilisateur",
|
||||
"createNewUserTitle": "Créer un nouvel utilisateur",
|
||||
"selectUserTitle": "Sélectionner un utilisateur",
|
||||
"signInSuccessTitle": "Connecté avec succès",
|
||||
"signInSuccessDescription": "Bienvenue, {username} !",
|
||||
"errorInvalidPassword": "mot de passe invalide"
|
||||
},
|
||||
"CoinsManager": {
|
||||
"title": "Gestion des pièces",
|
||||
"currentBalanceLabel": "Solde actuel",
|
||||
"coinsSuffix": "pièces",
|
||||
"addCoinsButton": "Ajouter des pièces",
|
||||
"removeCoinsButton": "Retirer des pièces",
|
||||
"statisticsTitle": "Statistiques",
|
||||
"totalEarnedLabel": "Total gagné",
|
||||
"totalSpentLabel": "Total dépensé",
|
||||
"totalTransactionsLabel": "Total des transactions",
|
||||
"todaysEarnedLabel": "Gagné aujourd'hui",
|
||||
"todaysSpentLabel": "Dépensé aujourd'hui",
|
||||
"todaysTransactionsLabel": "Transactions d'aujourd'hui",
|
||||
"transactionHistoryTitle": "Historique des transactions",
|
||||
"showLabel": "Afficher :",
|
||||
"entriesSuffix": "entrées",
|
||||
"showingEntries": "Affichage de {from} à {to} de {total} entrées",
|
||||
"noTransactionsTitle": "Aucune transaction pour l'instant",
|
||||
"noTransactionsDescription": "Votre historique de transactions apparaîtra ici une fois que vous commencerez à gagner ou dépenser des pièces",
|
||||
"pageLabel": "Page",
|
||||
"ofLabel": "sur",
|
||||
"transactionTypeHabitCompletion": "Complétion d'habitude",
|
||||
"transactionTypeTaskCompletion": "Complétion de tâche",
|
||||
"transactionTypeHabitUndo": "Annulation d'habitude",
|
||||
"transactionTypeTaskUndo": "Annulation de tâche",
|
||||
"transactionTypeWishRedemption": "Échange de souhait",
|
||||
"transactionTypeManualAdjustment": "Ajustement manuel",
|
||||
"transactionTypeCoinReset": "Réinitialisation des pièces",
|
||||
"transactionTypeInitialBalance": "Solde initial"
|
||||
},
|
||||
"NotificationBell": {
|
||||
"errorUpdateTimestamp": "Échec de la mise à jour du timestamp de lecture de notification :"
|
||||
},
|
||||
"PomodoroTimer": {
|
||||
"focusLabel1": "Reste concentré",
|
||||
"focusLabel2": "Tu peux le faire",
|
||||
"focusLabel3": "Continue",
|
||||
"focusLabel4": "Tout donner",
|
||||
"focusLabel5": "Fais-le arriver",
|
||||
"focusLabel6": "Reste fort",
|
||||
"focusLabel7": "Persiste",
|
||||
"focusLabel8": "Un pas à la fois",
|
||||
"focusLabel9": "Tu peux y arriver",
|
||||
"focusLabel10": "Concentre-toi et conquiers",
|
||||
"breakLabel1": "Prends une pause",
|
||||
"breakLabel2": "Relaxe-toi et recharge",
|
||||
"breakLabel3": "Respire profondément",
|
||||
"breakLabel4": "Étire-toi",
|
||||
"breakLabel5": "Rafraîchis-toi",
|
||||
"breakLabel6": "Tu le mérites",
|
||||
"breakLabel7": "Recharge ton énergie",
|
||||
"breakLabel8": "Éloigne-toi un moment",
|
||||
"breakLabel9": "Vide ton esprit",
|
||||
"breakLabel10": "Repose-toi et récupère",
|
||||
"focusType": "Concentration",
|
||||
"breakType": "Pause",
|
||||
"pauseButton": "Pause",
|
||||
"startButton": "Démarrer",
|
||||
"resetButton": "Réinitialiser",
|
||||
"skipButton": "Passer",
|
||||
"wakeLockNotSupported": "Le navigateur ne supporte pas le verrouillage de veille",
|
||||
"wakeLockInUse": "Le verrouillage de veille est déjà actif",
|
||||
"wakeLockRequestError": "Erreur lors de la demande de verrouillage de veille :",
|
||||
"wakeLockReleaseError": "Erreur lors de la libération du verrouillage de veille :"
|
||||
},
|
||||
"HabitCalendar": {
|
||||
"title": "Calendrier des habitudes",
|
||||
"calendarCardTitle": "Calendrier",
|
||||
"selectDatePrompt": "Sélectionner une date",
|
||||
"tasksSectionTitle": "Tâches",
|
||||
"habitsSectionTitle": "Habitudes",
|
||||
"errorCompletingPastHabit": "Erreur lors de la complétion d'une habitude passée :"
|
||||
},
|
||||
"NotificationDropdown": {
|
||||
"notLoggedIn": "Non connecté.",
|
||||
"userCompletedItem": "{username} a complété {itemName}.",
|
||||
"userRedeemedItem": "{username} a échangé {itemName}.",
|
||||
"activityRelatedToItem": "Activité liée à {itemName} par {username}.",
|
||||
"defaultUsername": "Quelqu'un",
|
||||
"defaultItemName": "un élément partagé",
|
||||
"notificationsTitle": "Notifications",
|
||||
"notificationsTooltip": "Affiche les complétions ou les échanges par d'autres utilisateurs pour les habitudes ou la liste de souhaits que vous avez partagés avec eux (vous devez être admin)",
|
||||
"noNotificationsYet": "Aucune notification pour l'instant."
|
||||
},
|
||||
"AboutModal": {
|
||||
"dialogArisLabel": "à propos",
|
||||
"changelogButton": "Journal des modifications",
|
||||
"createdByPrefix": "Créé avec ❤️ par",
|
||||
"starOnGitHubButton": "Étoile sur GitHub"
|
||||
},
|
||||
"PermissionSelector": {
|
||||
"permissionsTitle": "Permissions",
|
||||
"adminAccessLabel": "Accès administrateur",
|
||||
"adminAccessDescription": "Les administrateurs ont tous les droits sur les données de tous les utilisateurs",
|
||||
"resourceHabitTask": "Habitude / Tâche",
|
||||
"resourceWishlist": "Liste de souhaits",
|
||||
"resourceCoins": "Pièces",
|
||||
"permissionWrite": "Écriture",
|
||||
"permissionInteract": "Interaction"
|
||||
},
|
||||
"UserForm": {
|
||||
"toastUserUpdatedTitle": "Utilisateur mis à jour",
|
||||
"toastUserUpdatedDescription": "Utilisateur {username} mis à jour avec succès",
|
||||
"toastUserCreatedTitle": "Utilisateur créé",
|
||||
"toastUserCreatedDescription": "Utilisateur {username} créé avec succès",
|
||||
"actionUpdate": "mise à jour",
|
||||
"actionCreate": "création",
|
||||
"errorFailedUserAction": "Échec de la {action} de l'utilisateur",
|
||||
"errorTitle": "Erreur",
|
||||
"errorFileSizeLimit": "La taille du fichier doit être inférieure à 5MB",
|
||||
"toastAvatarUploadedTitle": "Avatar téléchargé",
|
||||
"toastAvatarUploadedDescription": "Avatar téléchargé avec succès",
|
||||
"errorFailedAvatarUpload": "Échec du téléchargement de l'avatar",
|
||||
"changeAvatarButton": "Changer l'avatar",
|
||||
"uploadAvatarButton": "Télécharger l'avatar",
|
||||
"usernameLabel": "Nom d'utilisateur",
|
||||
"usernamePlaceholder": "Nom d'utilisateur",
|
||||
"newPasswordLabel": "Nouveau mot de passe",
|
||||
"passwordLabel": "Mot de passe",
|
||||
"passwordPlaceholderEdit": "Laisser vide pour conserver l'actuel",
|
||||
"passwordPlaceholderCreate": "Entrer le mot de passe",
|
||||
"demoPasswordDisabledMessage": "Le mot de passe est automatiquement désactivé dans l'instance de démonstration",
|
||||
"disablePasswordLabel": "Désactiver le mot de passe",
|
||||
"cancelButton": "Annuler",
|
||||
"saveChangesButton": "Sauvegarder les modifications",
|
||||
"createUserButton": "Créer un utilisateur"
|
||||
},
|
||||
"ViewToggle": {
|
||||
"habitsLabel": "Habitudes",
|
||||
"tasksLabel": "Tâches"
|
||||
},
|
||||
"HabitItem": {
|
||||
"overdue": "En retard",
|
||||
"whenLabel": "Quand : {frequency}",
|
||||
"coinsPerCompletion": "{count} pièces par complétion",
|
||||
"completedStatus": "Complété",
|
||||
"completedStatusCount": "Complété ({completed}/{target})",
|
||||
"completedStatusCountMobile": "{completed}/{target}",
|
||||
"completeButton": "Compléter",
|
||||
"completeButtonCount": "Compléter ({completed}/{target})",
|
||||
"completeButtonCountMobile": "{completed}/{target}",
|
||||
"undoButton": "Annuler",
|
||||
"editButton": "Modifier"
|
||||
},
|
||||
"TransactionNoteEditor": {
|
||||
"noteTooLongTitle": "Note trop longue",
|
||||
"noteTooLongDescription": "Les notes doivent faire moins de 200 caractères",
|
||||
"errorSavingNoteTitle": "Erreur lors de la sauvegarde de la note",
|
||||
"errorDeletingNoteTitle": "Erreur lors de la suppression de la note",
|
||||
"pleaseTryAgainDescription": "Veuillez réessayer",
|
||||
"addNotePlaceholder": "Ajouter une note...",
|
||||
"saveNoteTitle": "Sauvegarder la note",
|
||||
"cancelButtonTitle": "Annuler",
|
||||
"deleteNoteTitle": "Supprimer la note",
|
||||
"editNoteAriaLabel": "Modifier la note"
|
||||
},
|
||||
"Profile": {
|
||||
"guestUsername": "Invité",
|
||||
"editProfileButton": "Modifier le profil",
|
||||
"signOutSuccessTitle": "Déconnexion réussie",
|
||||
"signOutSuccessDescription": "Vous avez été déconnecté de votre compte",
|
||||
"signOutErrorTitle": "Erreur de déconnexion",
|
||||
"signOutErrorDescription": "Échec de la déconnexion",
|
||||
"switchUserButton": "Changer d'utilisateur",
|
||||
"settingsLink": "Paramètres",
|
||||
"aboutButton": "À propos",
|
||||
"themeLabel": "Thème",
|
||||
"editProfileModalTitle": "Modifier le profil"
|
||||
},
|
||||
"PasswordEntryForm": {
|
||||
"notYouButton": "Ce n'est pas vous ?",
|
||||
"passwordLabel": "Mot de passe",
|
||||
"passwordPlaceholder": "Entrer le mot de passe",
|
||||
"loginErrorToastTitle": "Erreur",
|
||||
"loginFailedErrorToastDescription": "Échec de la connexion",
|
||||
"cancelButton": "Annuler",
|
||||
"loginButton": "Se connecter"
|
||||
},
|
||||
"CompletionCountBadge": {
|
||||
"countCompleted": "{completedCount}/{totalCount} complété"
|
||||
},
|
||||
"SettingsPage": {
|
||||
"title": "Paramètres",
|
||||
"uiSettingsTitle": "Paramètres de l'interface",
|
||||
"numberFormattingLabel": "Formatage des nombres",
|
||||
"numberFormattingDescription": "Formater les grands nombres (ex: 1K, 1M, 1B)",
|
||||
"numberGroupingLabel": "Regroupement des nombres",
|
||||
"numberGroupingDescription": "Utiliser les séparateurs de milliers (ex: 1,000 vs 1000)",
|
||||
"systemSettingsTitle": "Paramètres système",
|
||||
"timezoneLabel": "Fuseau horaire",
|
||||
"timezoneDescription": "Sélectionnez votre fuseau horaire pour un suivi précis des dates",
|
||||
"weekStartDayLabel": "Jour de début de semaine",
|
||||
"weekStartDayDescription": "Sélectionnez votre jour préféré pour commencer la semaine",
|
||||
"weekdays": {
|
||||
"sunday": "Dimanche",
|
||||
"monday": "Lundi",
|
||||
"tuesday": "Mardi",
|
||||
"wednesday": "Mercredi",
|
||||
"thursday": "Jeudi",
|
||||
"friday": "Vendredi",
|
||||
"saturday": "Samedi"
|
||||
},
|
||||
"autoBackupLabel": "Sauvegarde automatique",
|
||||
"autoBackupTooltip": "Lorsqu'il est activé, les données de l'application (habitudes, pièces, paramètres, etc.) sont automatiquement sauvegardées quotidiennement vers 2 heures du matin, heure du serveur. Les sauvegardes sont stockées sous forme de fichiers ZIP dans le répertoire `backups/` à la racine du projet. Seules les 7 dernières sauvegardes sont conservées ; les plus anciennes sont automatiquement supprimées.",
|
||||
"autoBackupDescription": "Effectuer une sauvegarde automatique quotidienne",
|
||||
"languageLabel": "Langue",
|
||||
"languageDescription": "Choisissez votre langue d'affichage préférée pour l'application.",
|
||||
"languageChangedTitle": "Langue modifiée",
|
||||
"languageChangedDescription": "Veuillez actualiser la page pour voir les changements",
|
||||
"languageDisabledInDemoTooltip": "Le changement de langue est désactivé dans la version de démonstration."
|
||||
},
|
||||
"Common": {
|
||||
"authenticationRequiredTitle": "Authentification requise",
|
||||
"authenticationRequiredDescription": "Veuillez vous connecter pour continuer.",
|
||||
"permissionDeniedTitle": "Permission refusée",
|
||||
"permissionDeniedDescription": "Vous n'avez pas la permission de {action} pour {resource}.",
|
||||
"undoButton": "Annuler",
|
||||
"redoButton": "Rétablir",
|
||||
"errorTitle": "Erreur"
|
||||
},
|
||||
"useHabits": {
|
||||
"alreadyCompletedTitle": "Déjà complété",
|
||||
"alreadyCompletedDescription": "Vous avez déjà complété cette habitude aujourd'hui.",
|
||||
"completedTitle": "Complété !",
|
||||
"earnedCoinsDescription": "Vous avez gagné {coinReward} pièces.",
|
||||
"progressTitle": "Progrès !",
|
||||
"progressDescription": "Vous avez complété {count}/{target} fois aujourd'hui.",
|
||||
"completionUndoneTitle": "Complétion annulée",
|
||||
"completionUndoneDescription": "Vous avez {count}/{target} complétions aujourd'hui.",
|
||||
"noCompletionsToUndoTitle": "Aucune complétion à annuler",
|
||||
"noCompletionsToUndoDescription": "Cette habitude n'a pas été complétée aujourd'hui.",
|
||||
"alreadyCompletedPastDateTitle": "Déjà complété",
|
||||
"alreadyCompletedPastDateDescription": "Cette habitude a déjà été complétée le {dateKey}.",
|
||||
"earnedCoinsPastDateDescription": "Vous avez gagné {coinReward} pièces pour {dateKey}.",
|
||||
"progressPastDateDescription": "Vous avez complété {count}/{target} fois le {dateKey}."
|
||||
},
|
||||
"useWishlist": {
|
||||
"redemptionLimitReachedTitle": "Limite de rachat atteinte",
|
||||
"redemptionLimitReachedDescription": "Vous avez atteint le nombre maximum de rachats pour \"{itemName}\".",
|
||||
"rewardRedeemedTitle": "🎉 Récompense échangée !",
|
||||
"rewardRedeemedDescription": "Vous avez échangé \"{itemName}\" pour {itemCoinCost} pièces.",
|
||||
"notEnoughCoinsTitle": "Pas assez de pièces",
|
||||
"notEnoughCoinsDescription": "Il vous manque {coinsNeeded} pièces pour échanger cette récompense."
|
||||
},
|
||||
"useCoins": {
|
||||
"invalidAmountTitle": "Montant invalide",
|
||||
"invalidAmountDescription": "Veuillez entrer un nombre positif valide",
|
||||
"successTitle": "Succès",
|
||||
"addedCoinsDescription": "Ajouté {amount} pièces",
|
||||
"removedCoinsDescription": "Retiré {amount} pièces",
|
||||
"transactionNotFoundDescription": "Transaction non trouvée"
|
||||
}
|
||||
}
|
||||
407
messages/ja.json
Normal file
407
messages/ja.json
Normal file
@@ -0,0 +1,407 @@
|
||||
{
|
||||
"Dashboard": {
|
||||
"title": "ダッシュボード"
|
||||
},
|
||||
"HabitList": {
|
||||
"myTasks": "マイタスク",
|
||||
"myHabits": "マイ習慣",
|
||||
"addTaskButton": "タスクを追加",
|
||||
"addHabitButton": "習慣を追加",
|
||||
"searchTasksPlaceholder": "タスクを検索...",
|
||||
"searchHabitsPlaceholder": "習慣を検索...",
|
||||
"sortByLabel": "並び替え:",
|
||||
"sortByName": "名前",
|
||||
"sortByCoinReward": "コイン報酬",
|
||||
"sortByDueDate": "締め切り",
|
||||
"sortByFrequency": "頻度",
|
||||
"toggleSortOrderAriaLabel": "並び順を切り替え",
|
||||
"noTasksFoundMessage": "検索条件に一致するタスクはありません。",
|
||||
"noHabitsFoundMessage": "検索条件に一致する習慣はありません。",
|
||||
"emptyStateTasksTitle": "タスクがありません",
|
||||
"emptyStateHabitsTitle": "習慣がありません",
|
||||
"emptyStateTasksDescription": "最初のタスクを作成して進捗を追跡しましょう",
|
||||
"emptyStateHabitsDescription": "最初の習慣を作成して進捗を追跡しましょう",
|
||||
"archivedSectionTitle": "アーカイブ",
|
||||
"deleteTaskDialogTitle": "タスクを削除",
|
||||
"deleteHabitDialogTitle": "習慣を削除",
|
||||
"deleteTaskDialogMessage": "このタスクを削除してもよろしいですか?この操作は元に戻せません。",
|
||||
"deleteHabitDialogMessage": "この習慣を削除してもよろしいですか?この操作は元に戻せません。",
|
||||
"deleteButton": "削除"
|
||||
},
|
||||
"DailyOverview": {
|
||||
"addTaskButtonLabel": "タスクを追加",
|
||||
"addHabitButtonLabel": "習慣を追加",
|
||||
"todaysOverviewTitle": "今日の概要",
|
||||
"dailyTasksTitle": "今日のタスク",
|
||||
"noTasksDueTodayMessage": "今日のタスクはありません。タスクを追加して始めましょう!",
|
||||
"dailyHabitsTitle": "今日の習慣",
|
||||
"noHabitsDueTodayMessage": "今日の習慣はありません。習慣を追加して始めましょう!",
|
||||
"wishlistGoalsTitle": "ウィッシュリスト目標",
|
||||
"redeemableBadgeLabel": "{count}/{total} 使用可能",
|
||||
"noWishlistItemsMessage": "ウィッシュリストにアイテムがありません。達成したい目標を追加しましょう!",
|
||||
"readyToRedeemMessage": "使用可能です!",
|
||||
"coinsToGoMessage": "あと{amount}コイン",
|
||||
"showLessButton": "一部を表示",
|
||||
"showAllButton": "すべて表示",
|
||||
"viewButton": "表示",
|
||||
"deleteTaskDialogTitle": "タスクを削除",
|
||||
"deleteHabitDialogTitle": "習慣を削除",
|
||||
"confirmDeleteDialogMessage": "\"{name}\"を削除してもよろしいですか?この操作は元に戻せません。",
|
||||
"deleteButton": "削除",
|
||||
"overdueTooltip": "期限超過"
|
||||
},
|
||||
"HabitContextMenuItems": {
|
||||
"startPomodoro": "ポモドーロを開始",
|
||||
"moveToToday": "今日に移動",
|
||||
"moveToTomorrow": "明日に移動",
|
||||
"unpin": "ピン留めを解除",
|
||||
"pin": "ピン留めする",
|
||||
"edit": "編集",
|
||||
"archive": "アーカイブ",
|
||||
"unarchive": "アーカイブ解除",
|
||||
"delete": "削除"
|
||||
},
|
||||
"HabitStreak": {
|
||||
"dailyCompletionStreakTitle": "毎日達成ストリーク",
|
||||
"tooltipHabitsLabel": "習慣",
|
||||
"tooltipTasksLabel": "タスク",
|
||||
"tooltipCompletedLabel": "完了"
|
||||
},
|
||||
"CoinBalance": {
|
||||
"coinBalanceTitle": "コイン残高"
|
||||
},
|
||||
"AddEditHabitModal": {
|
||||
"editTaskTitle": "タスクを編集",
|
||||
"editHabitTitle": "習慣を編集",
|
||||
"addNewTaskTitle": "新しいタスクを追加",
|
||||
"addNewHabitTitle": "新しい習慣を追加",
|
||||
"nameLabel": "名前 *",
|
||||
"descriptionLabel": "説明",
|
||||
"whenLabel": "いつ *",
|
||||
"completeLabel": "完了",
|
||||
"timesSuffix": "回",
|
||||
"rewardLabel": "報酬",
|
||||
"coinsSuffix": "コイン",
|
||||
"shareLabel": "共有",
|
||||
"saveChangesButton": "変更を保存",
|
||||
"addTaskButton": "タスクを追加",
|
||||
"addHabitButton": "習慣を追加"
|
||||
},
|
||||
"ConfirmDialog": {
|
||||
"confirmButton": "確認",
|
||||
"cancelButton": "キャンセル"
|
||||
},
|
||||
"AddEditWishlistItemModal": {
|
||||
"editTitle": "報酬を編集",
|
||||
"addTitle": "新しい報酬を追加",
|
||||
"nameLabel": "名前 *",
|
||||
"descriptionLabel": "説明",
|
||||
"costLabel": "コスト",
|
||||
"coinsSuffix": "コイン",
|
||||
"redeemableLabel": "使用可能",
|
||||
"timesSuffix": "回",
|
||||
"errorNameRequired": "名前は必須です",
|
||||
"errorCoinCostMin": "コインコストは1以上である必要があります",
|
||||
"errorTargetCompletionsMin": "目標達成回数は1以上である必要があります",
|
||||
"errorInvalidUrl": "有効なURLを入力してください",
|
||||
"linkLabel": "リンク",
|
||||
"shareLabel": "共有",
|
||||
"saveButton": "変更を保存",
|
||||
"addButton": "報酬を追加"
|
||||
},
|
||||
"Navigation": {
|
||||
"dashboard": "ダッシュボード",
|
||||
"tasks": "タスク",
|
||||
"habits": "習慣",
|
||||
"calendar": "カレンダー",
|
||||
"wishlist": "ウィッシュリスト",
|
||||
"coins": "コイン"
|
||||
},
|
||||
"TodayEarnedCoins": {
|
||||
"todaySuffix": "今日"
|
||||
},
|
||||
"WishlistItem": {
|
||||
"usesLeftSingular": "使用可能回数: 1回",
|
||||
"usesLeftPlural": "使用可能回数: {count}回",
|
||||
"coinsSuffix": "コイン",
|
||||
"redeem": "使用する",
|
||||
"redeemedDone": "完了",
|
||||
"redeemedExclamation": "使用しました!",
|
||||
"editButton": "編集",
|
||||
"archiveButton": "アーカイブ",
|
||||
"unarchiveButton": "アーカイブ解除",
|
||||
"deleteButton": "削除"
|
||||
},
|
||||
"WishlistManager": {
|
||||
"title": "マイウィッシュリスト",
|
||||
"addRewardButton": "報酬を追加",
|
||||
"emptyStateTitle": "ウィッシュリストが空です",
|
||||
"emptyStateDescription": "コインで獲得したい報酬を追加しましょう",
|
||||
"archivedSectionTitle": "アーカイブ",
|
||||
"popupBlockedTitle": "ポップアップがブロックされました",
|
||||
"popupBlockedDescription": "リンクを開くためにポップアップを許可してください",
|
||||
"deleteDialogTitle": "報酬を削除",
|
||||
"deleteDialogMessage": "この報酬を削除してもよろしいですか?この操作は元に戻せません。",
|
||||
"deleteButton": "削除"
|
||||
},
|
||||
"UserSelectModal": {
|
||||
"addUserButton": "ユーザーを追加",
|
||||
"createNewUserTitle": "新しいユーザーを作成",
|
||||
"selectUserTitle": "ユーザーを選択",
|
||||
"signInSuccessTitle": "サインインに成功しました",
|
||||
"signInSuccessDescription": "おかえりなさい、{username}さん!",
|
||||
"errorInvalidPassword": "パスワードが無効です"
|
||||
},
|
||||
"CoinsManager": {
|
||||
"title": "コイン管理",
|
||||
"currentBalanceLabel": "現在の残高",
|
||||
"coinsSuffix": "コイン",
|
||||
"addCoinsButton": "コインを追加",
|
||||
"removeCoinsButton": "コインを削除",
|
||||
"statisticsTitle": "統計",
|
||||
"totalEarnedLabel": "総獲得額",
|
||||
"totalSpentLabel": "総支出額",
|
||||
"totalTransactionsLabel": "総取引数",
|
||||
"todaysEarnedLabel": "今日の獲得額",
|
||||
"todaysSpentLabel": "今日の支出額",
|
||||
"todaysTransactionsLabel": "今日の取引数",
|
||||
"transactionHistoryTitle": "取引履歴",
|
||||
"showLabel": "表示:",
|
||||
"entriesSuffix": "件",
|
||||
"showingEntries": "{from} から {to} 件(全 {total} 件)",
|
||||
"noTransactionsTitle": "取引履歴がありません",
|
||||
"noTransactionsDescription": "コインを獲得または使用すると、ここに取引履歴が表示されます",
|
||||
"pageLabel": "ページ",
|
||||
"ofLabel": "/",
|
||||
"transactionTypeHabitCompletion": "習慣達成",
|
||||
"transactionTypeTaskCompletion": "タスク達成",
|
||||
"transactionTypeHabitUndo": "習慣取り消し",
|
||||
"transactionTypeTaskUndo": "タスク取り消し",
|
||||
"transactionTypeWishRedemption": "報酬使用",
|
||||
"transactionTypeManualAdjustment": "手動調整",
|
||||
"transactionTypeCoinReset": "コインリセット",
|
||||
"transactionTypeInitialBalance": "初期残高"
|
||||
},
|
||||
"NotificationBell": {
|
||||
"errorUpdateTimestamp": "通知の既読タイムスタンプの更新に失敗しました:"
|
||||
},
|
||||
"PomodoroTimer": {
|
||||
"focusLabel1": "集中しよう",
|
||||
"focusLabel2": "君ならできる",
|
||||
"focusLabel3": "頑張れ",
|
||||
"focusLabel4": "やり遂げろ",
|
||||
"focusLabel5": "実現させよう",
|
||||
"focusLabel6": "強く在れ",
|
||||
"focusLabel7": "突破しよう",
|
||||
"focusLabel8": "1歩ずつ進もう",
|
||||
"focusLabel9": "君にはできる",
|
||||
"focusLabel10": "集中して征服しよう",
|
||||
"breakLabel1": "休憩しよう",
|
||||
"breakLabel2": "リラックスして充電しよう",
|
||||
"breakLabel3": "深呼吸しよう",
|
||||
"breakLabel4": "ストレッチしよう",
|
||||
"breakLabel5": "リフレッシュしよう",
|
||||
"breakLabel6": "君ならできる",
|
||||
"breakLabel7": "エネルギーを充電しよう",
|
||||
"breakLabel8": "少し離れよう",
|
||||
"breakLabel9": "心をクリアにしよう",
|
||||
"breakLabel10": "休んで回復しよう",
|
||||
"focusType": "集中",
|
||||
"breakType": "休憩",
|
||||
"pauseButton": "一時停止",
|
||||
"startButton": "開始",
|
||||
"resetButton": "リセット",
|
||||
"skipButton": "スキップ",
|
||||
"wakeLockNotSupported": "ブラウザがWake Lockをサポートしていません",
|
||||
"wakeLockInUse": "Wake Lockは既に使用中です",
|
||||
"wakeLockRequestError": "Wake Lockのリクエストエラー:",
|
||||
"wakeLockReleaseError": "Wake Lockの解放エラー:"
|
||||
},
|
||||
"HabitCalendar": {
|
||||
"title": "習慣カレンダー",
|
||||
"calendarCardTitle": "カレンダー",
|
||||
"selectDatePrompt": "日付を選択",
|
||||
"tasksSectionTitle": "タスク",
|
||||
"habitsSectionTitle": "習慣",
|
||||
"errorCompletingPastHabit": "過去の習慣を完了する際にエラーが発生しました:"
|
||||
},
|
||||
"NotificationDropdown": {
|
||||
"notLoggedIn": "ログインしていません。",
|
||||
"userCompletedItem": "{username}さんが{itemName}を完了しました。",
|
||||
"userRedeemedItem": "{username}さんが{itemName}を使用しました。",
|
||||
"activityRelatedToItem": "{username}さんによる{itemName}に関連するアクティビティ。",
|
||||
"defaultUsername": "誰か",
|
||||
"defaultItemName": "共有アイテム",
|
||||
"notificationsTitle": "通知",
|
||||
"notificationsTooltip": "他のユーザーがあなたと共有した習慣やウィッシュリストの達成・使用を表示します(管理者のみ)",
|
||||
"noNotificationsYet": "まだ通知はありません。"
|
||||
},
|
||||
"AboutModal": {
|
||||
"dialogArisLabel": "概要",
|
||||
"changelogButton": "変更履歴",
|
||||
"createdByPrefix": "❤️で作成:",
|
||||
"starOnGitHubButton": "GitHubでスターしよう"
|
||||
},
|
||||
"PermissionSelector": {
|
||||
"permissionsTitle": "権限",
|
||||
"adminAccessLabel": "管理者アクセス",
|
||||
"adminAccessDescription": "管理者は全ユーザーの全データにアクセスできます",
|
||||
"resourceHabitTask": "習慣 / タスク",
|
||||
"resourceWishlist": "ウィッシュリスト",
|
||||
"resourceCoins": "コイン",
|
||||
"permissionWrite": "書き込み",
|
||||
"permissionInteract": "操作"
|
||||
},
|
||||
"UserForm": {
|
||||
"toastUserUpdatedTitle": "ユーザーを更新しました",
|
||||
"toastUserUpdatedDescription": "{username}さんの情報を更新しました",
|
||||
"toastUserCreatedTitle": "ユーザーを作成しました",
|
||||
"toastUserCreatedDescription": "{username}さんを作成しました",
|
||||
"actionUpdate": "更新",
|
||||
"actionCreate": "作成",
|
||||
"errorFailedUserAction": "ユーザーの{action}に失敗しました",
|
||||
"errorTitle": "エラー",
|
||||
"errorFileSizeLimit": "ファイルサイズは5MB以下である必要があります",
|
||||
"toastAvatarUploadedTitle": "アバターをアップロードしました",
|
||||
"toastAvatarUploadedDescription": "アバターのアップロードに成功しました",
|
||||
"errorFailedAvatarUpload": "アバターのアップロードに失敗しました",
|
||||
"changeAvatarButton": "アバターを変更",
|
||||
"uploadAvatarButton": "アバターをアップロード",
|
||||
"usernameLabel": "ユーザー名",
|
||||
"usernamePlaceholder": "ユーザー名",
|
||||
"newPasswordLabel": "新しいパスワード",
|
||||
"passwordLabel": "パスワード",
|
||||
"passwordPlaceholderEdit": "現在のままにする場合は空欄",
|
||||
"passwordPlaceholderCreate": "パスワードを入力",
|
||||
"demoPasswordDisabledMessage": "デモインスタンスではパスワードは自動的に無効化されます",
|
||||
"disablePasswordLabel": "パスワードを無効化",
|
||||
"cancelButton": "キャンセル",
|
||||
"saveChangesButton": "変更を保存",
|
||||
"createUserButton": "ユーザーを作成"
|
||||
},
|
||||
"ViewToggle": {
|
||||
"habitsLabel": "習慣",
|
||||
"tasksLabel": "タスク"
|
||||
},
|
||||
"HabitItem": {
|
||||
"overdue": "期限超過",
|
||||
"whenLabel": "いつ: {frequency}",
|
||||
"coinsPerCompletion": "1回あたり{count}コイン",
|
||||
"completedStatus": "完了",
|
||||
"completedStatusCount": "完了({completed}/{target})",
|
||||
"completedStatusCountMobile": "{completed}/{target}",
|
||||
"completeButton": "完了",
|
||||
"completeButtonCount": "完了({completed}/{target})",
|
||||
"completeButtonCountMobile": "{completed}/{target}",
|
||||
"undoButton": "取り消し",
|
||||
"editButton": "編集"
|
||||
},
|
||||
"TransactionNoteEditor": {
|
||||
"noteTooLongTitle": "メモが長すぎます",
|
||||
"noteTooLongDescription": "メモは200文字以内である必要があります",
|
||||
"errorSavingNoteTitle": "メモの保存エラー",
|
||||
"errorDeletingNoteTitle": "メモの削除エラー",
|
||||
"pleaseTryAgainDescription": "再度お試しください",
|
||||
"addNotePlaceholder": "メモを追加...",
|
||||
"saveNoteTitle": "メモを保存",
|
||||
"cancelButtonTitle": "キャンセル",
|
||||
"deleteNoteTitle": "メモを削除",
|
||||
"editNoteAriaLabel": "メモを編集"
|
||||
},
|
||||
"Profile": {
|
||||
"guestUsername": "ゲスト",
|
||||
"editProfileButton": "プロフィールを編集",
|
||||
"signOutSuccessTitle": "サインアウトに成功しました",
|
||||
"signOutSuccessDescription": "アカウントからサインアウトしました",
|
||||
"signOutErrorTitle": "サインアウトエラー",
|
||||
"signOutErrorDescription": "サインアウトに失敗しました",
|
||||
"switchUserButton": "ユーザーを切り替え",
|
||||
"settingsLink": "設定",
|
||||
"aboutButton": "概要",
|
||||
"themeLabel": "テーマ",
|
||||
"editProfileModalTitle": "プロフィールを編集"
|
||||
},
|
||||
"PasswordEntryForm": {
|
||||
"notYouButton": "違うユーザー?",
|
||||
"passwordLabel": "パスワード",
|
||||
"passwordPlaceholder": "パスワードを入力",
|
||||
"loginErrorToastTitle": "エラー",
|
||||
"loginFailedErrorToastDescription": "ログインに失敗しました",
|
||||
"cancelButton": "キャンセル",
|
||||
"loginButton": "ログイン"
|
||||
},
|
||||
"CompletionCountBadge": {
|
||||
"countCompleted": "完了 {completedCount}/{totalCount}"
|
||||
},
|
||||
"SettingsPage": {
|
||||
"title": "設定",
|
||||
"uiSettingsTitle": "UI設定",
|
||||
"numberFormattingLabel": "数字のフォーマット",
|
||||
"numberFormattingDescription": "大きな数字をフォーマットする(例:1K、1M、1B)",
|
||||
"numberGroupingLabel": "数字のグループ化",
|
||||
"numberGroupingDescription": "3桁区切りを使用する(例:1,000 対 1000)",
|
||||
"systemSettingsTitle": "システム設定",
|
||||
"timezoneLabel": "タイムゾーン",
|
||||
"timezoneDescription": "正確な日付追跡のためにタイムゾーンを選択",
|
||||
"weekStartDayLabel": "週の開始日",
|
||||
"weekStartDayDescription": "週の最初の曜日を選択",
|
||||
"weekdays": {
|
||||
"sunday": "日曜日",
|
||||
"monday": "月曜日",
|
||||
"tuesday": "火曜日",
|
||||
"wednesday": "水曜日",
|
||||
"thursday": "木曜日",
|
||||
"friday": "金曜日",
|
||||
"saturday": "土曜日"
|
||||
},
|
||||
"autoBackupLabel": "自動バックアップ",
|
||||
"autoBackupTooltip": "有効にすると、アプリケーションデータ(習慣、コイン、設定など)が毎日午前2時頃(サーバー時間)に自動的にバックアップされます。バックアップはプロジェクトルートの`backups/`ディレクトリにZIPファイルとして保存されます。最新の7つ分のバックアップのみ保持され、古いものは自動的に削除されます。",
|
||||
"autoBackupDescription": "毎日データを自動バックアップ",
|
||||
"languageLabel": "言語",
|
||||
"languageDescription": "アプリケーションの表示言語を選択",
|
||||
"languageChangedTitle": "言語が変更されました",
|
||||
"languageChangedDescription": "変更を反映するにはページを更新してください",
|
||||
"languageDisabledInDemoTooltip": "デモ版では言語の変更が無効になっています。"
|
||||
},
|
||||
"Common": {
|
||||
"authenticationRequiredTitle": "認証が必要です",
|
||||
"authenticationRequiredDescription": "続行するにはサインインしてください。",
|
||||
"permissionDeniedTitle": "権限がありません",
|
||||
"permissionDeniedDescription": "{resource}sに対する{action}権限がありません。",
|
||||
"undoButton": "取り消し",
|
||||
"redoButton": "やり直し",
|
||||
"errorTitle": "エラー"
|
||||
},
|
||||
"useHabits": {
|
||||
"alreadyCompletedTitle": "既に完了しています",
|
||||
"alreadyCompletedDescription": "今日は既にこの習慣を完了しています。",
|
||||
"completedTitle": "完了しました!",
|
||||
"earnedCoinsDescription": "{coinReward}コインを獲得しました。",
|
||||
"progressTitle": "進捗!",
|
||||
"progressDescription": "今日は{count}/{target}回完了しました。",
|
||||
"completionUndoneTitle": "完了を取り消しました",
|
||||
"completionUndoneDescription": "今日は{count}/{target}回完了しています。",
|
||||
"noCompletionsToUndoTitle": "取り消す完了がありません",
|
||||
"noCompletionsToUndoDescription": "この習慣は今日まだ完了していません。",
|
||||
"alreadyCompletedPastDateTitle": "既に完了しています",
|
||||
"alreadyCompletedPastDateDescription": "この習慣は{dateKey}に既に完了しています。",
|
||||
"earnedCoinsPastDateDescription": "{dateKey}に{coinReward}コインを獲得しました。",
|
||||
"progressPastDateDescription": "{dateKey}に{count}/{target}回完了しました。"
|
||||
},
|
||||
"useWishlist": {
|
||||
"redemptionLimitReachedTitle": "使用回数制限に達しました",
|
||||
"redemptionLimitReachedDescription": "\"{itemName}\"の最大使用回数に達しました。",
|
||||
"rewardRedeemedTitle": "🎉 報酬を使用しました!",
|
||||
"rewardRedeemedDescription": "\"{itemName}\"を{itemCoinCost}コインで使用しました。",
|
||||
"notEnoughCoinsTitle": "コインが不足しています",
|
||||
"notEnoughCoinsDescription": "この報酬を使用するにはあと{coinsNeeded}コイン必要です。"
|
||||
},
|
||||
"useCoins": {
|
||||
"invalidAmountTitle": "無効な値です",
|
||||
"invalidAmountDescription": "有効な正の数を入力してください",
|
||||
"successTitle": "成功しました",
|
||||
"addedCoinsDescription": "{amount}コインを追加しました",
|
||||
"removedCoinsDescription": "{amount}コインを削除しました",
|
||||
"transactionNotFoundDescription": "取引が見つかりません"
|
||||
}
|
||||
}
|
||||
407
messages/ru.json
Normal file
407
messages/ru.json
Normal file
@@ -0,0 +1,407 @@
|
||||
{
|
||||
"Dashboard": {
|
||||
"title": "Панель управления"
|
||||
},
|
||||
"HabitList": {
|
||||
"myTasks": "Мои задачи",
|
||||
"myHabits": "Мои привычки",
|
||||
"addTaskButton": "Добавить задачу",
|
||||
"addHabitButton": "Добавить привычку",
|
||||
"searchTasksPlaceholder": "Поиск задач...",
|
||||
"searchHabitsPlaceholder": "Поиск привычек...",
|
||||
"sortByLabel": "Сортировать по:",
|
||||
"sortByName": "Имени",
|
||||
"sortByCoinReward": "Награде",
|
||||
"sortByDueDate": "Сроку",
|
||||
"sortByFrequency": "Частоте",
|
||||
"toggleSortOrderAriaLabel": "Переключить порядок сортировки",
|
||||
"noTasksFoundMessage": "Задачи не найдены.",
|
||||
"noHabitsFoundMessage": "Привычки не найдены.",
|
||||
"emptyStateTasksTitle": "Нет задач",
|
||||
"emptyStateHabitsTitle": "Нет привычек",
|
||||
"emptyStateTasksDescription": "Создайте свою первую задачу",
|
||||
"emptyStateHabitsDescription": "Создайте свою первую привычку",
|
||||
"archivedSectionTitle": "Архив",
|
||||
"deleteTaskDialogTitle": "Удалить задачу",
|
||||
"deleteHabitDialogTitle": "Удалить привычку",
|
||||
"deleteTaskDialogMessage": "Вы уверены, что хотите удалить эту задачу? Это действие нельзя отменить.",
|
||||
"deleteHabitDialogMessage": "Вы уверены, что хотите удалить эту привычку? Это действие нельзя отменить.",
|
||||
"deleteButton": "Удалить"
|
||||
},
|
||||
"DailyOverview": {
|
||||
"addTaskButtonLabel": "Добавить задачу",
|
||||
"addHabitButtonLabel": "Добавить привычку",
|
||||
"todaysOverviewTitle": "Сегодня",
|
||||
"dailyTasksTitle": "Задачи на сегодня",
|
||||
"noTasksDueTodayMessage": "Нет задач на сегодня.",
|
||||
"dailyHabitsTitle": "Привычки на сегодня",
|
||||
"noHabitsDueTodayMessage": "Нет привычек на сегодня.",
|
||||
"wishlistGoalsTitle": "Цели",
|
||||
"redeemableBadgeLabel": "{count}/{total} Доступно",
|
||||
"noWishlistItemsMessage": "Нет целей.",
|
||||
"readyToRedeemMessage": "Доступно!",
|
||||
"coinsToGoMessage": "Осталось {amount} монет",
|
||||
"showLessButton": "Свернуть",
|
||||
"showAllButton": "Показать все",
|
||||
"viewButton": "Просмотр",
|
||||
"deleteTaskDialogTitle": "Удалить задачу",
|
||||
"deleteHabitDialogTitle": "Удалить привычку",
|
||||
"confirmDeleteDialogMessage": "Вы уверены, что хотите удалить \"{name}\"? Это действие нельзя отменить.",
|
||||
"deleteButton": "Удалить",
|
||||
"overdueTooltip": "Просрочено"
|
||||
},
|
||||
"HabitContextMenuItems": {
|
||||
"startPomodoro": "Начать помидорку",
|
||||
"moveToToday": "Перенести на сегодня",
|
||||
"moveToTomorrow": "Перенести на завтра",
|
||||
"unpin": "Открепить",
|
||||
"pin": "Закрепить",
|
||||
"edit": "Редактировать",
|
||||
"archive": "В архив",
|
||||
"unarchive": "Из архива",
|
||||
"delete": "Удалить"
|
||||
},
|
||||
"HabitStreak": {
|
||||
"dailyCompletionStreakTitle": "Ежедневный прогресс",
|
||||
"tooltipHabitsLabel": "привычки",
|
||||
"tooltipTasksLabel": "задачи",
|
||||
"tooltipCompletedLabel": "Выполнено"
|
||||
},
|
||||
"CoinBalance": {
|
||||
"coinBalanceTitle": "Баланс"
|
||||
},
|
||||
"AddEditHabitModal": {
|
||||
"editTaskTitle": "Редактировать задачу",
|
||||
"editHabitTitle": "Редактировать привычку",
|
||||
"addNewTaskTitle": "Новая задача",
|
||||
"addNewHabitTitle": "Новая привычка",
|
||||
"nameLabel": "Название *",
|
||||
"descriptionLabel": "Описание",
|
||||
"whenLabel": "Когда *",
|
||||
"completeLabel": "Выполнено",
|
||||
"timesSuffix": "раз",
|
||||
"rewardLabel": "Награда",
|
||||
"coinsSuffix": "монет",
|
||||
"shareLabel": "Поделиться",
|
||||
"saveChangesButton": "Сохранить",
|
||||
"addTaskButton": "Добавить задачу",
|
||||
"addHabitButton": "Добавить привычку"
|
||||
},
|
||||
"ConfirmDialog": {
|
||||
"confirmButton": "Подтвердить",
|
||||
"cancelButton": "Отмена"
|
||||
},
|
||||
"AddEditWishlistItemModal": {
|
||||
"editTitle": "Редактировать цель",
|
||||
"addTitle": "Новая цель",
|
||||
"nameLabel": "Название *",
|
||||
"descriptionLabel": "Описание",
|
||||
"costLabel": "Стоимость",
|
||||
"coinsSuffix": "монет",
|
||||
"redeemableLabel": "Доступно",
|
||||
"timesSuffix": "раз",
|
||||
"errorNameRequired": "Название обязательно",
|
||||
"errorCoinCostMin": "Минимальная стоимость 1 монета",
|
||||
"errorTargetCompletionsMin": "Минимум 1 выполнение",
|
||||
"errorInvalidUrl": "Некорректная ссылка",
|
||||
"linkLabel": "Ссылка",
|
||||
"shareLabel": "Поделиться",
|
||||
"saveButton": "Сохранить",
|
||||
"addButton": "Добавить цель"
|
||||
},
|
||||
"Navigation": {
|
||||
"dashboard": "Панель",
|
||||
"tasks": "Задачи",
|
||||
"habits": "Привычки",
|
||||
"calendar": "Календарь",
|
||||
"wishlist": "Цели",
|
||||
"coins": "Монеты"
|
||||
},
|
||||
"TodayEarnedCoins": {
|
||||
"todaySuffix": "сегодня"
|
||||
},
|
||||
"WishlistItem": {
|
||||
"usesLeftSingular": "использование",
|
||||
"usesLeftPlural": "использований",
|
||||
"coinsSuffix": "монет",
|
||||
"redeem": "Использовать",
|
||||
"redeemedDone": "Готово",
|
||||
"redeemedExclamation": "Использовано!",
|
||||
"editButton": "Редактировать",
|
||||
"archiveButton": "В архив",
|
||||
"unarchiveButton": "Из архива",
|
||||
"deleteButton": "Удалить"
|
||||
},
|
||||
"WishlistManager": {
|
||||
"title": "Мои цели",
|
||||
"addRewardButton": "Добавить цель",
|
||||
"emptyStateTitle": "Нет целей",
|
||||
"emptyStateDescription": "Добавьте цели, которые хотите достичь",
|
||||
"archivedSectionTitle": "Архив",
|
||||
"popupBlockedTitle": "Блокировка",
|
||||
"popupBlockedDescription": "Разрешите всплывающие окна для открытия ссылки",
|
||||
"deleteDialogTitle": "Удалить цель",
|
||||
"deleteDialogMessage": "Вы уверены, что хотите удалить эту цель? Это действие нельзя отменить.",
|
||||
"deleteButton": "Удалить"
|
||||
},
|
||||
"UserSelectModal": {
|
||||
"addUserButton": "Добавить пользователя",
|
||||
"createNewUserTitle": "Создать нового пользователя",
|
||||
"selectUserTitle": "Выбрать пользователя",
|
||||
"signInSuccessTitle": "Успешный вход",
|
||||
"signInSuccessDescription": "Добро пожаловать, {username}!",
|
||||
"errorInvalidPassword": "Неверный пароль"
|
||||
},
|
||||
"CoinsManager": {
|
||||
"title": "Управление монетами",
|
||||
"currentBalanceLabel": "Текущий баланс",
|
||||
"coinsSuffix": "монет",
|
||||
"addCoinsButton": "Добавить монеты",
|
||||
"removeCoinsButton": "Удалить монеты",
|
||||
"statisticsTitle": "Статистика",
|
||||
"totalEarnedLabel": "Всего заработано",
|
||||
"totalSpentLabel": "Всего потрачено",
|
||||
"totalTransactionsLabel": "Всего транзакций",
|
||||
"todaysEarnedLabel": "Заработано сегодня",
|
||||
"todaysSpentLabel": "Потрачено сегодня",
|
||||
"todaysTransactionsLabel": "Транзакций сегодня",
|
||||
"transactionHistoryTitle": "История транзакций",
|
||||
"showLabel": "Показать:",
|
||||
"entriesSuffix": "записей",
|
||||
"showingEntries": "Показано с {from} по {to} из {total} записей",
|
||||
"noTransactionsTitle": "Нет транзакций",
|
||||
"noTransactionsDescription": "История транзакций появится здесь, когда вы начнете зарабатывать или тратить монеты",
|
||||
"pageLabel": "Страница",
|
||||
"ofLabel": "из",
|
||||
"transactionTypeHabitCompletion": "Выполнение привычки",
|
||||
"transactionTypeTaskCompletion": "Выполнение задачи",
|
||||
"transactionTypeHabitUndo": "Отмена привычки",
|
||||
"transactionTypeTaskUndo": "Отмена задачи",
|
||||
"transactionTypeWishRedemption": "Использование цели",
|
||||
"transactionTypeManualAdjustment": "Ручная корректировка",
|
||||
"transactionTypeCoinReset": "Сброс монет",
|
||||
"transactionTypeInitialBalance": "Начальный баланс"
|
||||
},
|
||||
"NotificationBell": {
|
||||
"errorUpdateTimestamp": "Не удалось обновить отметку времени прочтения уведомления:"
|
||||
},
|
||||
"PomodoroTimer": {
|
||||
"focusLabel1": "Сосредоточьтесь",
|
||||
"focusLabel2": "У вас получится",
|
||||
"focusLabel3": "Продолжайте",
|
||||
"focusLabel4": "Разгромите это",
|
||||
"focusLabel5": "Воплотите это в жизнь",
|
||||
"focusLabel6": "Оставайтесь сильными",
|
||||
"focusLabel7": "Прорвитесь",
|
||||
"focusLabel8": "Один шаг за раз",
|
||||
"focusLabel9": "Вы можете это сделать",
|
||||
"focusLabel10": "Сосредоточьтесь и побеждайте",
|
||||
"breakLabel1": "Передохните",
|
||||
"breakLabel2": "Расслабьтесь и перезагрузитесь",
|
||||
"breakLabel3": "Дышите глубже",
|
||||
"breakLabel4": "Потянитесь",
|
||||
"breakLabel5": "Освежитесь",
|
||||
"breakLabel6": "Вы этого заслуживаете",
|
||||
"breakLabel7": "Восстановите энергию",
|
||||
"breakLabel8": "Отойдите на немного",
|
||||
"breakLabel9": "Очистите свой разум",
|
||||
"breakLabel10": "Отдохните и восстановитесь",
|
||||
"focusType": "Фокус",
|
||||
"breakType": "Перерыв",
|
||||
"pauseButton": "Пауза",
|
||||
"startButton": "Старт",
|
||||
"resetButton": "Сброс",
|
||||
"skipButton": "Пропустить",
|
||||
"wakeLockNotSupported": "Браузер не поддерживает блокировку экрана",
|
||||
"wakeLockInUse": "Блокировка экрана уже используется",
|
||||
"wakeLockRequestError": "Ошибка запроса блокировки экрана:",
|
||||
"wakeLockReleaseError": "Ошибка освобождения блокировки экрана:"
|
||||
},
|
||||
"HabitCalendar": {
|
||||
"title": "Календарь привычек",
|
||||
"calendarCardTitle": "Календарь",
|
||||
"selectDatePrompt": "Выберите дату",
|
||||
"tasksSectionTitle": "Задачи",
|
||||
"habitsSectionTitle": "Привычки",
|
||||
"errorCompletingPastHabit": "Ошибка завершения прошлой привычки:"
|
||||
},
|
||||
"NotificationDropdown": {
|
||||
"notLoggedIn": "Не выполнен вход.",
|
||||
"userCompletedItem": "{username} выполнил(а) {itemName}.",
|
||||
"userRedeemedItem": "{username} использовал(а) {itemName}.",
|
||||
"activityRelatedToItem": "Действие, связанное с {itemName}, пользователем {username}.",
|
||||
"defaultUsername": "Кто-то",
|
||||
"defaultItemName": "общий элемент",
|
||||
"notificationsTitle": "Уведомления",
|
||||
"notificationsTooltip": "Показывает завершения или погашения другими пользователями для привычек или списка желаний, которыми вы поделились с ними (вы должны быть администратором)",
|
||||
"noNotificationsYet": "Пока нет уведомлений."
|
||||
},
|
||||
"AboutModal": {
|
||||
"dialogArisLabel": "о программе",
|
||||
"changelogButton": "Список изменений",
|
||||
"createdByPrefix": "Сделано с любовью ❤️ от",
|
||||
"starOnGitHubButton": "Звезда на GitHub"
|
||||
},
|
||||
"PermissionSelector": {
|
||||
"permissionsTitle": "Разрешения",
|
||||
"adminAccessLabel": "Доступ администратора",
|
||||
"adminAccessDescription": "Администраторы имеют полный доступ ко всем данным для всех пользователей",
|
||||
"resourceHabitTask": "Привычка / Задача",
|
||||
"resourceWishlist": "Список желаний",
|
||||
"resourceCoins": "Монеты",
|
||||
"permissionWrite": "Запись",
|
||||
"permissionInteract": "Взаимодействие"
|
||||
},
|
||||
"UserForm": {
|
||||
"toastUserUpdatedTitle": "Пользователь обновлен",
|
||||
"toastUserUpdatedDescription": "Пользователь {username} успешно обновлен",
|
||||
"toastUserCreatedTitle": "Пользователь создан",
|
||||
"toastUserCreatedDescription": "Пользователь {username} успешно создан",
|
||||
"actionUpdate": "обновить",
|
||||
"actionCreate": "создать",
|
||||
"errorFailedUserAction": "Не удалось {action} пользователя",
|
||||
"errorTitle": "Ошибка",
|
||||
"errorFileSizeLimit": "Размер файла должен быть менее 5 МБ",
|
||||
"toastAvatarUploadedTitle": "Аватар загружен",
|
||||
"toastAvatarUploadedDescription": "Аватар успешно загружен",
|
||||
"errorFailedAvatarUpload": "Не удалось загрузить аватар",
|
||||
"changeAvatarButton": "Изменить аватар",
|
||||
"uploadAvatarButton": "Загрузить аватар",
|
||||
"usernameLabel": "Имя пользователя",
|
||||
"usernamePlaceholder": "Имя пользователя",
|
||||
"newPasswordLabel": "Новый пароль",
|
||||
"passwordLabel": "Пароль",
|
||||
"passwordPlaceholderEdit": "Оставьте пустым, чтобы сохранить текущий",
|
||||
"passwordPlaceholderCreate": "Введите пароль",
|
||||
"demoPasswordDisabledMessage": "Пароль автоматически отключен в демонстрационном экземпляре",
|
||||
"disablePasswordLabel": "Отключить пароль",
|
||||
"cancelButton": "Отмена",
|
||||
"saveChangesButton": "Сохранить изменения",
|
||||
"createUserButton": "Создать пользователя"
|
||||
},
|
||||
"ViewToggle": {
|
||||
"habitsLabel": "Привычки",
|
||||
"tasksLabel": "Задачи"
|
||||
},
|
||||
"HabitItem": {
|
||||
"overdue": "Просрочено",
|
||||
"whenLabel": "Когда: {frequency}",
|
||||
"coinsPerCompletion": "{count} монет за выполнение",
|
||||
"completedStatus": "Выполнено",
|
||||
"completedStatusCount": "Выполнено ({completed}/{target})",
|
||||
"completedStatusCountMobile": "{completed}/{target}",
|
||||
"completeButton": "Выполнить",
|
||||
"completeButtonCount": "Выполнить ({completed}/{target})",
|
||||
"completeButtonCountMobile": "{completed}/{target}",
|
||||
"undoButton": "Отменить",
|
||||
"editButton": "Редактировать"
|
||||
},
|
||||
"TransactionNoteEditor": {
|
||||
"noteTooLongTitle": "Слишком длинная заметка",
|
||||
"noteTooLongDescription": "Заметки должны быть менее 200 символов",
|
||||
"errorSavingNoteTitle": "Ошибка сохранения заметки",
|
||||
"errorDeletingNoteTitle": "Ошибка удаления заметки",
|
||||
"pleaseTryAgainDescription": "Пожалуйста, попробуйте еще раз",
|
||||
"addNotePlaceholder": "Добавить заметку...",
|
||||
"saveNoteTitle": "Сохранить заметку",
|
||||
"cancelButtonTitle": "Отмена",
|
||||
"deleteNoteTitle": "Удалить заметку",
|
||||
"editNoteAriaLabel": "Редактировать заметку"
|
||||
},
|
||||
"Profile": {
|
||||
"guestUsername": "Гость",
|
||||
"editProfileButton": "Редактировать профиль",
|
||||
"signOutSuccessTitle": "Выход выполнен успешно",
|
||||
"signOutSuccessDescription": "Вы вышли из своей учетной записи",
|
||||
"signOutErrorTitle": "Ошибка выхода",
|
||||
"signOutErrorDescription": "Не удалось выйти",
|
||||
"switchUserButton": "Сменить пользователя",
|
||||
"settingsLink": "Настройки",
|
||||
"aboutButton": "О программе",
|
||||
"themeLabel": "Тема",
|
||||
"editProfileModalTitle": "Редактировать профиль"
|
||||
},
|
||||
"PasswordEntryForm": {
|
||||
"notYouButton": "Не вы?",
|
||||
"passwordLabel": "Пароль",
|
||||
"passwordPlaceholder": "Введите пароль",
|
||||
"loginErrorToastTitle": "Ошибка",
|
||||
"loginFailedErrorToastDescription": "Не удалось войти",
|
||||
"cancelButton": "Отмена",
|
||||
"loginButton": "Войти"
|
||||
},
|
||||
"CompletionCountBadge": {
|
||||
"countCompleted": "{completedCount}/{totalCount} выполнено"
|
||||
},
|
||||
"SettingsPage": {
|
||||
"title": "Настройки",
|
||||
"uiSettingsTitle": "Интерфейс",
|
||||
"numberFormattingLabel": "Формат чисел",
|
||||
"numberFormattingDescription": "Использовать сокращения (например, 1К, 1М, 1Млрд)",
|
||||
"numberGroupingLabel": "Разделители",
|
||||
"numberGroupingDescription": "Использовать разделители тысяч (например, 1 000 вместо 1000)",
|
||||
"systemSettingsTitle": "Система",
|
||||
"timezoneLabel": "Часовой пояс",
|
||||
"timezoneDescription": "Выберите ваш часовой пояс",
|
||||
"weekStartDayLabel": "Первый день недели",
|
||||
"weekStartDayDescription": "Выберите первый день недели",
|
||||
"weekdays": {
|
||||
"sunday": "Воскресенье",
|
||||
"monday": "Понедельник",
|
||||
"tuesday": "Вторник",
|
||||
"wednesday": "Среда",
|
||||
"thursday": "Четверг",
|
||||
"friday": "Пятница",
|
||||
"saturday": "Суббота"
|
||||
},
|
||||
"autoBackupLabel": "Авто-бэкап",
|
||||
"autoBackupTooltip": "При включении данные будут автоматически резервироваться ежедневно около 2:00 по времени сервера. Бэкапы хранятся в виде ZIP-файлов в директории `backups/`. Хранятся только последние 7 бэкапов.",
|
||||
"autoBackupDescription": "Автоматическое резервное копирование данных",
|
||||
"languageLabel": "Язык",
|
||||
"languageDescription": "Выберите предпочитаемый язык интерфейса.",
|
||||
"languageChangedTitle": "Язык изменен",
|
||||
"languageChangedDescription": "Перезагрузите страницу для применения изменений",
|
||||
"languageDisabledInDemoTooltip": "Смена языка недоступна в демо-версии."
|
||||
},
|
||||
"Common": {
|
||||
"authenticationRequiredTitle": "Требуется аутентификация",
|
||||
"authenticationRequiredDescription": "Пожалуйста, войдите, чтобы продолжить.",
|
||||
"permissionDeniedTitle": "Отказано в доступе",
|
||||
"permissionDeniedDescription": "У вас нет разрешения на {action} для {resource}.",
|
||||
"undoButton": "Отменить",
|
||||
"redoButton": "Повторить",
|
||||
"errorTitle": "Ошибка"
|
||||
},
|
||||
"useHabits": {
|
||||
"alreadyCompletedTitle": "Уже выполнено",
|
||||
"alreadyCompletedDescription": "Вы уже выполнили эту привычку сегодня.",
|
||||
"completedTitle": "Выполнено!",
|
||||
"earnedCoinsDescription": "Вы заработали {coinReward} монет.",
|
||||
"progressTitle": "Прогресс!",
|
||||
"progressDescription": "Вы выполнили {count}/{target} раз сегодня.",
|
||||
"completionUndoneTitle": "Выполнение отменено",
|
||||
"completionUndoneDescription": "У вас {count}/{target} выполнений сегодня.",
|
||||
"noCompletionsToUndoTitle": "Нет отмен",
|
||||
"noCompletionsToUndoDescription": "Эта привычка не была выполнена сегодня.",
|
||||
"alreadyCompletedPastDateTitle": "Уже выполнено",
|
||||
"alreadyCompletedPastDateDescription": "Эта привычка уже была выполнена {dateKey}.",
|
||||
"earnedCoinsPastDateDescription": "Вы заработали {coinReward} монет за {dateKey}.",
|
||||
"progressPastDateDescription": "Вы выполнили {count}/{target} раз {dateKey}."
|
||||
},
|
||||
"useWishlist": {
|
||||
"redemptionLimitReachedTitle": "Достигнут лимит погашения",
|
||||
"redemptionLimitReachedDescription": "Вы достигли максимального количества погашений для \"{itemName}\".",
|
||||
"rewardRedeemedTitle": "🎉 Награда получена!",
|
||||
"rewardRedeemedDescription": "Вы получили \"{itemName}\" за {itemCoinCost} монет.",
|
||||
"notEnoughCoinsTitle": "Недостаточно монет",
|
||||
"notEnoughCoinsDescription": "Вам нужно еще {coinsNeeded} монет, чтобы получить эту награду."
|
||||
},
|
||||
"useCoins": {
|
||||
"invalidAmountTitle": "Неверная сумма",
|
||||
"invalidAmountDescription": "Пожалуйста, введите положительное число",
|
||||
"successTitle": "Успех",
|
||||
"addedCoinsDescription": "Добавлено {amount} монет",
|
||||
"removedCoinsDescription": "Удалено {amount} монет",
|
||||
"transactionNotFoundDescription": "Транзакция не найдена"
|
||||
}
|
||||
}
|
||||
407
messages/zh.json
Normal file
407
messages/zh.json
Normal file
@@ -0,0 +1,407 @@
|
||||
{
|
||||
"Dashboard": {
|
||||
"title": "仪表板"
|
||||
},
|
||||
"HabitList": {
|
||||
"myTasks": "我的任务",
|
||||
"myHabits": "我的习惯",
|
||||
"addTaskButton": "添加任务",
|
||||
"addHabitButton": "添加习惯",
|
||||
"searchTasksPlaceholder": "搜索任务...",
|
||||
"searchHabitsPlaceholder": "搜索习惯...",
|
||||
"sortByLabel": "排序方式:",
|
||||
"sortByName": "名称",
|
||||
"sortByCoinReward": "金币奖励",
|
||||
"sortByDueDate": "截止日期",
|
||||
"sortByFrequency": "频率",
|
||||
"toggleSortOrderAriaLabel": "切换排序顺序",
|
||||
"noTasksFoundMessage": "未找到符合搜索条件的任务。",
|
||||
"noHabitsFoundMessage": "未找到符合搜索条件的习惯。",
|
||||
"emptyStateTasksTitle": "暂无任务",
|
||||
"emptyStateHabitsTitle": "暂无习惯",
|
||||
"emptyStateTasksDescription": "创建第一个任务以开始跟踪进度",
|
||||
"emptyStateHabitsDescription": "创建第一个习惯以开始跟踪进度",
|
||||
"archivedSectionTitle": "已归档",
|
||||
"deleteTaskDialogTitle": "删除任务",
|
||||
"deleteHabitDialogTitle": "删除习惯",
|
||||
"deleteTaskDialogMessage": "确定要删除此任务吗?此操作无法撤消。",
|
||||
"deleteHabitDialogMessage": "确定要删除此习惯吗?此操作无法撤消。",
|
||||
"deleteButton": "删除"
|
||||
},
|
||||
"DailyOverview": {
|
||||
"addTaskButtonLabel": "添加任务",
|
||||
"addHabitButtonLabel": "添加习惯",
|
||||
"todaysOverviewTitle": "今日概览",
|
||||
"dailyTasksTitle": "每日任务",
|
||||
"noTasksDueTodayMessage": "今天没有任务。添加一些任务以开始!",
|
||||
"dailyHabitsTitle": "每日习惯",
|
||||
"noHabitsDueTodayMessage": "今天没有习惯。添加一些习惯以开始!",
|
||||
"wishlistGoalsTitle": "愿望清单目标",
|
||||
"redeemableBadgeLabel": "{count}/{total} 可兑换",
|
||||
"noWishlistItemsMessage": "还没有愿望清单项目。添加一些目标来努力实现吧!",
|
||||
"readyToRedeemMessage": "准备兑换!",
|
||||
"coinsToGoMessage": "还需 {amount} 个金币",
|
||||
"showLessButton": "显示更少",
|
||||
"showAllButton": "显示全部",
|
||||
"viewButton": "查看",
|
||||
"deleteTaskDialogTitle": "删除任务",
|
||||
"deleteHabitDialogTitle": "删除习惯",
|
||||
"confirmDeleteDialogMessage": "确定要删除\"{name}\"吗?此操作无法撤消。",
|
||||
"deleteButton": "删除",
|
||||
"overdueTooltip": "逾期"
|
||||
},
|
||||
"HabitContextMenuItems": {
|
||||
"startPomodoro": "开始番茄钟",
|
||||
"moveToToday": "移动到今天",
|
||||
"moveToTomorrow": "移动到明天",
|
||||
"unpin": "取消固定",
|
||||
"pin": "固定",
|
||||
"edit": "编辑",
|
||||
"archive": "归档",
|
||||
"unarchive": "取消归档",
|
||||
"delete": "删除"
|
||||
},
|
||||
"HabitStreak": {
|
||||
"dailyCompletionStreakTitle": "每日完成连胜",
|
||||
"tooltipHabitsLabel": "习惯",
|
||||
"tooltipTasksLabel": "任务",
|
||||
"tooltipCompletedLabel": "已完成"
|
||||
},
|
||||
"CoinBalance": {
|
||||
"coinBalanceTitle": "金币余额"
|
||||
},
|
||||
"AddEditHabitModal": {
|
||||
"editTaskTitle": "编辑任务",
|
||||
"editHabitTitle": "编辑习惯",
|
||||
"addNewTaskTitle": "添加新任务",
|
||||
"addNewHabitTitle": "添加新习惯",
|
||||
"nameLabel": "名称 *",
|
||||
"descriptionLabel": "描述",
|
||||
"whenLabel": "时间 *",
|
||||
"completeLabel": "完成",
|
||||
"timesSuffix": "次",
|
||||
"rewardLabel": "奖励",
|
||||
"coinsSuffix": "金币",
|
||||
"shareLabel": "分享",
|
||||
"saveChangesButton": "保存更改",
|
||||
"addTaskButton": "添加任务",
|
||||
"addHabitButton": "添加习惯"
|
||||
},
|
||||
"ConfirmDialog": {
|
||||
"confirmButton": "确认",
|
||||
"cancelButton": "取消"
|
||||
},
|
||||
"AddEditWishlistItemModal": {
|
||||
"editTitle": "编辑奖励",
|
||||
"addTitle": "添加新奖励",
|
||||
"nameLabel": "名称 *",
|
||||
"descriptionLabel": "描述",
|
||||
"costLabel": "成本",
|
||||
"coinsSuffix": "金币",
|
||||
"redeemableLabel": "可兑换",
|
||||
"timesSuffix": "次",
|
||||
"errorNameRequired": "名称是必填项",
|
||||
"errorCoinCostMin": "金币成本至少为 1",
|
||||
"errorTargetCompletionsMin": "目标完成次数至少为 1",
|
||||
"errorInvalidUrl": "请输入有效的 URL",
|
||||
"linkLabel": "链接",
|
||||
"shareLabel": "分享",
|
||||
"saveButton": "保存更改",
|
||||
"addButton": "添加奖励"
|
||||
},
|
||||
"Navigation": {
|
||||
"dashboard": "仪表板",
|
||||
"tasks": "任务",
|
||||
"habits": "习惯",
|
||||
"calendar": "日历",
|
||||
"wishlist": "愿望清单",
|
||||
"coins": "金币"
|
||||
},
|
||||
"TodayEarnedCoins": {
|
||||
"todaySuffix": "今天"
|
||||
},
|
||||
"WishlistItem": {
|
||||
"usesLeftSingular": "剩余 1 次",
|
||||
"usesLeftPlural": "剩余 {count} 次",
|
||||
"coinsSuffix": "金币",
|
||||
"redeem": "兑换",
|
||||
"redeemedDone": "完成",
|
||||
"redeemedExclamation": "已兑换!",
|
||||
"editButton": "编辑",
|
||||
"archiveButton": "归档",
|
||||
"unarchiveButton": "取消归档",
|
||||
"deleteButton": "删除"
|
||||
},
|
||||
"WishlistManager": {
|
||||
"title": "我的愿望清单",
|
||||
"addRewardButton": "添加奖励",
|
||||
"emptyStateTitle": "您的愿望清单是空的",
|
||||
"emptyStateDescription": "添加您想用金币兑换的奖励",
|
||||
"archivedSectionTitle": "已归档",
|
||||
"popupBlockedTitle": "弹出窗口被阻止",
|
||||
"popupBlockedDescription": "请允许弹出窗口以打开链接",
|
||||
"deleteDialogTitle": "删除奖励",
|
||||
"deleteDialogMessage": "确定要删除此奖励吗?此操作无法撤消。",
|
||||
"deleteButton": "删除"
|
||||
},
|
||||
"UserSelectModal": {
|
||||
"addUserButton": "添加用户",
|
||||
"createNewUserTitle": "创建新用户",
|
||||
"selectUserTitle": "选择用户",
|
||||
"signInSuccessTitle": "登录成功",
|
||||
"signInSuccessDescription": "欢迎回来,{username}!",
|
||||
"errorInvalidPassword": "密码错误"
|
||||
},
|
||||
"CoinsManager": {
|
||||
"title": "金币管理",
|
||||
"currentBalanceLabel": "当前余额",
|
||||
"coinsSuffix": "金币",
|
||||
"addCoinsButton": "添加金币",
|
||||
"removeCoinsButton": "移除金币",
|
||||
"statisticsTitle": "统计",
|
||||
"totalEarnedLabel": "总收入",
|
||||
"totalSpentLabel": "总支出",
|
||||
"totalTransactionsLabel": "总交易数",
|
||||
"todaysEarnedLabel": "今日收入",
|
||||
"todaysSpentLabel": "今日支出",
|
||||
"todaysTransactionsLabel": "今日交易数",
|
||||
"transactionHistoryTitle": "交易历史",
|
||||
"showLabel": "显示:",
|
||||
"entriesSuffix": "条",
|
||||
"showingEntries": "显示 {from} 到 {to} 条,共 {total} 条",
|
||||
"noTransactionsTitle": "尚无交易记录",
|
||||
"noTransactionsDescription": "当您开始赚取或花费金币时,您的交易历史将在此显示",
|
||||
"pageLabel": "第",
|
||||
"ofLabel": "页,共",
|
||||
"transactionTypeHabitCompletion": "习惯完成",
|
||||
"transactionTypeTaskCompletion": "任务完成",
|
||||
"transactionTypeHabitUndo": "习惯撤销",
|
||||
"transactionTypeTaskUndo": "任务撤销",
|
||||
"transactionTypeWishRedemption": "愿望兑换",
|
||||
"transactionTypeManualAdjustment": "手动调整",
|
||||
"transactionTypeCoinReset": "金币重置",
|
||||
"transactionTypeInitialBalance": "初始余额"
|
||||
},
|
||||
"NotificationBell": {
|
||||
"errorUpdateTimestamp": "更新通知阅读时间戳失败:"
|
||||
},
|
||||
"PomodoroTimer": {
|
||||
"focusLabel1": "保持专注",
|
||||
"focusLabel2": "你可以的",
|
||||
"focusLabel3": "继续加油",
|
||||
"focusLabel4": "全力以赴",
|
||||
"focusLabel5": "让它实现",
|
||||
"focusLabel6": "坚持下去",
|
||||
"focusLabel7": "突破自我",
|
||||
"focusLabel8": "一步一个脚印",
|
||||
"focusLabel9": "你能做到",
|
||||
"focusLabel10": "专注并征服",
|
||||
"breakLabel1": "休息一下",
|
||||
"breakLabel2": "放松充电",
|
||||
"breakLabel3": "深呼吸",
|
||||
"breakLabel4": "伸展身体",
|
||||
"breakLabel5": "刷新自己",
|
||||
"breakLabel6": "你值得拥有",
|
||||
"breakLabel7": "补充能量",
|
||||
"breakLabel8": "暂时离开一下",
|
||||
"breakLabel9": "清空思绪",
|
||||
"breakLabel10": "休息并恢复",
|
||||
"focusType": "专注",
|
||||
"breakType": "休息",
|
||||
"pauseButton": "暂停",
|
||||
"startButton": "开始",
|
||||
"resetButton": "重置",
|
||||
"skipButton": "跳过",
|
||||
"wakeLockNotSupported": "浏览器不支持唤醒锁",
|
||||
"wakeLockInUse": "唤醒锁已在使用中",
|
||||
"wakeLockRequestError": "请求唤醒锁时发生错误:",
|
||||
"wakeLockReleaseError": "释放唤醒锁时发生错误:"
|
||||
},
|
||||
"HabitCalendar": {
|
||||
"title": "习惯日历",
|
||||
"calendarCardTitle": "日历",
|
||||
"selectDatePrompt": "选择一个日期",
|
||||
"tasksSectionTitle": "任务",
|
||||
"habitsSectionTitle": "习惯",
|
||||
"errorCompletingPastHabit": "完成过去习惯时出错:"
|
||||
},
|
||||
"NotificationDropdown": {
|
||||
"notLoggedIn": "未登录。",
|
||||
"userCompletedItem": "{username} 完成了 {itemName}。",
|
||||
"userRedeemedItem": "{username} 兑换了 {itemName}。",
|
||||
"activityRelatedToItem": "{username} 对 {itemName} 的相关活动。",
|
||||
"defaultUsername": "某人",
|
||||
"defaultItemName": "一个共享项目",
|
||||
"notificationsTitle": "通知",
|
||||
"notificationsTooltip": "显示其他用户对您与他们共享的习惯或愿望清单的完成或兑换情况(您必须是管理员)",
|
||||
"noNotificationsYet": "尚无通知。"
|
||||
},
|
||||
"AboutModal": {
|
||||
"dialogArisLabel": "关于",
|
||||
"changelogButton": "更新日志",
|
||||
"createdByPrefix": "由 ❤️ 创建",
|
||||
"starOnGitHubButton": "在 GitHub 上点赞"
|
||||
},
|
||||
"PermissionSelector": {
|
||||
"permissionsTitle": "权限",
|
||||
"adminAccessLabel": "管理员权限",
|
||||
"adminAccessDescription": "管理员对所有用户的全部数据拥有完整权限",
|
||||
"resourceHabitTask": "习惯/任务",
|
||||
"resourceWishlist": "愿望清单",
|
||||
"resourceCoins": "金币",
|
||||
"permissionWrite": "写入",
|
||||
"permissionInteract": "交互"
|
||||
},
|
||||
"UserForm": {
|
||||
"toastUserUpdatedTitle": "用户已更新",
|
||||
"toastUserUpdatedDescription": "成功更新用户 {username}",
|
||||
"toastUserCreatedTitle": "用户已创建",
|
||||
"toastUserCreatedDescription": "成功创建用户 {username}",
|
||||
"actionUpdate": "更新",
|
||||
"actionCreate": "创建",
|
||||
"errorFailedUserAction": "用户 {action} 失败",
|
||||
"errorTitle": "错误",
|
||||
"errorFileSizeLimit": "文件大小必须小于 5MB",
|
||||
"toastAvatarUploadedTitle": "头像已上传",
|
||||
"toastAvatarUploadedDescription": "成功上传头像",
|
||||
"errorFailedAvatarUpload": "头像上传失败",
|
||||
"changeAvatarButton": "更改头像",
|
||||
"uploadAvatarButton": "上传头像",
|
||||
"usernameLabel": "用户名",
|
||||
"usernamePlaceholder": "用户名",
|
||||
"newPasswordLabel": "新密码",
|
||||
"passwordLabel": "密码",
|
||||
"passwordPlaceholderEdit": "留空以保持当前密码",
|
||||
"passwordPlaceholderCreate": "输入密码",
|
||||
"demoPasswordDisabledMessage": "在演示实例中密码自动禁用",
|
||||
"disablePasswordLabel": "禁用密码",
|
||||
"cancelButton": "取消",
|
||||
"saveChangesButton": "保存更改",
|
||||
"createUserButton": "创建用户"
|
||||
},
|
||||
"ViewToggle": {
|
||||
"habitsLabel": "习惯",
|
||||
"tasksLabel": "任务"
|
||||
},
|
||||
"HabitItem": {
|
||||
"overdue": "逾期",
|
||||
"whenLabel": "时间:{frequency}",
|
||||
"coinsPerCompletion": "{count} 金币每次完成",
|
||||
"completedStatus": "已完成",
|
||||
"completedStatusCount": "已完成 ({completed}/{target})",
|
||||
"completedStatusCountMobile": "{completed}/{target}",
|
||||
"completeButton": "完成",
|
||||
"completeButtonCount": "完成 ({completed}/{target})",
|
||||
"completeButtonCountMobile": "{completed}/{target}",
|
||||
"undoButton": "撤销",
|
||||
"editButton": "编辑"
|
||||
},
|
||||
"TransactionNoteEditor": {
|
||||
"noteTooLongTitle": "备注太长",
|
||||
"noteTooLongDescription": "备注必须少于200个字符",
|
||||
"errorSavingNoteTitle": "保存备注出错",
|
||||
"errorDeletingNoteTitle": "删除备注出错",
|
||||
"pleaseTryAgainDescription": "请重试",
|
||||
"addNotePlaceholder": "添加备注...",
|
||||
"saveNoteTitle": "保存备注",
|
||||
"cancelButtonTitle": "取消",
|
||||
"deleteNoteTitle": "删除备注",
|
||||
"editNoteAriaLabel": "编辑备注"
|
||||
},
|
||||
"Profile": {
|
||||
"guestUsername": "游客",
|
||||
"editProfileButton": "编辑资料",
|
||||
"signOutSuccessTitle": "登出成功",
|
||||
"signOutSuccessDescription": "您已从您的账户登出",
|
||||
"signOutErrorTitle": "登出错误",
|
||||
"signOutErrorDescription": "登出失败",
|
||||
"switchUserButton": "切换用户",
|
||||
"settingsLink": "设置",
|
||||
"aboutButton": "关于",
|
||||
"themeLabel": "主题",
|
||||
"editProfileModalTitle": "编辑资料"
|
||||
},
|
||||
"PasswordEntryForm": {
|
||||
"notYouButton": "不是您?",
|
||||
"passwordLabel": "密码",
|
||||
"passwordPlaceholder": "输入密码",
|
||||
"loginErrorToastTitle": "错误",
|
||||
"loginFailedErrorToastDescription": "登录失败",
|
||||
"cancelButton": "取消",
|
||||
"loginButton": "登录"
|
||||
},
|
||||
"CompletionCountBadge": {
|
||||
"countCompleted": "{completedCount}/{totalCount} 已完成"
|
||||
},
|
||||
"SettingsPage": {
|
||||
"title": "设置",
|
||||
"uiSettingsTitle": "界面设置",
|
||||
"numberFormattingLabel": "数字格式化",
|
||||
"numberFormattingDescription": "格式化大数字 (例如: 1K, 1M, 1B)",
|
||||
"numberGroupingLabel": "数字分组",
|
||||
"numberGroupingDescription": "使用千位分隔符 (例如: 1,000 vs 1000)",
|
||||
"systemSettingsTitle": "系统设置",
|
||||
"timezoneLabel": "时区",
|
||||
"timezoneDescription": "选择您的时区以获得准确的日期跟踪",
|
||||
"weekStartDayLabel": "周起始日",
|
||||
"weekStartDayDescription": "选择您偏好的每周第一天",
|
||||
"weekdays": {
|
||||
"sunday": "周日",
|
||||
"monday": "周一",
|
||||
"tuesday": "周二",
|
||||
"wednesday": "周三",
|
||||
"thursday": "周四",
|
||||
"friday": "周五",
|
||||
"saturday": "周六"
|
||||
},
|
||||
"autoBackupLabel": "自动备份",
|
||||
"autoBackupTooltip": "启用后,应用程序数据(习惯、金币、设置等)将在每天凌晨2点左右自动备份。备份文件存储在项目根目录的`backups/`目录中,仅保留最近7个备份,旧的备份会被自动删除。",
|
||||
"autoBackupDescription": "每天自动备份数据",
|
||||
"languageLabel": "语言",
|
||||
"languageDescription": "选择应用程序的首选显示语言。",
|
||||
"languageChangedTitle": "语言已更改",
|
||||
"languageChangedDescription": "请刷新页面以查看更改",
|
||||
"languageDisabledInDemoTooltip": "在演示版本中禁用更改语言。"
|
||||
},
|
||||
"Common": {
|
||||
"authenticationRequiredTitle": "需要身份验证",
|
||||
"authenticationRequiredDescription": "请登录以继续。",
|
||||
"permissionDeniedTitle": "权限被拒绝",
|
||||
"permissionDeniedDescription": "您没有对{resource}的{action}权限。",
|
||||
"undoButton": "撤销",
|
||||
"redoButton": "重做",
|
||||
"errorTitle": "错误"
|
||||
},
|
||||
"useHabits": {
|
||||
"alreadyCompletedTitle": "已完成",
|
||||
"alreadyCompletedDescription": "您今天已经完成了这个习惯。",
|
||||
"completedTitle": "已完成!",
|
||||
"earnedCoinsDescription": "您获得了{coinReward}金币。",
|
||||
"progressTitle": "有进展!",
|
||||
"progressDescription": "您今天已完成{count}/{target}次。",
|
||||
"completionUndoneTitle": "完成已撤销",
|
||||
"completionUndoneDescription": "您今天有{count}/{target}次完成。",
|
||||
"noCompletionsToUndoTitle": "没有可撤销的完成",
|
||||
"noCompletionsToUndoDescription": "这个习惯今天还没有完成过。",
|
||||
"alreadyCompletedPastDateTitle": "已完成",
|
||||
"alreadyCompletedPastDateDescription": "这个习惯已于{dateKey}完成。",
|
||||
"earnedCoinsPastDateDescription": "您因{dateKey}获得了{coinReward}金币。",
|
||||
"progressPastDateDescription": "您于{dateKey}完成了{count}/{target}次。"
|
||||
},
|
||||
"useWishlist": {
|
||||
"redemptionLimitReachedTitle": "达到兑换限制",
|
||||
"redemptionLimitReachedDescription": "您已达到\"{itemName}\"的最大兑换次数。",
|
||||
"rewardRedeemedTitle": "🎉 奖励已兑换!",
|
||||
"rewardRedeemedDescription": "您已用{itemCoinCost}金币兑换了\"{itemName}\"。",
|
||||
"notEnoughCoinsTitle": "金币不足",
|
||||
"notEnoughCoinsDescription": "您还需要{coinsNeeded}金币才能兑换此奖励。"
|
||||
},
|
||||
"useCoins": {
|
||||
"invalidAmountTitle": "无效金额",
|
||||
"invalidAmountDescription": "请输入有效的正数",
|
||||
"successTitle": "成功",
|
||||
"addedCoinsDescription": "添加了{amount}金币",
|
||||
"removedCoinsDescription": "移除了{amount}金币",
|
||||
"transactionNotFoundDescription": "未找到交易记录"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { NextConfig } from "next";
|
||||
import createNextIntlPlugin from 'next-intl/plugin';
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
@@ -51,4 +52,5 @@ const nextConfig: NextConfig = {
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
const withNextIntl = createNextIntlPlugin();
|
||||
export default withNextIntl(nextConfig);
|
||||
|
||||
141
package-lock.json
generated
141
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "habittrove",
|
||||
"version": "0.2.8",
|
||||
"version": "0.2.11",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "habittrove",
|
||||
"version": "0.2.8",
|
||||
"version": "0.2.11",
|
||||
"dependencies": {
|
||||
"@emoji-mart/data": "^1.2.1",
|
||||
"@emoji-mart/react": "^1.1.1",
|
||||
@@ -41,6 +41,7 @@
|
||||
"luxon": "^3.5.0",
|
||||
"next": "15.2.3",
|
||||
"next-auth": "^5.0.0-beta.25",
|
||||
"next-intl": "^4.1.0",
|
||||
"next-themes": "^0.4.4",
|
||||
"node-cron": "^3.0.3",
|
||||
"react": "^19.0.0",
|
||||
@@ -306,6 +307,66 @@
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz",
|
||||
"integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig=="
|
||||
},
|
||||
"node_modules/@formatjs/ecma402-abstract": {
|
||||
"version": "2.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.4.tgz",
|
||||
"integrity": "sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@formatjs/fast-memoize": "2.2.7",
|
||||
"@formatjs/intl-localematcher": "0.6.1",
|
||||
"decimal.js": "^10.4.3",
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/ecma402-abstract/node_modules/@formatjs/intl-localematcher": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.1.tgz",
|
||||
"integrity": "sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/fast-memoize": {
|
||||
"version": "2.2.7",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz",
|
||||
"integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/icu-messageformat-parser": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.2.tgz",
|
||||
"integrity": "sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@formatjs/ecma402-abstract": "2.3.4",
|
||||
"@formatjs/icu-skeleton-parser": "1.8.14",
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/icu-skeleton-parser": {
|
||||
"version": "1.8.14",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.14.tgz",
|
||||
"integrity": "sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@formatjs/ecma402-abstract": "2.3.4",
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/intl-localematcher": {
|
||||
"version": "0.5.10",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.10.tgz",
|
||||
"integrity": "sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "2"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||
@@ -2097,6 +2158,12 @@
|
||||
"integrity": "sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@schummar/icu-type-parser": {
|
||||
"version": "1.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz",
|
||||
"integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@swc/counter": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
|
||||
@@ -3999,6 +4066,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decimal.js": {
|
||||
"version": "10.5.0",
|
||||
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz",
|
||||
"integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/decimal.js-light": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
|
||||
@@ -5490,6 +5563,18 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/intl-messageformat": {
|
||||
"version": "10.7.16",
|
||||
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.16.tgz",
|
||||
"integrity": "sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@formatjs/ecma402-abstract": "2.3.4",
|
||||
"@formatjs/fast-memoize": "2.2.7",
|
||||
"@formatjs/icu-messageformat-parser": "2.11.2",
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-alphabetical": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
|
||||
@@ -7068,6 +7153,15 @@
|
||||
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
|
||||
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/neo-async": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
|
||||
@@ -7156,6 +7250,33 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/next-intl": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.1.0.tgz",
|
||||
"integrity": "sha512-JNJRjc7sdnfUxhZmGcvzDszZ60tQKrygV/VLsgzXhnJDxQPn1cN2rVpc53adA1SvBJwPK2O6Sc6b4gYSILjCzw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/amannn"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@formatjs/intl-localematcher": "^0.5.4",
|
||||
"negotiator": "^1.0.0",
|
||||
"use-intl": "^4.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/next-themes": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.4.tgz",
|
||||
@@ -9294,7 +9415,7 @@
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
|
||||
"integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -9468,6 +9589,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/use-intl": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.1.0.tgz",
|
||||
"integrity": "sha512-mQvDYFvoGn+bm/PWvlQOtluKCknsQ5a9F1Cj0hMfBjMBVTwnOqLPd6srhjvVdEQEQFVyHM1PfyifKqKYb11M9Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@formatjs/fast-memoize": "^2.2.0",
|
||||
"@schummar/icu-type-parser": "1.21.5",
|
||||
"intl-messageformat": "^10.5.14"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-sidecar": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "habittrove",
|
||||
"version": "0.2.11",
|
||||
"version": "0.2.12",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
@@ -49,6 +49,7 @@
|
||||
"luxon": "^3.5.0",
|
||||
"next": "15.2.3",
|
||||
"next-auth": "^5.0.0-beta.25",
|
||||
"next-intl": "^4.1.0",
|
||||
"next-themes": "^0.4.4",
|
||||
"node-cron": "^3.0.3",
|
||||
"react": "^19.0.0",
|
||||
|
||||
Reference in New Issue
Block a user