Added i18n support (#129)

This commit is contained in:
Doh
2025-05-18 09:00:48 -04:00
committed by GitHub
parent 95197e216c
commit 91ffe46863
47 changed files with 3603 additions and 455 deletions

View File

@@ -9,6 +9,8 @@ import Layout from '@/components/Layout'
import { Toaster } from '@/components/ui/toaster'
import { ThemeProvider } from "@/components/theme-provider"
import { SessionProvider } from 'next-auth/react'
import { NextIntlClientProvider } from 'next-intl';
import { getLocale, getMessages } from 'next-intl/server';
// Inter (clean, modern, excellent readability)
@@ -37,6 +39,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(),
@@ -48,7 +55,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={{
@@ -79,18 +86,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>

View File

@@ -11,15 +11,19 @@ import {
} from "@/components/ui/tooltip";
import { DynamicTimeNoSSR } from '@/components/DynamicTimeNoSSR';
import { useAtom } from 'jotai';
import { settingsAtom } from '@/lib/atoms';
import { useTranslations } from 'next-intl';
import { settingsAtom, serverSettingsAtom } from '@/lib/atoms';
import { Settings, WeekDay } from '@/lib/types'
import { saveSettings, uploadAvatar } from '../actions/data'
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { Button } from '@/components/ui/button';
import { User, Info } from 'lucide-react'; // Import Info icon
import { toast } from '@/hooks/use-toast'
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)
@@ -32,17 +36,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
@@ -59,9 +63,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
@@ -80,14 +84,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">
@@ -113,9 +117,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">
@@ -138,9 +142,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>
@@ -151,7 +155,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>
@@ -159,18 +163,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
@@ -186,6 +186,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 >