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

@@ -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
}

View File

@@ -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
)

View File

@@ -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
)