From 3d2434e2230246f28315529a49d02b8029fe9df3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 17 Oct 2026 14:30:37 +0000 Subject: [PATCH 1/2] fix(i18n): translate remaining hardcoded screens Complete i18n migration for all remaining hardcoded user-facing strings in root src/ (follow-up to #47, #48-#51, #54). Move strings to translation keys and fill in all 9 locales (de, en, es, fr, it, ja, ko, nl, pt). Screens covered: - ChoresView: all modals, buttons, labels, empty states - Leaderboard: titles, period toggles, chore counts - StreakBadge: tooltip and labels - MusicView: empty state, player selection, playback controls - NowPlayingBar: all aria-labels - GroceryView: list selector, add/delete, item states - OnboardingView: all steps, auth methods, error messages - CardPickerModal: title and empty state - ViewNameModal: labels and buttons - Registry: default entities placeholder Also fixed defaultConfig for ha-entities-list card to use translation key for placeholder instead of hardcoded "Entities" default title. All changes mirrored to beacon/src/. TypeScript types enforce completeness across all locales. German translations vouched for by maintainer; others machine-generated but consistent with #54 terminology (To-dos vs Chores). Signed-off-by: Cursor Agent Co-authored-by: Aaron Sachs --- beacon/src/components/ChoresView.tsx | 53 ++++----- beacon/src/components/GroceryView.tsx | 22 ++-- beacon/src/components/Leaderboard.tsx | 15 ++- beacon/src/components/MusicView.tsx | 24 ++-- beacon/src/components/NowPlayingBar.tsx | 18 +-- beacon/src/components/OnboardingView.tsx | 52 ++++----- beacon/src/components/StreakBadge.tsx | 6 +- .../src/components/cards/CardPickerModal.tsx | 4 +- beacon/src/components/cards/ViewNameModal.tsx | 8 +- beacon/src/components/cards/registry.tsx | 4 +- beacon/src/i18n/translations/de.ts | 104 ++++++++++++++++++ beacon/src/i18n/translations/en.ts | 104 ++++++++++++++++++ beacon/src/i18n/translations/es.ts | 104 ++++++++++++++++++ beacon/src/i18n/translations/fr.ts | 104 ++++++++++++++++++ beacon/src/i18n/translations/it.ts | 104 ++++++++++++++++++ beacon/src/i18n/translations/ja.ts | 104 ++++++++++++++++++ beacon/src/i18n/translations/ko.ts | 104 ++++++++++++++++++ beacon/src/i18n/translations/nl.ts | 104 ++++++++++++++++++ beacon/src/i18n/translations/pt.ts | 104 ++++++++++++++++++ src/components/ChoresView.tsx | 53 ++++----- src/components/GroceryView.tsx | 22 ++-- src/components/Leaderboard.tsx | 15 ++- src/components/MusicView.tsx | 24 ++-- src/components/NowPlayingBar.tsx | 18 +-- src/components/OnboardingView.tsx | 52 ++++----- src/components/StreakBadge.tsx | 6 +- src/components/cards/CardPickerModal.tsx | 4 +- src/components/cards/ViewNameModal.tsx | 8 +- src/components/cards/registry.tsx | 4 +- src/i18n/translations/de.ts | 104 ++++++++++++++++++ src/i18n/translations/en.ts | 104 ++++++++++++++++++ src/i18n/translations/es.ts | 104 ++++++++++++++++++ src/i18n/translations/fr.ts | 104 ++++++++++++++++++ src/i18n/translations/it.ts | 104 ++++++++++++++++++ src/i18n/translations/ja.ts | 104 ++++++++++++++++++ src/i18n/translations/ko.ts | 104 ++++++++++++++++++ src/i18n/translations/nl.ts | 104 ++++++++++++++++++ src/i18n/translations/pt.ts | 104 ++++++++++++++++++ 38 files changed, 2098 insertions(+), 186 deletions(-) diff --git a/beacon/src/components/ChoresView.tsx b/beacon/src/components/ChoresView.tsx index 3485c92b..c1d4218d 100644 --- a/beacon/src/components/ChoresView.tsx +++ b/beacon/src/components/ChoresView.tsx @@ -6,6 +6,7 @@ import { StreakBadge } from './StreakBadge'; import { useChores } from '../hooks/useChores'; import { useFamily } from '../hooks/useFamily'; import { useSettings } from '../hooks/useSettings'; +import { useTranslation } from '../i18n'; const CHORE_ICONS = ['🧹', '🍽️', '🐕', '🛏️', '📚', '🗑️', '👕', '🧺', '🪥', '🚿', '🧼', '💪']; @@ -36,6 +37,7 @@ function UnassignedChoreCard({ onDelete, currencySymbol = '$', }: UnassignedChoreCardProps) { + const { t } = useTranslation(); const [showMemberPicker, setShowMemberPicker] = useState(false); const [pickerAction, setPickerAction] = useState<'claim' | 'complete'>('claim'); const [confirmDelete, setConfirmDelete] = useState(false); @@ -80,7 +82,7 @@ function UnassignedChoreCard({ setShowMemberPicker(true); }} > - Claim + {t('chores.claim')} @@ -104,10 +106,10 @@ function UnassignedChoreCard({ type="button" className="chore-card-action-btn chore-card-action-btn--danger" onClick={handleDeleteClick} - aria-label={`Delete ${chore.name}`} + aria-label={t('chores.deleteChoreAria', { name: chore.name })} > {confirmDelete ? ( - Sure? + {t('common.sure')} ) : ( )} @@ -119,7 +121,7 @@ function UnassignedChoreCard({
e.stopPropagation()}>

- {pickerAction === 'claim' ? 'Who claims this?' : 'Who completed this?'} + {pickerAction === 'claim' ? t('chores.whoClaims') : t('chores.whoCompleted')}

)} {members.length === 0 ? (
- Add family members first to assign chores. + {t('chores.addMembersFirst')}
) : ( <> {unassignedChores.length > 0 && (
-

Open Chores

+

{t('chores.openChores')}

{unassignedChores.map((chore) => ( {memberChores.length === 0 ? ( -
No chores assigned
+
{t('chores.noChoresAssigned')}
) : (
{memberChores.map((chore) => ( @@ -368,7 +371,7 @@ export function ChoresView() { onClick={() => openAddForm(member.id)} > - Add chore for {member.name} + {t('chores.addChoreFor', { name: member.name })}
))} @@ -380,7 +383,7 @@ export function ChoresView() {
e.stopPropagation()}>
-

{isEditing ? 'Edit Chore' : 'New Chore'}

+

{isEditing ? t('chores.editChore') : t('chores.newChore')}

@@ -389,7 +392,7 @@ export function ChoresView() {
{/* Icon picker */}
- +
{CHORE_ICONS.map((icon) => ( )}
diff --git a/beacon/src/components/GroceryView.tsx b/beacon/src/components/GroceryView.tsx index 3e6c9f39..7f43b646 100644 --- a/beacon/src/components/GroceryView.tsx +++ b/beacon/src/components/GroceryView.tsx @@ -4,6 +4,7 @@ import { GroceryList } from '../types/grocery'; import { AnyListClient } from '../api/anylist'; import { callHaService, hasToken } from '../api/ha-rest'; import { useLocalTasks } from '../hooks/useLocalTasks'; +import { useTranslation } from '../i18n'; interface TodoItem { uid: string; @@ -40,6 +41,7 @@ interface GroceryViewProps { } export function GroceryView({ defaultListId, mode = 'grocery', groceryListIds = [] }: GroceryViewProps) { + const { t } = useTranslation(); const [haLists, setHaLists] = useState([]); const [selectedListId, setSelectedListId] = useState(defaultListId || ''); const [haItems, setHaItems] = useState([]); @@ -249,11 +251,11 @@ export function GroceryView({ defaultListId, mode = 'grocery', groceryListIds = onChange={(e) => setSelectedListId(e.target.value)} > {allLists.length === 0 && ( - + )} {allLists.map(list => ( ))} @@ -267,7 +269,7 @@ export function GroceryView({ defaultListId, mode = 'grocery', groceryListIds = ref={inputRef} type="text" className="grocery-view-input" - placeholder="Add an item..." + placeholder={t('grocery.addItemPlaceholder')} value={inputValue} onChange={(e) => setInputValue(e.target.value)} disabled={submitting || !selectedListId} @@ -276,7 +278,7 @@ export function GroceryView({ defaultListId, mode = 'grocery', groceryListIds = type="submit" className="grocery-view-add-btn" disabled={!inputValue.trim() || submitting} - aria-label="Add item" + aria-label={t('grocery.addItem')} > @@ -285,7 +287,7 @@ export function GroceryView({ defaultListId, mode = 'grocery', groceryListIds = {/* Items list */}
{loading && items.length === 0 ? ( -
Loading items...
+
{t('grocery.loadingItems')}
) : items.length === 0 && !loading ? (
@@ -296,7 +298,7 @@ export function GroceryView({ defaultListId, mode = 'grocery', groceryListIds =
-

No items yet — add one above

+

{t('grocery.empty')}

) : ( <> @@ -304,7 +306,7 @@ export function GroceryView({ defaultListId, mode = 'grocery', groceryListIds = {uncheckedItems.length > 0 && (
- Items ({uncheckedItems.length}) + {t('grocery.items', { count: uncheckedItems.length })}
{uncheckedItems.map(item => (
@@ -321,7 +323,7 @@ export function GroceryView({ defaultListId, mode = 'grocery', groceryListIds = type="button" className="grocery-view-delete" onClick={() => handleDelete(item)} - aria-label="Delete item" + aria-label={t('grocery.deleteItem')} > @@ -340,7 +342,7 @@ export function GroceryView({ defaultListId, mode = 'grocery', groceryListIds = onClick={() => setCompletedCollapsed(prev => !prev)} > {completedCollapsed ? : } - Completed ({checkedItems.length}) + {t('grocery.completed', { count: checkedItems.length })} {!completedCollapsed && checkedItems.map(item => (
@@ -367,7 +369,7 @@ export function GroceryView({ defaultListId, mode = 'grocery', groceryListIds = type="button" className="grocery-view-delete" onClick={() => handleDelete(item)} - aria-label="Delete item" + aria-label={t('grocery.deleteItem')} > diff --git a/beacon/src/components/Leaderboard.tsx b/beacon/src/components/Leaderboard.tsx index b3472bb7..b0c27c43 100644 --- a/beacon/src/components/Leaderboard.tsx +++ b/beacon/src/components/Leaderboard.tsx @@ -5,6 +5,7 @@ import { useFamily } from '../hooks/useFamily'; import { useSettings } from '../hooks/useSettings'; import { MemberEarnings, formatChoreValue } from '../types/family'; import { localDayKey } from '../api/date-keys'; +import { useTranslation } from '../i18n'; interface LeaderboardProps { open: boolean; @@ -34,6 +35,7 @@ function getMonthRange(): [string, string] { const RANK_DECORATIONS = ['\u{1F947}', '\u{1F948}', '\u{1F949}']; export function Leaderboard({ open, onClose }: LeaderboardProps) { + const { t } = useTranslation(); const { members } = useFamily(); const { settings } = useSettings(); const { getEarningsForPeriod, getStreakForMember } = useChores(); @@ -61,7 +63,7 @@ export function Leaderboard({ open, onClose }: LeaderboardProps) { return (
-

Leaderboard

+

{t('leaderboard.title')}

@@ -74,20 +76,20 @@ export function Leaderboard({ open, onClose }: LeaderboardProps) { className={`lb-period-btn ${period === 'week' ? 'lb-period-btn--active' : ''}`} onClick={() => setPeriod('week')} > - This Week + {t('leaderboard.thisWeek')}
{ranked.length === 0 && ( -
Add family members and chores to see the leaderboard.
+
{t('leaderboard.empty')}
)}
@@ -109,7 +111,10 @@ export function Leaderboard({ open, onClose }: LeaderboardProps) {
{entry.member.name} - {entry.earnings.chore_count} chore{entry.earnings.chore_count !== 1 ? 's' : ''} + {entry.earnings.chore_count === 1 + ? t('leaderboard.choreCompletedSingular', { count: entry.earnings.chore_count }) + : t('leaderboard.choresCompleted', { count: entry.earnings.chore_count }) + }
diff --git a/beacon/src/components/MusicView.tsx b/beacon/src/components/MusicView.tsx index ad4ac6e1..c73c1683 100644 --- a/beacon/src/components/MusicView.tsx +++ b/beacon/src/components/MusicView.tsx @@ -10,6 +10,7 @@ import { ChevronDown, } from 'lucide-react'; import { MediaPlayer } from '../types/music'; +import { useTranslation } from '../i18n'; interface MusicViewProps { players: MediaPlayer[]; @@ -44,14 +45,15 @@ export function MusicView({ onPrevious, onSetVolume, }: MusicViewProps) { + const { t } = useTranslation(); // Empty state when no media players found if (players.length === 0) { return (
🎵
-

No Media Players

-

Connect a media player in Home Assistant to control music from here.

+

{t('music.noPlayers')}

+

{t('music.noPlayersDesc')}

); @@ -104,7 +106,7 @@ export function MusicView({ className="music-player-selector-btn" onClick={() => setShowPlayerDropdown(!showPlayerDropdown)} > - {player?.friendly_name || 'Select Player'} + {player?.friendly_name || t('music.selectPlayer')} {showPlayerDropdown && ( @@ -123,7 +125,7 @@ export function MusicView({ > {p.friendly_name} {p.state === 'playing' && ( - Playing + {t('music.nowPlaying')} )} ))} @@ -138,7 +140,7 @@ export function MusicView({ {player?.media_album_name ) : (
@@ -150,7 +152,7 @@ export function MusicView({ {/* Track info */}

- {player?.media_title || player?.app_name || 'Nothing Playing'} + {player?.media_title || player?.app_name || t('music.nowPlaying')}

{player?.media_artist || ''} @@ -180,7 +182,7 @@ export function MusicView({ type="button" className="music-control-btn" onClick={() => onPrevious(player?.entity_id)} - aria-label="Previous track" + aria-label={t('music.previousTrack')} > @@ -188,7 +190,7 @@ export function MusicView({ type="button" className="music-control-btn music-control-btn--play" onClick={() => (isPlaying ? onPause : onPlay)(player?.entity_id)} - aria-label={isPlaying ? 'Pause' : 'Play'} + aria-label={isPlaying ? t('music.pause') : t('music.play')} > {isPlaying ? : } @@ -196,7 +198,7 @@ export function MusicView({ type="button" className="music-control-btn" onClick={() => onNext(player?.entity_id)} - aria-label="Next track" + aria-label={t('music.nextTrack')} > @@ -213,7 +215,7 @@ export function MusicView({ player?.entity_id, ) } - aria-label={player?.is_volume_muted ? 'Unmute' : 'Mute'} + aria-label={player?.is_volume_muted ? t('music.unmute') : t('music.mute')} > {player?.is_volume_muted ? : } @@ -225,7 +227,7 @@ export function MusicView({ step={0.02} value={player?.is_volume_muted ? 0 : (player?.volume_level ?? 0.5)} onChange={(e) => onSetVolume(parseFloat(e.target.value), player?.entity_id)} - aria-label="Volume" + aria-label={t('music.volume')} />

diff --git a/beacon/src/components/NowPlayingBar.tsx b/beacon/src/components/NowPlayingBar.tsx index b5bf512a..ef2e2b3a 100644 --- a/beacon/src/components/NowPlayingBar.tsx +++ b/beacon/src/components/NowPlayingBar.tsx @@ -9,6 +9,7 @@ import { } from 'lucide-react'; import { MediaPlayer } from '../types/music'; import { getConfig } from '../config'; +import { useTranslation } from '../i18n'; interface NowPlayingBarProps { player: MediaPlayer | null; @@ -29,6 +30,7 @@ export function NowPlayingBar({ onSetVolume, onExpand, }: NowPlayingBarProps) { + const { t } = useTranslation(); // Local progress tracking const [position, setPosition] = useState(player?.media_position ?? 0); const animRef = useRef | null>(null); @@ -59,7 +61,7 @@ export function NowPlayingBar({ : null; return ( -
+
{/* Thin progress bar across the top */} {duration > 0 && (
@@ -71,13 +73,13 @@ export function NowPlayingBar({ type="button" className="now-playing-info" onClick={onExpand} - aria-label="Open music view" + aria-label={t('music.openMusicView')} > {artSrc ? ( {player.media_album_name ) : (
@@ -98,7 +100,7 @@ export function NowPlayingBar({ type="button" className="now-playing-btn" onClick={onPrevious} - aria-label="Previous track" + aria-label={t('music.previousTrack')} > @@ -106,7 +108,7 @@ export function NowPlayingBar({ type="button" className="now-playing-btn now-playing-btn--play" onClick={isPlaying ? onPause : onPlay} - aria-label={isPlaying ? 'Pause' : 'Play'} + aria-label={isPlaying ? t('music.pause') : t('music.play')} > {isPlaying ? : } @@ -114,7 +116,7 @@ export function NowPlayingBar({ type="button" className="now-playing-btn" onClick={onNext} - aria-label="Next track" + aria-label={t('music.nextTrack')} > @@ -126,7 +128,7 @@ export function NowPlayingBar({ type="button" className="now-playing-btn" onClick={() => onSetVolume(player.is_volume_muted ? (player.volume_level || 0.5) : 0)} - aria-label={player.is_volume_muted ? 'Unmute' : 'Mute'} + aria-label={player.is_volume_muted ? t('music.unmute') : t('music.mute')} > {player.is_volume_muted ? : } @@ -138,7 +140,7 @@ export function NowPlayingBar({ step={0.02} value={player.is_volume_muted ? 0 : (player.volume_level ?? 0.5)} onChange={(e) => onSetVolume(parseFloat(e.target.value))} - aria-label="Volume" + aria-label={t('music.volume')} />
diff --git a/beacon/src/components/OnboardingView.tsx b/beacon/src/components/OnboardingView.tsx index a944a630..711f79c3 100644 --- a/beacon/src/components/OnboardingView.tsx +++ b/beacon/src/components/OnboardingView.tsx @@ -1,6 +1,7 @@ import { useState, useCallback } from 'react'; import { ArrowRight, Check, ExternalLink, Key, LogIn } from 'lucide-react'; import beaconLogo from '../assets/beacon-app-icon.svg'; +import { useTranslation } from '../i18n'; // --------------------------------------------------------------------------- // Types @@ -197,6 +198,7 @@ const styles = { // --------------------------------------------------------------------------- export default function OnboardingView({ onComplete, onOAuthStart }: OnboardingViewProps) { + const { t } = useTranslation(); const [step, setStep] = useState(1); const [haUrl, setHaUrl] = useState(''); const [urlError, setUrlError] = useState(''); @@ -216,7 +218,7 @@ export default function OnboardingView({ onComplete, onOAuthStart }: OnboardingV const testConnection = useCallback(async () => { const trimmed = haUrl.trim().replace(/\/+$/, ''); if (!isValidUrl(trimmed)) { - setUrlError('URL must start with http:// or https://'); + setUrlError(t('onboarding.urlError')); return; } setUrlError(''); @@ -228,24 +230,24 @@ export default function OnboardingView({ onComplete, onOAuthStart }: OnboardingV setHaUrl(trimmed); setStep(3); } else { - setUrlError(`Connection failed (HTTP ${response.status}). Check the URL and try again.`); + setUrlError(t('onboarding.connectionFailed', { status: response.status })); } } catch { - setUrlError('Could not reach Home Assistant. Verify the URL and ensure it is accessible.'); + setUrlError(t('onboarding.notReachable')); } finally { setConnecting(false); } - }, [haUrl, isValidUrl]); + }, [haUrl, isValidUrl, t]); // ---- Complete setup ---- const handleComplete = useCallback(() => { if (!token.trim()) { - setTokenError('Please enter your access token.'); + setTokenError(t('onboarding.tokenRequired')); return; } setTokenError(''); onComplete(haUrl, token.trim()); - }, [haUrl, token, onComplete]); + }, [haUrl, token, onComplete, t]); // ---- Render steps ---- @@ -263,13 +265,13 @@ export default function OnboardingView({ onComplete, onOAuthStart }: OnboardingV const renderStep1 = () => (
Beacon -

Welcome to Beacon

-

Connect your Home Assistant to get started.

+

{t('onboarding.welcome')}

+

{t('onboarding.welcomeDesc')}

@@ -277,16 +279,16 @@ export default function OnboardingView({ onComplete, onOAuthStart }: OnboardingV const renderStep2 = () => (
-

Connect to Home Assistant

+

{t('onboarding.connectTitle')}

- Enter the URL of your Home Assistant instance. + {t('onboarding.connectDesc')}

- + { setHaUrl(e.target.value); @@ -311,7 +313,7 @@ export default function OnboardingView({ onComplete, onOAuthStart }: OnboardingV disabled={connecting || !haUrl.trim()} onClick={testConnection} > - {connecting ? 'Connecting...' : 'Connect'} + {connecting ? t('onboarding.connecting') : t('onboarding.connect')} {!connecting && }
@@ -319,9 +321,9 @@ export default function OnboardingView({ onComplete, onOAuthStart }: OnboardingV const renderStep3 = () => (
-

Authentication

+

{t('onboarding.authTitle')}

- Choose how to authenticate with Home Assistant. + {t('onboarding.authDesc')}

@@ -332,17 +334,17 @@ export default function OnboardingView({ onComplete, onOAuthStart }: OnboardingV >

- Long-lived Access Token + {t('onboarding.tokenMethod')}

- Use a token generated from your Home Assistant profile. + {t('onboarding.tokenMethodDesc')}

{authMethod === 'token' && (
{ setToken(e.target.value); @@ -356,9 +358,9 @@ export default function OnboardingView({ onComplete, onOAuthStart }: OnboardingV /> {tokenError &&

{tokenError}

}
- How to create a token: + {t('onboarding.tokenHint')}
- Go to your HA Profile → Long-lived tokens → Create Token + {t('onboarding.tokenHintDetail')}
)} @@ -371,10 +373,10 @@ export default function OnboardingView({ onComplete, onOAuthStart }: OnboardingV >

- Sign in with Home Assistant + {t('onboarding.oauthMethod')}

- Authenticate using your Home Assistant credentials via OAuth2. + {t('onboarding.oauthMethodDesc')}

{authMethod === 'oauth' && ( @@ -386,7 +388,7 @@ export default function OnboardingView({ onComplete, onOAuthStart }: OnboardingV onOAuthStart?.(haUrl); }} > - Open Home Assistant + {t('onboarding.openHA')}
@@ -403,7 +405,7 @@ export default function OnboardingView({ onComplete, onOAuthStart }: OnboardingV disabled={!token.trim()} onClick={handleComplete} > - Complete Setup + {t('onboarding.completeSetup')} )} diff --git a/beacon/src/components/StreakBadge.tsx b/beacon/src/components/StreakBadge.tsx index 1010ecd0..ac66de92 100644 --- a/beacon/src/components/StreakBadge.tsx +++ b/beacon/src/components/StreakBadge.tsx @@ -1,5 +1,6 @@ import { useState } from 'react'; import { Streak } from '../types/family'; +import { useTranslation } from '../i18n'; interface StreakBadgeProps { streak: Streak; @@ -7,6 +8,7 @@ interface StreakBadgeProps { } export function StreakBadge({ streak, size = 'md' }: StreakBadgeProps) { + const { t } = useTranslation(); const [showLongest, setShowLongest] = useState(false); if (streak.current === 0 && streak.longest === 0) return null; @@ -20,13 +22,13 @@ export function StreakBadge({ streak, size = 'md' }: StreakBadgeProps) { type="button" className={`streak-badge ${sizeClass} ${hotClass}`} onClick={() => setShowLongest((v) => !v)} - title={`Longest streak: ${streak.longest} days`} + title={t('streak.longestTooltip', { count: streak.longest })} > 🔥 {showLongest ? ( <> - Best: {streak.longest} + {t('streak.best')} {streak.longest} ) : ( streak.current diff --git a/beacon/src/components/cards/CardPickerModal.tsx b/beacon/src/components/cards/CardPickerModal.tsx index e91abab8..e3882d77 100644 --- a/beacon/src/components/cards/CardPickerModal.tsx +++ b/beacon/src/components/cards/CardPickerModal.tsx @@ -17,7 +17,7 @@ export function CardPickerModal({ region, onPick, onClose }: CardPickerModalProp
e.stopPropagation()}>
-

Add Card

+

{t('cardPicker.title')}

@@ -34,7 +34,7 @@ export function CardPickerModal({ region, onPick, onClose }: CardPickerModalProp ))} {options.length === 0 && ( -
No card types available for this area.
+
{t('cardPicker.empty')}
)}
diff --git a/beacon/src/components/cards/ViewNameModal.tsx b/beacon/src/components/cards/ViewNameModal.tsx index e2566474..41f1637a 100644 --- a/beacon/src/components/cards/ViewNameModal.tsx +++ b/beacon/src/components/cards/ViewNameModal.tsx @@ -1,4 +1,5 @@ import { useState } from 'react'; +import { useTranslation } from '../../i18n'; interface ViewNameModalProps { title: string; @@ -9,6 +10,7 @@ interface ViewNameModalProps { /** Small text-input modal used for adding/renaming a dashboard view (tab). */ export function ViewNameModal({ title, initialValue, onSave, onClose }: ViewNameModalProps) { + const { t } = useTranslation(); const [name, setName] = useState(initialValue); const handleSave = () => { @@ -25,7 +27,7 @@ export function ViewNameModal({ title, initialValue, onSave, onClose }: ViewName
- +
- - + +
diff --git a/beacon/src/components/cards/registry.tsx b/beacon/src/components/cards/registry.tsx index 1d7e8a16..816f054b 100644 --- a/beacon/src/components/cards/registry.tsx +++ b/beacon/src/components/cards/registry.tsx @@ -104,10 +104,10 @@ export const cardRegistry: Record = { displayNameKey: 'cards.haEntitiesList.displayName', icon: '📶', component: HaEntitiesListCard, - defaultConfig: { title: 'Entities', subtitle: '', entity_ids: [] }, + defaultConfig: { title: '', subtitle: '', entity_ids: [] }, defaultSize: 'sm', configFields: [ - { type: 'text', key: 'title', labelKey: 'cards.common.titleLabel' }, + { type: 'text', key: 'title', labelKey: 'cards.common.titleLabel', placeholderKey: 'cardConfig.entitiesPlaceholder' }, { type: 'text', key: 'subtitle', labelKey: 'cards.common.subtitleLabel' }, { type: 'entity-list', key: 'entity_ids', labelKey: 'cards.common.entitiesLabel' }, ], diff --git a/beacon/src/i18n/translations/de.ts b/beacon/src/i18n/translations/de.ts index 184ecd0e..0c7061f7 100644 --- a/beacon/src/i18n/translations/de.ts +++ b/beacon/src/i18n/translations/de.ts @@ -431,4 +431,108 @@ export const de: Record = { 'photoFrame.pauseSlideshow': 'Diashow pausieren', 'photoFrame.resumeSlideshow': 'Diashow fortsetzen', 'photoFrame.nextPhoto': 'Nächstes Foto', + + // ---- chores view ---- + 'chores.title': 'Aufgaben', + 'chores.addChore': 'Aufgabe hinzufügen', + 'chores.editChore': 'Aufgabe bearbeiten', + 'chores.newChore': 'Neue Aufgabe', + 'chores.claim': 'Übernehmen', + 'chores.complete': 'Erledigen', + 'chores.whoClaims': 'Wer übernimmt dies?', + 'chores.whoCompleted': 'Wer hat das erledigt?', + 'chores.editChoreAria': '{name} bearbeiten', + 'chores.deleteChoreAria': '{name} löschen', + 'chores.name': 'Name', + 'chores.value': 'Wert', + 'chores.frequency': 'Häufigkeit', + 'chores.assignedTo': 'Zugewiesen an', + 'chores.assignTo': 'Zuweisen an', + 'chores.icon': 'Symbol', + 'chores.daily': 'Täglich', + 'chores.weekly': 'Wöchentlich', + 'chores.monthly': 'Monatlich', + 'chores.oneTime': 'Einmalig', + 'chores.namePlaceholder': 'z.B. Wohnzimmer saugen', + 'chores.anyone': 'Jeder', + 'chores.empty': 'Noch keine Aufgaben — füge eine hinzu', + 'chores.addMembersFirst': 'Füge erst Familienmitglieder hinzu, um Aufgaben zuzuweisen.', + 'chores.openChores': 'Offene Aufgaben', + 'chores.noChoresAssigned': 'Keine Aufgaben zugewiesen', + 'chores.addChoreFor': 'Aufgabe für {name} hinzufügen', + 'chores.saveChanges': 'Änderungen speichern', + + // ---- leaderboard ---- + 'leaderboard.title': 'Bestenliste', + 'leaderboard.thisWeek': 'Diese Woche', + 'leaderboard.thisMonth': 'Dieser Monat', + 'leaderboard.empty': 'Füge Familienmitglieder und Aufgaben hinzu, um die Bestenliste zu sehen.', + 'leaderboard.choresCompleted': '{count} Aufgaben', + 'leaderboard.choreCompletedSingular': '{count} Aufgabe', + + // ---- streak badge ---- + 'streak.longestTooltip': 'Längste Serie: {count} Tage', + 'streak.best': 'Beste:', + + // ---- music view ---- + 'music.noPlayers': 'Keine Mediaplayer', + 'music.noPlayersDesc': 'Verbinde einen Mediaplayer in Home Assistant, um hier Musik zu steuern.', + 'music.selectPlayer': 'Player auswählen', + 'music.nowPlaying': 'Läuft gerade', + 'music.volume': 'Lautstärke', + 'music.openMusicView': 'Musikansicht öffnen', + 'music.previousTrack': 'Vorheriger Titel', + 'music.nextTrack': 'Nächster Titel', + 'music.play': 'Abspielen', + 'music.pause': 'Pausieren', + 'music.mute': 'Stumm', + 'music.unmute': 'Laut', + 'music.albumArt': 'Albumcover', + + // ---- grocery / lists view ---- + 'grocery.noListsAvailable': 'Keine Listen verfügbar', + 'grocery.addItem': 'Artikel hinzufügen', + 'grocery.addItemPlaceholder': 'Artikel hinzufügen...', + 'grocery.loadingItems': 'Artikel werden geladen...', + 'grocery.empty': 'Noch keine Artikel — füge oben einen hinzu', + 'grocery.items': 'Artikel ({count})', + 'grocery.completed': 'Erledigt ({count})', + 'grocery.deleteItem': 'Artikel löschen', + 'grocery.haList': '{name} (HA)', + + // ---- card picker modal ---- + 'cardPicker.title': 'Karte hinzufügen', + 'cardPicker.empty': 'Keine Kartentypen für diesen Bereich verfügbar.', + + // ---- card config modal (common) ---- + 'cardConfig.entitiesPlaceholder': 'Entitäten', + + // ---- view name modal ---- + 'viewName.name': 'Name', + + // ---- onboarding ---- + 'onboarding.welcome': 'Willkommen bei Beacon', + 'onboarding.welcomeDesc': 'Verbinde dein Home Assistant, um loszulegen.', + 'onboarding.getStarted': 'Los geht\'s', + 'onboarding.connectTitle': 'Mit Home Assistant verbinden', + 'onboarding.connectDesc': 'Gib die URL deiner Home Assistant-Instanz ein.', + 'onboarding.haUrl': 'Home Assistant URL', + 'onboarding.haUrlPlaceholder': 'https://homeassistant.local:8123', + 'onboarding.connect': 'Verbinden', + 'onboarding.connecting': 'Verbindung wird hergestellt...', + 'onboarding.authTitle': 'Authentifizierung', + 'onboarding.authDesc': 'Wähle, wie du dich bei Home Assistant authentifizieren möchtest.', + 'onboarding.tokenMethod': 'Langlebiges Zugriffstoken', + 'onboarding.tokenMethodDesc': 'Verwende ein Token aus deinem Home Assistant-Profil.', + 'onboarding.tokenPlaceholder': 'Token hier einfügen', + 'onboarding.tokenHint': 'So erstellst du ein Token:', + 'onboarding.tokenHintDetail': 'Gehe zu deinem HA-Profil → Langlebige Zugriffstoken → Token erstellen', + 'onboarding.oauthMethod': 'Mit Home Assistant anmelden', + 'onboarding.oauthMethodDesc': 'Authentifiziere dich mit deinen Home Assistant-Zugangsdaten via OAuth2.', + 'onboarding.openHA': 'Home Assistant öffnen', + 'onboarding.completeSetup': 'Einrichtung abschließen', + 'onboarding.urlError': 'URL muss mit http:// oder https:// beginnen', + 'onboarding.connectionFailed': 'Verbindung fehlgeschlagen (HTTP {status}). Überprüfe die URL und versuche es erneut.', + 'onboarding.notReachable': 'Home Assistant konnte nicht erreicht werden. Überprüfe die URL und stelle sicher, dass sie erreichbar ist.', + 'onboarding.tokenRequired': 'Bitte gib dein Zugriffstoken ein.', }; diff --git a/beacon/src/i18n/translations/en.ts b/beacon/src/i18n/translations/en.ts index 19e92bc3..d7b8889d 100644 --- a/beacon/src/i18n/translations/en.ts +++ b/beacon/src/i18n/translations/en.ts @@ -445,6 +445,110 @@ export const en = { 'photoFrame.pauseSlideshow': 'Pause slideshow', 'photoFrame.resumeSlideshow': 'Resume slideshow', 'photoFrame.nextPhoto': 'Next photo', + + // ---- chores view ---- + 'chores.title': 'Chores', + 'chores.addChore': 'Add Chore', + 'chores.editChore': 'Edit Chore', + 'chores.newChore': 'New Chore', + 'chores.claim': 'Claim', + 'chores.complete': 'Complete', + 'chores.whoClaims': 'Who claims this?', + 'chores.whoCompleted': 'Who completed this?', + 'chores.editChoreAria': 'Edit {name}', + 'chores.deleteChoreAria': 'Delete {name}', + 'chores.name': 'Name', + 'chores.value': 'Value', + 'chores.frequency': 'Frequency', + 'chores.assignedTo': 'Assign To', + 'chores.assignTo': 'Assign To', + 'chores.icon': 'Icon', + 'chores.daily': 'Daily', + 'chores.weekly': 'Weekly', + 'chores.monthly': 'Monthly', + 'chores.oneTime': 'One-time', + 'chores.namePlaceholder': 'e.g., Vacuum living room', + 'chores.anyone': 'Anyone', + 'chores.empty': 'No chores yet — add one to get started', + 'chores.addMembersFirst': 'Add family members first to assign chores.', + 'chores.openChores': 'Open Chores', + 'chores.noChoresAssigned': 'No chores assigned', + 'chores.addChoreFor': 'Add chore for {name}', + 'chores.saveChanges': 'Save Changes', + + // ---- leaderboard ---- + 'leaderboard.title': 'Leaderboard', + 'leaderboard.thisWeek': 'This Week', + 'leaderboard.thisMonth': 'This Month', + 'leaderboard.empty': 'Add family members and chores to see the leaderboard.', + 'leaderboard.choresCompleted': '{count} chores', + 'leaderboard.choreCompletedSingular': '{count} chore', + + // ---- streak badge ---- + 'streak.longestTooltip': 'Longest streak: {count} days', + 'streak.best': 'Best:', + + // ---- music view ---- + 'music.noPlayers': 'No Media Players', + 'music.noPlayersDesc': 'Connect a media player in Home Assistant to control music from here.', + 'music.selectPlayer': 'Select Player', + 'music.nowPlaying': 'Now Playing', + 'music.volume': 'Volume', + 'music.openMusicView': 'Open music view', + 'music.previousTrack': 'Previous track', + 'music.nextTrack': 'Next track', + 'music.play': 'Play', + 'music.pause': 'Pause', + 'music.mute': 'Mute', + 'music.unmute': 'Unmute', + 'music.albumArt': 'Album art', + + // ---- grocery / lists view ---- + 'grocery.noListsAvailable': 'No lists available', + 'grocery.addItem': 'Add item', + 'grocery.addItemPlaceholder': 'Add an item...', + 'grocery.loadingItems': 'Loading items...', + 'grocery.empty': 'No items yet — add one above', + 'grocery.items': 'Items ({count})', + 'grocery.completed': 'Completed ({count})', + 'grocery.deleteItem': 'Delete item', + 'grocery.haList': '{name} (HA)', + + // ---- card picker modal ---- + 'cardPicker.title': 'Add Card', + 'cardPicker.empty': 'No card types available for this area.', + + // ---- card config modal (common) ---- + 'cardConfig.entitiesPlaceholder': 'Entities', + + // ---- view name modal ---- + 'viewName.name': 'Name', + + // ---- onboarding ---- + 'onboarding.welcome': 'Welcome to Beacon', + 'onboarding.welcomeDesc': 'Connect your Home Assistant to get started.', + 'onboarding.getStarted': 'Get Started', + 'onboarding.connectTitle': 'Connect to Home Assistant', + 'onboarding.connectDesc': 'Enter the URL of your Home Assistant instance.', + 'onboarding.haUrl': 'Home Assistant URL', + 'onboarding.haUrlPlaceholder': 'https://homeassistant.local:8123', + 'onboarding.connect': 'Connect', + 'onboarding.connecting': 'Connecting...', + 'onboarding.authTitle': 'Authentication', + 'onboarding.authDesc': 'Choose how to authenticate with Home Assistant.', + 'onboarding.tokenMethod': 'Long-lived Access Token', + 'onboarding.tokenMethodDesc': 'Use a token generated from your Home Assistant profile.', + 'onboarding.tokenPlaceholder': 'Paste your token here', + 'onboarding.tokenHint': 'How to create a token:', + 'onboarding.tokenHintDetail': 'Go to your HA Profile → Long-lived tokens → Create Token', + 'onboarding.oauthMethod': 'Sign in with Home Assistant', + 'onboarding.oauthMethodDesc': 'Authenticate using your Home Assistant credentials via OAuth2.', + 'onboarding.openHA': 'Open Home Assistant', + 'onboarding.completeSetup': 'Complete Setup', + 'onboarding.urlError': 'URL must start with http:// or https://', + 'onboarding.connectionFailed': 'Connection failed (HTTP {status}). Check the URL and try again.', + 'onboarding.notReachable': 'Could not reach Home Assistant. Verify the URL and ensure it is accessible.', + 'onboarding.tokenRequired': 'Please enter your access token.', } as const; export type TranslationKey = keyof typeof en; diff --git a/beacon/src/i18n/translations/es.ts b/beacon/src/i18n/translations/es.ts index 5e981d9d..047c7dbe 100644 --- a/beacon/src/i18n/translations/es.ts +++ b/beacon/src/i18n/translations/es.ts @@ -431,4 +431,108 @@ export const es: Record = { 'photoFrame.pauseSlideshow': 'Pausar presentación', 'photoFrame.resumeSlideshow': 'Reanudar presentación', 'photoFrame.nextPhoto': 'Siguiente foto', + + // ---- chores view ---- + 'chores.title': 'Tareas', + 'chores.addChore': 'Agregar tarea', + 'chores.editChore': 'Editar tarea', + 'chores.newChore': 'Nueva tarea', + 'chores.claim': 'Reclamar', + 'chores.complete': 'Completar', + 'chores.whoClaims': '¿Quién reclama esto?', + 'chores.whoCompleted': '¿Quién completó esto?', + 'chores.editChoreAria': 'Editar {name}', + 'chores.deleteChoreAria': 'Eliminar {name}', + 'chores.name': 'Nombre', + 'chores.value': 'Valor', + 'chores.frequency': 'Frecuencia', + 'chores.assignedTo': 'Asignado a', + 'chores.assignTo': 'Asignar a', + 'chores.icon': 'Icono', + 'chores.daily': 'Diario', + 'chores.weekly': 'Semanal', + 'chores.monthly': 'Mensual', + 'chores.oneTime': 'Una vez', + 'chores.namePlaceholder': 'p. ej., Aspirar la sala', + 'chores.anyone': 'Cualquiera', + 'chores.empty': 'Aún no hay tareas — agrega una', + 'chores.addMembersFirst': 'Agrega miembros de la familia primero para asignar tareas.', + 'chores.openChores': 'Tareas abiertas', + 'chores.noChoresAssigned': 'No hay tareas asignadas', + 'chores.addChoreFor': 'Agregar tarea para {name}', + 'chores.saveChanges': 'Guardar cambios', + + // ---- leaderboard ---- + 'leaderboard.title': 'Tabla de clasificación', + 'leaderboard.thisWeek': 'Esta semana', + 'leaderboard.thisMonth': 'Este mes', + 'leaderboard.empty': 'Agrega miembros de la familia y tareas para ver la tabla de clasificación.', + 'leaderboard.choresCompleted': '{count} tareas', + 'leaderboard.choreCompletedSingular': '{count} tarea', + + // ---- streak badge ---- + 'streak.longestTooltip': 'Racha más larga: {count} días', + 'streak.best': 'Mejor:', + + // ---- music view ---- + 'music.noPlayers': 'Sin reproductores multimedia', + 'music.noPlayersDesc': 'Conecta un reproductor multimedia en Home Assistant para controlar música desde aquí.', + 'music.selectPlayer': 'Seleccionar reproductor', + 'music.nowPlaying': 'Reproduciendo ahora', + 'music.volume': 'Volumen', + 'music.openMusicView': 'Abrir vista de música', + 'music.previousTrack': 'Pista anterior', + 'music.nextTrack': 'Siguiente pista', + 'music.play': 'Reproducir', + 'music.pause': 'Pausar', + 'music.mute': 'Silenciar', + 'music.unmute': 'Activar sonido', + 'music.albumArt': 'Arte del álbum', + + // ---- grocery / lists view ---- + 'grocery.noListsAvailable': 'No hay listas disponibles', + 'grocery.addItem': 'Agregar artículo', + 'grocery.addItemPlaceholder': 'Agregar un artículo...', + 'grocery.loadingItems': 'Cargando artículos...', + 'grocery.empty': 'Aún no hay artículos — agrega uno arriba', + 'grocery.items': 'Artículos ({count})', + 'grocery.completed': 'Completados ({count})', + 'grocery.deleteItem': 'Eliminar artículo', + 'grocery.haList': '{name} (HA)', + + // ---- card picker modal ---- + 'cardPicker.title': 'Agregar tarjeta', + 'cardPicker.empty': 'No hay tipos de tarjetas disponibles para esta área.', + + // ---- card config modal (common) ---- + 'cardConfig.entitiesPlaceholder': 'Entidades', + + // ---- view name modal ---- + 'viewName.name': 'Nombre', + + // ---- onboarding ---- + 'onboarding.welcome': 'Bienvenido a Beacon', + 'onboarding.welcomeDesc': 'Conecta tu Home Assistant para comenzar.', + 'onboarding.getStarted': 'Comenzar', + 'onboarding.connectTitle': 'Conectar a Home Assistant', + 'onboarding.connectDesc': 'Ingresa la URL de tu instancia de Home Assistant.', + 'onboarding.haUrl': 'URL de Home Assistant', + 'onboarding.haUrlPlaceholder': 'https://homeassistant.local:8123', + 'onboarding.connect': 'Conectar', + 'onboarding.connecting': 'Conectando...', + 'onboarding.authTitle': 'Autenticación', + 'onboarding.authDesc': 'Elige cómo autenticarte con Home Assistant.', + 'onboarding.tokenMethod': 'Token de acceso de larga duración', + 'onboarding.tokenMethodDesc': 'Usa un token generado desde tu perfil de Home Assistant.', + 'onboarding.tokenPlaceholder': 'Pega tu token aquí', + 'onboarding.tokenHint': 'Cómo crear un token:', + 'onboarding.tokenHintDetail': 'Ve a tu Perfil de HA → Tokens de larga duración → Crear token', + 'onboarding.oauthMethod': 'Iniciar sesión con Home Assistant', + 'onboarding.oauthMethodDesc': 'Autentícate usando tus credenciales de Home Assistant vía OAuth2.', + 'onboarding.openHA': 'Abrir Home Assistant', + 'onboarding.completeSetup': 'Completar configuración', + 'onboarding.urlError': 'La URL debe comenzar con http:// o https://', + 'onboarding.connectionFailed': 'Conexión fallida (HTTP {status}). Verifica la URL e intenta nuevamente.', + 'onboarding.notReachable': 'No se pudo alcanzar Home Assistant. Verifica la URL y asegúrate de que sea accesible.', + 'onboarding.tokenRequired': 'Por favor ingresa tu token de acceso.', }; diff --git a/beacon/src/i18n/translations/fr.ts b/beacon/src/i18n/translations/fr.ts index 0aba3a56..f55d0549 100644 --- a/beacon/src/i18n/translations/fr.ts +++ b/beacon/src/i18n/translations/fr.ts @@ -431,4 +431,108 @@ export const fr: Record = { 'photoFrame.pauseSlideshow': 'Mettre le diaporama en pause', 'photoFrame.resumeSlideshow': 'Reprendre le diaporama', 'photoFrame.nextPhoto': 'Photo suivante', + + // ---- chores view ---- + 'chores.title': 'Tâches', + 'chores.addChore': 'Ajouter une tâche', + 'chores.editChore': 'Modifier la tâche', + 'chores.newChore': 'Nouvelle tâche', + 'chores.claim': 'Revendiquer', + 'chores.complete': 'Terminer', + 'chores.whoClaims': 'Qui revendique cela ?', + 'chores.whoCompleted': 'Qui a terminé cela ?', + 'chores.editChoreAria': 'Modifier {name}', + 'chores.deleteChoreAria': 'Supprimer {name}', + 'chores.name': 'Nom', + 'chores.value': 'Valeur', + 'chores.frequency': 'Fréquence', + 'chores.assignedTo': 'Assigné à', + 'chores.assignTo': 'Assigner à', + 'chores.icon': 'Icône', + 'chores.daily': 'Quotidien', + 'chores.weekly': 'Hebdomadaire', + 'chores.monthly': 'Mensuel', + 'chores.oneTime': 'Une fois', + 'chores.namePlaceholder': 'p. ex., Passer l\'aspirateur', + 'chores.anyone': 'N\'importe qui', + 'chores.empty': 'Pas encore de tâches — ajoutez-en une', + 'chores.addMembersFirst': 'Ajoutez d\'abord des membres de la famille pour assigner des tâches.', + 'chores.openChores': 'Tâches ouvertes', + 'chores.noChoresAssigned': 'Aucune tâche assignée', + 'chores.addChoreFor': 'Ajouter une tâche pour {name}', + 'chores.saveChanges': 'Enregistrer les modifications', + + // ---- leaderboard ---- + 'leaderboard.title': 'Classement', + 'leaderboard.thisWeek': 'Cette semaine', + 'leaderboard.thisMonth': 'Ce mois-ci', + 'leaderboard.empty': 'Ajoutez des membres de la famille et des tâches pour voir le classement.', + 'leaderboard.choresCompleted': '{count} tâches', + 'leaderboard.choreCompletedSingular': '{count} tâche', + + // ---- streak badge ---- + 'streak.longestTooltip': 'Série la plus longue : {count} jours', + 'streak.best': 'Meilleur :', + + // ---- music view ---- + 'music.noPlayers': 'Aucun lecteur multimédia', + 'music.noPlayersDesc': 'Connectez un lecteur multimédia dans Home Assistant pour contrôler la musique ici.', + 'music.selectPlayer': 'Sélectionner un lecteur', + 'music.nowPlaying': 'En cours de lecture', + 'music.volume': 'Volume', + 'music.openMusicView': 'Ouvrir la vue musicale', + 'music.previousTrack': 'Piste précédente', + 'music.nextTrack': 'Piste suivante', + 'music.play': 'Lecture', + 'music.pause': 'Pause', + 'music.mute': 'Muet', + 'music.unmute': 'Rétablir le son', + 'music.albumArt': 'Pochette d\'album', + + // ---- grocery / lists view ---- + 'grocery.noListsAvailable': 'Aucune liste disponible', + 'grocery.addItem': 'Ajouter un article', + 'grocery.addItemPlaceholder': 'Ajouter un article...', + 'grocery.loadingItems': 'Chargement des articles...', + 'grocery.empty': 'Pas encore d\'articles — ajoutez-en un ci-dessus', + 'grocery.items': 'Articles ({count})', + 'grocery.completed': 'Terminés ({count})', + 'grocery.deleteItem': 'Supprimer l\'article', + 'grocery.haList': '{name} (HA)', + + // ---- card picker modal ---- + 'cardPicker.title': 'Ajouter une carte', + 'cardPicker.empty': 'Aucun type de carte disponible pour cette zone.', + + // ---- card config modal (common) ---- + 'cardConfig.entitiesPlaceholder': 'Entités', + + // ---- view name modal ---- + 'viewName.name': 'Nom', + + // ---- onboarding ---- + 'onboarding.welcome': 'Bienvenue sur Beacon', + 'onboarding.welcomeDesc': 'Connectez votre Home Assistant pour commencer.', + 'onboarding.getStarted': 'Commencer', + 'onboarding.connectTitle': 'Se connecter à Home Assistant', + 'onboarding.connectDesc': 'Entrez l\'URL de votre instance Home Assistant.', + 'onboarding.haUrl': 'URL de Home Assistant', + 'onboarding.haUrlPlaceholder': 'https://homeassistant.local:8123', + 'onboarding.connect': 'Connecter', + 'onboarding.connecting': 'Connexion en cours...', + 'onboarding.authTitle': 'Authentification', + 'onboarding.authDesc': 'Choisissez comment vous authentifier avec Home Assistant.', + 'onboarding.tokenMethod': 'Jeton d\'accès de longue durée', + 'onboarding.tokenMethodDesc': 'Utilisez un jeton généré depuis votre profil Home Assistant.', + 'onboarding.tokenPlaceholder': 'Collez votre jeton ici', + 'onboarding.tokenHint': 'Comment créer un jeton :', + 'onboarding.tokenHintDetail': 'Allez dans votre Profil HA → Jetons de longue durée → Créer un jeton', + 'onboarding.oauthMethod': 'Se connecter avec Home Assistant', + 'onboarding.oauthMethodDesc': 'Authentifiez-vous en utilisant vos identifiants Home Assistant via OAuth2.', + 'onboarding.openHA': 'Ouvrir Home Assistant', + 'onboarding.completeSetup': 'Terminer la configuration', + 'onboarding.urlError': 'L\'URL doit commencer par http:// ou https://', + 'onboarding.connectionFailed': 'Échec de la connexion (HTTP {status}). Vérifiez l\'URL et réessayez.', + 'onboarding.notReachable': 'Impossible d\'atteindre Home Assistant. Vérifiez l\'URL et assurez-vous qu\'elle est accessible.', + 'onboarding.tokenRequired': 'Veuillez entrer votre jeton d\'accès.', }; diff --git a/beacon/src/i18n/translations/it.ts b/beacon/src/i18n/translations/it.ts index 5fb614f9..b70f375e 100644 --- a/beacon/src/i18n/translations/it.ts +++ b/beacon/src/i18n/translations/it.ts @@ -431,4 +431,108 @@ export const it: Record = { 'photoFrame.pauseSlideshow': 'Metti in pausa la presentazione', 'photoFrame.resumeSlideshow': 'Riprendi la presentazione', 'photoFrame.nextPhoto': 'Foto successiva', + + // ---- chores view ---- + 'chores.title': 'Faccende', + 'chores.addChore': 'Aggiungi faccenda', + 'chores.editChore': 'Modifica faccenda', + 'chores.newChore': 'Nuova faccenda', + 'chores.claim': 'Rivendica', + 'chores.complete': 'Completa', + 'chores.whoClaims': 'Chi rivendica questo?', + 'chores.whoCompleted': 'Chi ha completato questo?', + 'chores.editChoreAria': 'Modifica {name}', + 'chores.deleteChoreAria': 'Elimina {name}', + 'chores.name': 'Nome', + 'chores.value': 'Valore', + 'chores.frequency': 'Frequenza', + 'chores.assignedTo': 'Assegnato a', + 'chores.assignTo': 'Assegna a', + 'chores.icon': 'Icona', + 'chores.daily': 'Giornaliero', + 'chores.weekly': 'Settimanale', + 'chores.monthly': 'Mensile', + 'chores.oneTime': 'Una volta', + 'chores.namePlaceholder': 'es., Aspirare il soggiorno', + 'chores.anyone': 'Chiunque', + 'chores.empty': 'Nessuna faccenda ancora — aggiungine una', + 'chores.addMembersFirst': 'Aggiungi prima i membri della famiglia per assegnare le faccende.', + 'chores.openChores': 'Faccende aperte', + 'chores.noChoresAssigned': 'Nessuna faccenda assegnata', + 'chores.addChoreFor': 'Aggiungi faccenda per {name}', + 'chores.saveChanges': 'Salva modifiche', + + // ---- leaderboard ---- + 'leaderboard.title': 'Classifica', + 'leaderboard.thisWeek': 'Questa settimana', + 'leaderboard.thisMonth': 'Questo mese', + 'leaderboard.empty': 'Aggiungi membri della famiglia e faccende per vedere la classifica.', + 'leaderboard.choresCompleted': '{count} faccende', + 'leaderboard.choreCompletedSingular': '{count} faccenda', + + // ---- streak badge ---- + 'streak.longestTooltip': 'Serie più lunga: {count} giorni', + 'streak.best': 'Migliore:', + + // ---- music view ---- + 'music.noPlayers': 'Nessun lettore multimediale', + 'music.noPlayersDesc': 'Collega un lettore multimediale in Home Assistant per controllare la musica da qui.', + 'music.selectPlayer': 'Seleziona lettore', + 'music.nowPlaying': 'In riproduzione', + 'music.volume': 'Volume', + 'music.openMusicView': 'Apri vista musica', + 'music.previousTrack': 'Traccia precedente', + 'music.nextTrack': 'Traccia successiva', + 'music.play': 'Riproduci', + 'music.pause': 'Pausa', + 'music.mute': 'Muto', + 'music.unmute': 'Riattiva audio', + 'music.albumArt': 'Copertina album', + + // ---- grocery / lists view ---- + 'grocery.noListsAvailable': 'Nessuna lista disponibile', + 'grocery.addItem': 'Aggiungi articolo', + 'grocery.addItemPlaceholder': 'Aggiungi un articolo...', + 'grocery.loadingItems': 'Caricamento articoli...', + 'grocery.empty': 'Nessun articolo ancora — aggiungine uno sopra', + 'grocery.items': 'Articoli ({count})', + 'grocery.completed': 'Completati ({count})', + 'grocery.deleteItem': 'Elimina articolo', + 'grocery.haList': '{name} (HA)', + + // ---- card picker modal ---- + 'cardPicker.title': 'Aggiungi scheda', + 'cardPicker.empty': 'Nessun tipo di scheda disponibile per quest\'area.', + + // ---- card config modal (common) ---- + 'cardConfig.entitiesPlaceholder': 'Entità', + + // ---- view name modal ---- + 'viewName.name': 'Nome', + + // ---- onboarding ---- + 'onboarding.welcome': 'Benvenuto su Beacon', + 'onboarding.welcomeDesc': 'Collega il tuo Home Assistant per iniziare.', + 'onboarding.getStarted': 'Inizia', + 'onboarding.connectTitle': 'Collega a Home Assistant', + 'onboarding.connectDesc': 'Inserisci l\'URL della tua istanza Home Assistant.', + 'onboarding.haUrl': 'URL di Home Assistant', + 'onboarding.haUrlPlaceholder': 'https://homeassistant.local:8123', + 'onboarding.connect': 'Collega', + 'onboarding.connecting': 'Connessione in corso...', + 'onboarding.authTitle': 'Autenticazione', + 'onboarding.authDesc': 'Scegli come autenticarti con Home Assistant.', + 'onboarding.tokenMethod': 'Token di accesso a lungo termine', + 'onboarding.tokenMethodDesc': 'Usa un token generato dal tuo profilo Home Assistant.', + 'onboarding.tokenPlaceholder': 'Incolla il tuo token qui', + 'onboarding.tokenHint': 'Come creare un token:', + 'onboarding.tokenHintDetail': 'Vai al tuo Profilo HA → Token a lungo termine → Crea token', + 'onboarding.oauthMethod': 'Accedi con Home Assistant', + 'onboarding.oauthMethodDesc': 'Autenticati utilizzando le tue credenziali Home Assistant tramite OAuth2.', + 'onboarding.openHA': 'Apri Home Assistant', + 'onboarding.completeSetup': 'Completa configurazione', + 'onboarding.urlError': 'L\'URL deve iniziare con http:// o https://', + 'onboarding.connectionFailed': 'Connessione fallita (HTTP {status}). Controlla l\'URL e riprova.', + 'onboarding.notReachable': 'Impossibile raggiungere Home Assistant. Verifica l\'URL e assicurati che sia accessibile.', + 'onboarding.tokenRequired': 'Inserisci il tuo token di accesso.', }; diff --git a/beacon/src/i18n/translations/ja.ts b/beacon/src/i18n/translations/ja.ts index 9ed214b8..d22a8a87 100644 --- a/beacon/src/i18n/translations/ja.ts +++ b/beacon/src/i18n/translations/ja.ts @@ -431,4 +431,108 @@ export const ja: Record = { 'photoFrame.pauseSlideshow': 'スライドショーを一時停止', 'photoFrame.resumeSlideshow': 'スライドショーを再開', 'photoFrame.nextPhoto': '次の写真', + + // ---- chores view ---- + 'chores.title': '家事', + 'chores.addChore': '家事を追加', + 'chores.editChore': '家事を編集', + 'chores.newChore': '新しい家事', + 'chores.claim': '引き受ける', + 'chores.complete': '完了する', + 'chores.whoClaims': '誰が引き受けますか?', + 'chores.whoCompleted': '誰が完了しましたか?', + 'chores.editChoreAria': '{name}を編集', + 'chores.deleteChoreAria': '{name}を削除', + 'chores.name': '名前', + 'chores.value': '価値', + 'chores.frequency': '頻度', + 'chores.assignedTo': '担当者', + 'chores.assignTo': '割り当て先', + 'chores.icon': 'アイコン', + 'chores.daily': '毎日', + 'chores.weekly': '毎週', + 'chores.monthly': '毎月', + 'chores.oneTime': '一回のみ', + 'chores.namePlaceholder': '例:リビングを掃除機で掃除', + 'chores.anyone': '誰でも', + 'chores.empty': 'まだ家事がありません — 追加してください', + 'chores.addMembersFirst': '家事を割り当てるには、まず家族を追加してください。', + 'chores.openChores': '未完了の家事', + 'chores.noChoresAssigned': '割り当てられた家事はありません', + 'chores.addChoreFor': '{name}の家事を追加', + 'chores.saveChanges': '変更を保存', + + // ---- leaderboard ---- + 'leaderboard.title': 'リーダーボード', + 'leaderboard.thisWeek': '今週', + 'leaderboard.thisMonth': '今月', + 'leaderboard.empty': '家族と家事を追加して、リーダーボードを表示してください。', + 'leaderboard.choresCompleted': '{count}件の家事', + 'leaderboard.choreCompletedSingular': '{count}件の家事', + + // ---- streak badge ---- + 'streak.longestTooltip': '最長連続記録:{count}日', + 'streak.best': '最高:', + + // ---- music view ---- + 'music.noPlayers': 'メディアプレーヤーがありません', + 'music.noPlayersDesc': 'Home Assistantでメディアプレーヤーを接続して、ここから音楽を制御してください。', + 'music.selectPlayer': 'プレーヤーを選択', + 'music.nowPlaying': '再生中', + 'music.volume': '音量', + 'music.openMusicView': '音楽ビューを開く', + 'music.previousTrack': '前のトラック', + 'music.nextTrack': '次のトラック', + 'music.play': '再生', + 'music.pause': '一時停止', + 'music.mute': 'ミュート', + 'music.unmute': 'ミュート解除', + 'music.albumArt': 'アルバムアート', + + // ---- grocery / lists view ---- + 'grocery.noListsAvailable': '利用可能なリストがありません', + 'grocery.addItem': 'アイテムを追加', + 'grocery.addItemPlaceholder': 'アイテムを追加...', + 'grocery.loadingItems': 'アイテムを読み込み中...', + 'grocery.empty': 'まだアイテムがありません — 上に追加してください', + 'grocery.items': 'アイテム({count})', + 'grocery.completed': '完了済み({count})', + 'grocery.deleteItem': 'アイテムを削除', + 'grocery.haList': '{name}(HA)', + + // ---- card picker modal ---- + 'cardPicker.title': 'カードを追加', + 'cardPicker.empty': 'このエリアに利用可能なカードタイプはありません。', + + // ---- card config modal (common) ---- + 'cardConfig.entitiesPlaceholder': 'エンティティ', + + // ---- view name modal ---- + 'viewName.name': '名前', + + // ---- onboarding ---- + 'onboarding.welcome': 'Beaconへようこそ', + 'onboarding.welcomeDesc': 'Home Assistantを接続して始めましょう。', + 'onboarding.getStarted': '始める', + 'onboarding.connectTitle': 'Home Assistantに接続', + 'onboarding.connectDesc': 'Home AssistantインスタンスのURLを入力してください。', + 'onboarding.haUrl': 'Home Assistant URL', + 'onboarding.haUrlPlaceholder': 'https://homeassistant.local:8123', + 'onboarding.connect': '接続', + 'onboarding.connecting': '接続中...', + 'onboarding.authTitle': '認証', + 'onboarding.authDesc': 'Home Assistantへの認証方法を選択してください。', + 'onboarding.tokenMethod': '長期アクセストークン', + 'onboarding.tokenMethodDesc': 'Home Assistantプロファイルから生成されたトークンを使用します。', + 'onboarding.tokenPlaceholder': 'トークンをここに貼り付けてください', + 'onboarding.tokenHint': 'トークンの作成方法:', + 'onboarding.tokenHintDetail': 'HAプロファイル → 長期トークン → トークンを作成', + 'onboarding.oauthMethod': 'Home Assistantでサインイン', + 'onboarding.oauthMethodDesc': 'OAuth2を介してHome Assistant認証情報を使用して認証します。', + 'onboarding.openHA': 'Home Assistantを開く', + 'onboarding.completeSetup': 'セットアップを完了', + 'onboarding.urlError': 'URLはhttp://またはhttps://で始まる必要があります', + 'onboarding.connectionFailed': '接続に失敗しました(HTTP {status})。URLを確認して再試行してください。', + 'onboarding.notReachable': 'Home Assistantに到達できませんでした。URLを確認し、アクセス可能であることを確認してください。', + 'onboarding.tokenRequired': 'アクセストークンを入力してください。', }; diff --git a/beacon/src/i18n/translations/ko.ts b/beacon/src/i18n/translations/ko.ts index c695306f..35d6cc93 100644 --- a/beacon/src/i18n/translations/ko.ts +++ b/beacon/src/i18n/translations/ko.ts @@ -431,4 +431,108 @@ export const ko: Record = { 'photoFrame.pauseSlideshow': '슬라이드쇼 일시정지', 'photoFrame.resumeSlideshow': '슬라이드쇼 재개', 'photoFrame.nextPhoto': '다음 사진', + + // ---- chores view ---- + 'chores.title': '집안일', + 'chores.addChore': '집안일 추가', + 'chores.editChore': '집안일 수정', + 'chores.newChore': '새 집안일', + 'chores.claim': '맡기', + 'chores.complete': '완료', + 'chores.whoClaims': '누가 이것을 맡나요?', + 'chores.whoCompleted': '누가 이것을 완료했나요?', + 'chores.editChoreAria': '{name} 수정', + 'chores.deleteChoreAria': '{name} 삭제', + 'chores.name': '이름', + 'chores.value': '가치', + 'chores.frequency': '빈도', + 'chores.assignedTo': '담당자', + 'chores.assignTo': '배정', + 'chores.icon': '아이콘', + 'chores.daily': '매일', + 'chores.weekly': '매주', + 'chores.monthly': '매월', + 'chores.oneTime': '한 번', + 'chores.namePlaceholder': '예: 거실 청소', + 'chores.anyone': '누구나', + 'chores.empty': '아직 집안일이 없습니다 — 추가하세요', + 'chores.addMembersFirst': '집안일을 배정하려면 먼저 가족 구성원을 추가하세요.', + 'chores.openChores': '미완료 집안일', + 'chores.noChoresAssigned': '배정된 집안일이 없습니다', + 'chores.addChoreFor': '{name}의 집안일 추가', + 'chores.saveChanges': '변경사항 저장', + + // ---- leaderboard ---- + 'leaderboard.title': '리더보드', + 'leaderboard.thisWeek': '이번 주', + 'leaderboard.thisMonth': '이번 달', + 'leaderboard.empty': '가족 구성원과 집안일을 추가하여 리더보드를 확인하세요.', + 'leaderboard.choresCompleted': '{count}개 집안일', + 'leaderboard.choreCompletedSingular': '{count}개 집안일', + + // ---- streak badge ---- + 'streak.longestTooltip': '최장 연속 기록: {count}일', + 'streak.best': '최고:', + + // ---- music view ---- + 'music.noPlayers': '미디어 플레이어 없음', + 'music.noPlayersDesc': 'Home Assistant에서 미디어 플레이어를 연결하여 여기에서 음악을 제어하세요.', + 'music.selectPlayer': '플레이어 선택', + 'music.nowPlaying': '재생 중', + 'music.volume': '볼륨', + 'music.openMusicView': '음악 보기 열기', + 'music.previousTrack': '이전 트랙', + 'music.nextTrack': '다음 트랙', + 'music.play': '재생', + 'music.pause': '일시 정지', + 'music.mute': '음소거', + 'music.unmute': '음소거 해제', + 'music.albumArt': '앨범 아트', + + // ---- grocery / lists view ---- + 'grocery.noListsAvailable': '사용 가능한 목록이 없습니다', + 'grocery.addItem': '항목 추가', + 'grocery.addItemPlaceholder': '항목 추가...', + 'grocery.loadingItems': '항목 로딩 중...', + 'grocery.empty': '아직 항목이 없습니다 — 위에 추가하세요', + 'grocery.items': '항목 ({count})', + 'grocery.completed': '완료 ({count})', + 'grocery.deleteItem': '항목 삭제', + 'grocery.haList': '{name} (HA)', + + // ---- card picker modal ---- + 'cardPicker.title': '카드 추가', + 'cardPicker.empty': '이 영역에 사용 가능한 카드 유형이 없습니다.', + + // ---- card config modal (common) ---- + 'cardConfig.entitiesPlaceholder': '엔티티', + + // ---- view name modal ---- + 'viewName.name': '이름', + + // ---- onboarding ---- + 'onboarding.welcome': 'Beacon에 오신 것을 환영합니다', + 'onboarding.welcomeDesc': 'Home Assistant를 연결하여 시작하세요.', + 'onboarding.getStarted': '시작하기', + 'onboarding.connectTitle': 'Home Assistant에 연결', + 'onboarding.connectDesc': 'Home Assistant 인스턴스의 URL을 입력하세요.', + 'onboarding.haUrl': 'Home Assistant URL', + 'onboarding.haUrlPlaceholder': 'https://homeassistant.local:8123', + 'onboarding.connect': '연결', + 'onboarding.connecting': '연결 중...', + 'onboarding.authTitle': '인증', + 'onboarding.authDesc': 'Home Assistant 인증 방법을 선택하세요.', + 'onboarding.tokenMethod': '장기 액세스 토큰', + 'onboarding.tokenMethodDesc': 'Home Assistant 프로필에서 생성된 토큰을 사용하세요.', + 'onboarding.tokenPlaceholder': '여기에 토큰을 붙여넣으세요', + 'onboarding.tokenHint': '토큰 생성 방법:', + 'onboarding.tokenHintDetail': 'HA 프로필 → 장기 토큰 → 토큰 생성', + 'onboarding.oauthMethod': 'Home Assistant로 로그인', + 'onboarding.oauthMethodDesc': 'OAuth2를 통해 Home Assistant 자격 증명을 사용하여 인증하세요.', + 'onboarding.openHA': 'Home Assistant 열기', + 'onboarding.completeSetup': '설정 완료', + 'onboarding.urlError': 'URL은 http:// 또는 https://로 시작해야 합니다', + 'onboarding.connectionFailed': '연결 실패 (HTTP {status}). URL을 확인하고 다시 시도하세요.', + 'onboarding.notReachable': 'Home Assistant에 연결할 수 없습니다. URL을 확인하고 접근 가능한지 확인하세요.', + 'onboarding.tokenRequired': '액세스 토큰을 입력하세요.', }; diff --git a/beacon/src/i18n/translations/nl.ts b/beacon/src/i18n/translations/nl.ts index 7ceb5248..865e410e 100644 --- a/beacon/src/i18n/translations/nl.ts +++ b/beacon/src/i18n/translations/nl.ts @@ -431,4 +431,108 @@ export const nl: Record = { 'photoFrame.pauseSlideshow': 'Diavoorstelling pauzeren', 'photoFrame.resumeSlideshow': 'Diavoorstelling hervatten', 'photoFrame.nextPhoto': 'Volgende foto', + + // ---- chores view ---- + 'chores.title': 'Karweitjes', + 'chores.addChore': 'Karweitje toevoegen', + 'chores.editChore': 'Karweitje bewerken', + 'chores.newChore': 'Nieuw karweitje', + 'chores.claim': 'Claimen', + 'chores.complete': 'Voltooien', + 'chores.whoClaims': 'Wie claimt dit?', + 'chores.whoCompleted': 'Wie heeft dit voltooid?', + 'chores.editChoreAria': '{name} bewerken', + 'chores.deleteChoreAria': '{name} verwijderen', + 'chores.name': 'Naam', + 'chores.value': 'Waarde', + 'chores.frequency': 'Frequentie', + 'chores.assignedTo': 'Toegewezen aan', + 'chores.assignTo': 'Toewijzen aan', + 'chores.icon': 'Pictogram', + 'chores.daily': 'Dagelijks', + 'chores.weekly': 'Wekelijks', + 'chores.monthly': 'Maandelijks', + 'chores.oneTime': 'Eenmalig', + 'chores.namePlaceholder': 'bijv. Woonkamer stofzuigen', + 'chores.anyone': 'Iedereen', + 'chores.empty': 'Nog geen karweitjes — voeg er een toe', + 'chores.addMembersFirst': 'Voeg eerst gezinsleden toe om karweitjes toe te wijzen.', + 'chores.openChores': 'Open karweitjes', + 'chores.noChoresAssigned': 'Geen karweitjes toegewezen', + 'chores.addChoreFor': 'Karweitje toevoegen voor {name}', + 'chores.saveChanges': 'Wijzigingen opslaan', + + // ---- leaderboard ---- + 'leaderboard.title': 'Klassement', + 'leaderboard.thisWeek': 'Deze week', + 'leaderboard.thisMonth': 'Deze maand', + 'leaderboard.empty': 'Voeg gezinsleden en karweitjes toe om het klassement te zien.', + 'leaderboard.choresCompleted': '{count} karweitjes', + 'leaderboard.choreCompletedSingular': '{count} karweitje', + + // ---- streak badge ---- + 'streak.longestTooltip': 'Langste reeks: {count} dagen', + 'streak.best': 'Beste:', + + // ---- music view ---- + 'music.noPlayers': 'Geen mediaspelers', + 'music.noPlayersDesc': 'Verbind een mediaspeler in Home Assistant om hier muziek te bedienen.', + 'music.selectPlayer': 'Speler selecteren', + 'music.nowPlaying': 'Nu bezig', + 'music.volume': 'Volume', + 'music.openMusicView': 'Muziekweergave openen', + 'music.previousTrack': 'Vorig nummer', + 'music.nextTrack': 'Volgend nummer', + 'music.play': 'Afspelen', + 'music.pause': 'Pauzeren', + 'music.mute': 'Dempen', + 'music.unmute': 'Geluid aan', + 'music.albumArt': 'Albumhoes', + + // ---- grocery / lists view ---- + 'grocery.noListsAvailable': 'Geen lijsten beschikbaar', + 'grocery.addItem': 'Item toevoegen', + 'grocery.addItemPlaceholder': 'Voeg een item toe...', + 'grocery.loadingItems': 'Items laden...', + 'grocery.empty': 'Nog geen items — voeg er een toe hierboven', + 'grocery.items': 'Items ({count})', + 'grocery.completed': 'Voltooid ({count})', + 'grocery.deleteItem': 'Item verwijderen', + 'grocery.haList': '{name} (HA)', + + // ---- card picker modal ---- + 'cardPicker.title': 'Kaart toevoegen', + 'cardPicker.empty': 'Geen kaarttypen beschikbaar voor dit gebied.', + + // ---- card config modal (common) ---- + 'cardConfig.entitiesPlaceholder': 'Entiteiten', + + // ---- view name modal ---- + 'viewName.name': 'Naam', + + // ---- onboarding ---- + 'onboarding.welcome': 'Welkom bij Beacon', + 'onboarding.welcomeDesc': 'Verbind je Home Assistant om te beginnen.', + 'onboarding.getStarted': 'Aan de slag', + 'onboarding.connectTitle': 'Verbinden met Home Assistant', + 'onboarding.connectDesc': 'Voer de URL van je Home Assistant-instantie in.', + 'onboarding.haUrl': 'Home Assistant URL', + 'onboarding.haUrlPlaceholder': 'https://homeassistant.local:8123', + 'onboarding.connect': 'Verbinden', + 'onboarding.connecting': 'Verbinden...', + 'onboarding.authTitle': 'Authenticatie', + 'onboarding.authDesc': 'Kies hoe je wilt authenticeren met Home Assistant.', + 'onboarding.tokenMethod': 'Langdurig toegangstoken', + 'onboarding.tokenMethodDesc': 'Gebruik een token gegenereerd vanuit je Home Assistant-profiel.', + 'onboarding.tokenPlaceholder': 'Plak je token hier', + 'onboarding.tokenHint': 'Hoe maak je een token:', + 'onboarding.tokenHintDetail': 'Ga naar je HA-profiel → Langdurige tokens → Token aanmaken', + 'onboarding.oauthMethod': 'Inloggen met Home Assistant', + 'onboarding.oauthMethodDesc': 'Authenticeer met je Home Assistant-referenties via OAuth2.', + 'onboarding.openHA': 'Home Assistant openen', + 'onboarding.completeSetup': 'Installatie voltooien', + 'onboarding.urlError': 'URL moet beginnen met http:// of https://', + 'onboarding.connectionFailed': 'Verbinding mislukt (HTTP {status}). Controleer de URL en probeer opnieuw.', + 'onboarding.notReachable': 'Kan Home Assistant niet bereiken. Controleer de URL en zorg ervoor dat het toegankelijk is.', + 'onboarding.tokenRequired': 'Voer je toegangstoken in.', }; diff --git a/beacon/src/i18n/translations/pt.ts b/beacon/src/i18n/translations/pt.ts index 06b8b67b..352a1af4 100644 --- a/beacon/src/i18n/translations/pt.ts +++ b/beacon/src/i18n/translations/pt.ts @@ -431,4 +431,108 @@ export const pt: Record = { 'photoFrame.pauseSlideshow': 'Pausar apresentação', 'photoFrame.resumeSlideshow': 'Retomar apresentação', 'photoFrame.nextPhoto': 'Próxima foto', + + // ---- chores view ---- + 'chores.title': 'Tarefas', + 'chores.addChore': 'Adicionar tarefa', + 'chores.editChore': 'Editar tarefa', + 'chores.newChore': 'Nova tarefa', + 'chores.claim': 'Reivindicar', + 'chores.complete': 'Concluir', + 'chores.whoClaims': 'Quem reivindica isto?', + 'chores.whoCompleted': 'Quem concluiu isto?', + 'chores.editChoreAria': 'Editar {name}', + 'chores.deleteChoreAria': 'Excluir {name}', + 'chores.name': 'Nome', + 'chores.value': 'Valor', + 'chores.frequency': 'Frequência', + 'chores.assignedTo': 'Atribuído a', + 'chores.assignTo': 'Atribuir a', + 'chores.icon': 'Ícone', + 'chores.daily': 'Diário', + 'chores.weekly': 'Semanal', + 'chores.monthly': 'Mensal', + 'chores.oneTime': 'Uma vez', + 'chores.namePlaceholder': 'ex: Aspirar a sala', + 'chores.anyone': 'Qualquer um', + 'chores.empty': 'Ainda não há tarefas — adicione uma', + 'chores.addMembersFirst': 'Adicione membros da família primeiro para atribuir tarefas.', + 'chores.openChores': 'Tarefas abertas', + 'chores.noChoresAssigned': 'Nenhuma tarefa atribuída', + 'chores.addChoreFor': 'Adicionar tarefa para {name}', + 'chores.saveChanges': 'Salvar alterações', + + // ---- leaderboard ---- + 'leaderboard.title': 'Classificação', + 'leaderboard.thisWeek': 'Esta semana', + 'leaderboard.thisMonth': 'Este mês', + 'leaderboard.empty': 'Adicione membros da família e tarefas para ver a classificação.', + 'leaderboard.choresCompleted': '{count} tarefas', + 'leaderboard.choreCompletedSingular': '{count} tarefa', + + // ---- streak badge ---- + 'streak.longestTooltip': 'Sequência mais longa: {count} dias', + 'streak.best': 'Melhor:', + + // ---- music view ---- + 'music.noPlayers': 'Nenhum reprodutor de mídia', + 'music.noPlayersDesc': 'Conecte um reprodutor de mídia no Home Assistant para controlar música aqui.', + 'music.selectPlayer': 'Selecionar reprodutor', + 'music.nowPlaying': 'Tocando agora', + 'music.volume': 'Volume', + 'music.openMusicView': 'Abrir visualização de música', + 'music.previousTrack': 'Faixa anterior', + 'music.nextTrack': 'Próxima faixa', + 'music.play': 'Reproduzir', + 'music.pause': 'Pausar', + 'music.mute': 'Silenciar', + 'music.unmute': 'Ativar som', + 'music.albumArt': 'Capa do álbum', + + // ---- grocery / lists view ---- + 'grocery.noListsAvailable': 'Nenhuma lista disponível', + 'grocery.addItem': 'Adicionar item', + 'grocery.addItemPlaceholder': 'Adicionar um item...', + 'grocery.loadingItems': 'Carregando itens...', + 'grocery.empty': 'Ainda não há itens — adicione um acima', + 'grocery.items': 'Itens ({count})', + 'grocery.completed': 'Concluídos ({count})', + 'grocery.deleteItem': 'Excluir item', + 'grocery.haList': '{name} (HA)', + + // ---- card picker modal ---- + 'cardPicker.title': 'Adicionar cartão', + 'cardPicker.empty': 'Nenhum tipo de cartão disponível para esta área.', + + // ---- card config modal (common) ---- + 'cardConfig.entitiesPlaceholder': 'Entidades', + + // ---- view name modal ---- + 'viewName.name': 'Nome', + + // ---- onboarding ---- + 'onboarding.welcome': 'Bem-vindo ao Beacon', + 'onboarding.welcomeDesc': 'Conecte seu Home Assistant para começar.', + 'onboarding.getStarted': 'Começar', + 'onboarding.connectTitle': 'Conectar ao Home Assistant', + 'onboarding.connectDesc': 'Digite a URL da sua instância do Home Assistant.', + 'onboarding.haUrl': 'URL do Home Assistant', + 'onboarding.haUrlPlaceholder': 'https://homeassistant.local:8123', + 'onboarding.connect': 'Conectar', + 'onboarding.connecting': 'Conectando...', + 'onboarding.authTitle': 'Autenticação', + 'onboarding.authDesc': 'Escolha como autenticar com o Home Assistant.', + 'onboarding.tokenMethod': 'Token de acesso de longa duração', + 'onboarding.tokenMethodDesc': 'Use um token gerado do seu perfil do Home Assistant.', + 'onboarding.tokenPlaceholder': 'Cole seu token aqui', + 'onboarding.tokenHint': 'Como criar um token:', + 'onboarding.tokenHintDetail': 'Vá para seu Perfil HA → Tokens de longa duração → Criar token', + 'onboarding.oauthMethod': 'Entrar com Home Assistant', + 'onboarding.oauthMethodDesc': 'Autentique usando suas credenciais do Home Assistant via OAuth2.', + 'onboarding.openHA': 'Abrir Home Assistant', + 'onboarding.completeSetup': 'Concluir configuração', + 'onboarding.urlError': 'URL deve começar com http:// ou https://', + 'onboarding.connectionFailed': 'Conexão falhou (HTTP {status}). Verifique a URL e tente novamente.', + 'onboarding.notReachable': 'Não foi possível alcançar o Home Assistant. Verifique a URL e certifique-se de que esteja acessível.', + 'onboarding.tokenRequired': 'Por favor, insira seu token de acesso.', }; diff --git a/src/components/ChoresView.tsx b/src/components/ChoresView.tsx index 3485c92b..c1d4218d 100644 --- a/src/components/ChoresView.tsx +++ b/src/components/ChoresView.tsx @@ -6,6 +6,7 @@ import { StreakBadge } from './StreakBadge'; import { useChores } from '../hooks/useChores'; import { useFamily } from '../hooks/useFamily'; import { useSettings } from '../hooks/useSettings'; +import { useTranslation } from '../i18n'; const CHORE_ICONS = ['🧹', '🍽️', '🐕', '🛏️', '📚', '🗑️', '👕', '🧺', '🪥', '🚿', '🧼', '💪']; @@ -36,6 +37,7 @@ function UnassignedChoreCard({ onDelete, currencySymbol = '$', }: UnassignedChoreCardProps) { + const { t } = useTranslation(); const [showMemberPicker, setShowMemberPicker] = useState(false); const [pickerAction, setPickerAction] = useState<'claim' | 'complete'>('claim'); const [confirmDelete, setConfirmDelete] = useState(false); @@ -80,7 +82,7 @@ function UnassignedChoreCard({ setShowMemberPicker(true); }} > - Claim + {t('chores.claim')} @@ -104,10 +106,10 @@ function UnassignedChoreCard({ type="button" className="chore-card-action-btn chore-card-action-btn--danger" onClick={handleDeleteClick} - aria-label={`Delete ${chore.name}`} + aria-label={t('chores.deleteChoreAria', { name: chore.name })} > {confirmDelete ? ( - Sure? + {t('common.sure')} ) : ( )} @@ -119,7 +121,7 @@ function UnassignedChoreCard({
e.stopPropagation()}>

- {pickerAction === 'claim' ? 'Who claims this?' : 'Who completed this?'} + {pickerAction === 'claim' ? t('chores.whoClaims') : t('chores.whoCompleted')}

)} {members.length === 0 ? (
- Add family members first to assign chores. + {t('chores.addMembersFirst')}
) : ( <> {unassignedChores.length > 0 && (
-

Open Chores

+

{t('chores.openChores')}

{unassignedChores.map((chore) => ( {memberChores.length === 0 ? ( -
No chores assigned
+
{t('chores.noChoresAssigned')}
) : (
{memberChores.map((chore) => ( @@ -368,7 +371,7 @@ export function ChoresView() { onClick={() => openAddForm(member.id)} > - Add chore for {member.name} + {t('chores.addChoreFor', { name: member.name })}
))} @@ -380,7 +383,7 @@ export function ChoresView() {
e.stopPropagation()}>
-

{isEditing ? 'Edit Chore' : 'New Chore'}

+

{isEditing ? t('chores.editChore') : t('chores.newChore')}

@@ -389,7 +392,7 @@ export function ChoresView() {
{/* Icon picker */}
- +
{CHORE_ICONS.map((icon) => ( )}
diff --git a/src/components/GroceryView.tsx b/src/components/GroceryView.tsx index 3e6c9f39..7f43b646 100644 --- a/src/components/GroceryView.tsx +++ b/src/components/GroceryView.tsx @@ -4,6 +4,7 @@ import { GroceryList } from '../types/grocery'; import { AnyListClient } from '../api/anylist'; import { callHaService, hasToken } from '../api/ha-rest'; import { useLocalTasks } from '../hooks/useLocalTasks'; +import { useTranslation } from '../i18n'; interface TodoItem { uid: string; @@ -40,6 +41,7 @@ interface GroceryViewProps { } export function GroceryView({ defaultListId, mode = 'grocery', groceryListIds = [] }: GroceryViewProps) { + const { t } = useTranslation(); const [haLists, setHaLists] = useState([]); const [selectedListId, setSelectedListId] = useState(defaultListId || ''); const [haItems, setHaItems] = useState([]); @@ -249,11 +251,11 @@ export function GroceryView({ defaultListId, mode = 'grocery', groceryListIds = onChange={(e) => setSelectedListId(e.target.value)} > {allLists.length === 0 && ( - + )} {allLists.map(list => ( ))} @@ -267,7 +269,7 @@ export function GroceryView({ defaultListId, mode = 'grocery', groceryListIds = ref={inputRef} type="text" className="grocery-view-input" - placeholder="Add an item..." + placeholder={t('grocery.addItemPlaceholder')} value={inputValue} onChange={(e) => setInputValue(e.target.value)} disabled={submitting || !selectedListId} @@ -276,7 +278,7 @@ export function GroceryView({ defaultListId, mode = 'grocery', groceryListIds = type="submit" className="grocery-view-add-btn" disabled={!inputValue.trim() || submitting} - aria-label="Add item" + aria-label={t('grocery.addItem')} > @@ -285,7 +287,7 @@ export function GroceryView({ defaultListId, mode = 'grocery', groceryListIds = {/* Items list */}
{loading && items.length === 0 ? ( -
Loading items...
+
{t('grocery.loadingItems')}
) : items.length === 0 && !loading ? (
@@ -296,7 +298,7 @@ export function GroceryView({ defaultListId, mode = 'grocery', groceryListIds =
-

No items yet — add one above

+

{t('grocery.empty')}

) : ( <> @@ -304,7 +306,7 @@ export function GroceryView({ defaultListId, mode = 'grocery', groceryListIds = {uncheckedItems.length > 0 && (
- Items ({uncheckedItems.length}) + {t('grocery.items', { count: uncheckedItems.length })}
{uncheckedItems.map(item => (
@@ -321,7 +323,7 @@ export function GroceryView({ defaultListId, mode = 'grocery', groceryListIds = type="button" className="grocery-view-delete" onClick={() => handleDelete(item)} - aria-label="Delete item" + aria-label={t('grocery.deleteItem')} > @@ -340,7 +342,7 @@ export function GroceryView({ defaultListId, mode = 'grocery', groceryListIds = onClick={() => setCompletedCollapsed(prev => !prev)} > {completedCollapsed ? : } - Completed ({checkedItems.length}) + {t('grocery.completed', { count: checkedItems.length })} {!completedCollapsed && checkedItems.map(item => (
@@ -367,7 +369,7 @@ export function GroceryView({ defaultListId, mode = 'grocery', groceryListIds = type="button" className="grocery-view-delete" onClick={() => handleDelete(item)} - aria-label="Delete item" + aria-label={t('grocery.deleteItem')} > diff --git a/src/components/Leaderboard.tsx b/src/components/Leaderboard.tsx index b3472bb7..b0c27c43 100644 --- a/src/components/Leaderboard.tsx +++ b/src/components/Leaderboard.tsx @@ -5,6 +5,7 @@ import { useFamily } from '../hooks/useFamily'; import { useSettings } from '../hooks/useSettings'; import { MemberEarnings, formatChoreValue } from '../types/family'; import { localDayKey } from '../api/date-keys'; +import { useTranslation } from '../i18n'; interface LeaderboardProps { open: boolean; @@ -34,6 +35,7 @@ function getMonthRange(): [string, string] { const RANK_DECORATIONS = ['\u{1F947}', '\u{1F948}', '\u{1F949}']; export function Leaderboard({ open, onClose }: LeaderboardProps) { + const { t } = useTranslation(); const { members } = useFamily(); const { settings } = useSettings(); const { getEarningsForPeriod, getStreakForMember } = useChores(); @@ -61,7 +63,7 @@ export function Leaderboard({ open, onClose }: LeaderboardProps) { return (
-

Leaderboard

+

{t('leaderboard.title')}

@@ -74,20 +76,20 @@ export function Leaderboard({ open, onClose }: LeaderboardProps) { className={`lb-period-btn ${period === 'week' ? 'lb-period-btn--active' : ''}`} onClick={() => setPeriod('week')} > - This Week + {t('leaderboard.thisWeek')}
{ranked.length === 0 && ( -
Add family members and chores to see the leaderboard.
+
{t('leaderboard.empty')}
)}
@@ -109,7 +111,10 @@ export function Leaderboard({ open, onClose }: LeaderboardProps) {
{entry.member.name} - {entry.earnings.chore_count} chore{entry.earnings.chore_count !== 1 ? 's' : ''} + {entry.earnings.chore_count === 1 + ? t('leaderboard.choreCompletedSingular', { count: entry.earnings.chore_count }) + : t('leaderboard.choresCompleted', { count: entry.earnings.chore_count }) + }
diff --git a/src/components/MusicView.tsx b/src/components/MusicView.tsx index ad4ac6e1..c73c1683 100644 --- a/src/components/MusicView.tsx +++ b/src/components/MusicView.tsx @@ -10,6 +10,7 @@ import { ChevronDown, } from 'lucide-react'; import { MediaPlayer } from '../types/music'; +import { useTranslation } from '../i18n'; interface MusicViewProps { players: MediaPlayer[]; @@ -44,14 +45,15 @@ export function MusicView({ onPrevious, onSetVolume, }: MusicViewProps) { + const { t } = useTranslation(); // Empty state when no media players found if (players.length === 0) { return (
🎵
-

No Media Players

-

Connect a media player in Home Assistant to control music from here.

+

{t('music.noPlayers')}

+

{t('music.noPlayersDesc')}

); @@ -104,7 +106,7 @@ export function MusicView({ className="music-player-selector-btn" onClick={() => setShowPlayerDropdown(!showPlayerDropdown)} > - {player?.friendly_name || 'Select Player'} + {player?.friendly_name || t('music.selectPlayer')} {showPlayerDropdown && ( @@ -123,7 +125,7 @@ export function MusicView({ > {p.friendly_name} {p.state === 'playing' && ( - Playing + {t('music.nowPlaying')} )} ))} @@ -138,7 +140,7 @@ export function MusicView({ {player?.media_album_name ) : (
@@ -150,7 +152,7 @@ export function MusicView({ {/* Track info */}

- {player?.media_title || player?.app_name || 'Nothing Playing'} + {player?.media_title || player?.app_name || t('music.nowPlaying')}

{player?.media_artist || ''} @@ -180,7 +182,7 @@ export function MusicView({ type="button" className="music-control-btn" onClick={() => onPrevious(player?.entity_id)} - aria-label="Previous track" + aria-label={t('music.previousTrack')} > @@ -188,7 +190,7 @@ export function MusicView({ type="button" className="music-control-btn music-control-btn--play" onClick={() => (isPlaying ? onPause : onPlay)(player?.entity_id)} - aria-label={isPlaying ? 'Pause' : 'Play'} + aria-label={isPlaying ? t('music.pause') : t('music.play')} > {isPlaying ? : } @@ -196,7 +198,7 @@ export function MusicView({ type="button" className="music-control-btn" onClick={() => onNext(player?.entity_id)} - aria-label="Next track" + aria-label={t('music.nextTrack')} > @@ -213,7 +215,7 @@ export function MusicView({ player?.entity_id, ) } - aria-label={player?.is_volume_muted ? 'Unmute' : 'Mute'} + aria-label={player?.is_volume_muted ? t('music.unmute') : t('music.mute')} > {player?.is_volume_muted ? : } @@ -225,7 +227,7 @@ export function MusicView({ step={0.02} value={player?.is_volume_muted ? 0 : (player?.volume_level ?? 0.5)} onChange={(e) => onSetVolume(parseFloat(e.target.value), player?.entity_id)} - aria-label="Volume" + aria-label={t('music.volume')} />

diff --git a/src/components/NowPlayingBar.tsx b/src/components/NowPlayingBar.tsx index b5bf512a..ef2e2b3a 100644 --- a/src/components/NowPlayingBar.tsx +++ b/src/components/NowPlayingBar.tsx @@ -9,6 +9,7 @@ import { } from 'lucide-react'; import { MediaPlayer } from '../types/music'; import { getConfig } from '../config'; +import { useTranslation } from '../i18n'; interface NowPlayingBarProps { player: MediaPlayer | null; @@ -29,6 +30,7 @@ export function NowPlayingBar({ onSetVolume, onExpand, }: NowPlayingBarProps) { + const { t } = useTranslation(); // Local progress tracking const [position, setPosition] = useState(player?.media_position ?? 0); const animRef = useRef | null>(null); @@ -59,7 +61,7 @@ export function NowPlayingBar({ : null; return ( -
+
{/* Thin progress bar across the top */} {duration > 0 && (
@@ -71,13 +73,13 @@ export function NowPlayingBar({ type="button" className="now-playing-info" onClick={onExpand} - aria-label="Open music view" + aria-label={t('music.openMusicView')} > {artSrc ? ( {player.media_album_name ) : (
@@ -98,7 +100,7 @@ export function NowPlayingBar({ type="button" className="now-playing-btn" onClick={onPrevious} - aria-label="Previous track" + aria-label={t('music.previousTrack')} > @@ -106,7 +108,7 @@ export function NowPlayingBar({ type="button" className="now-playing-btn now-playing-btn--play" onClick={isPlaying ? onPause : onPlay} - aria-label={isPlaying ? 'Pause' : 'Play'} + aria-label={isPlaying ? t('music.pause') : t('music.play')} > {isPlaying ? : } @@ -114,7 +116,7 @@ export function NowPlayingBar({ type="button" className="now-playing-btn" onClick={onNext} - aria-label="Next track" + aria-label={t('music.nextTrack')} > @@ -126,7 +128,7 @@ export function NowPlayingBar({ type="button" className="now-playing-btn" onClick={() => onSetVolume(player.is_volume_muted ? (player.volume_level || 0.5) : 0)} - aria-label={player.is_volume_muted ? 'Unmute' : 'Mute'} + aria-label={player.is_volume_muted ? t('music.unmute') : t('music.mute')} > {player.is_volume_muted ? : } @@ -138,7 +140,7 @@ export function NowPlayingBar({ step={0.02} value={player.is_volume_muted ? 0 : (player.volume_level ?? 0.5)} onChange={(e) => onSetVolume(parseFloat(e.target.value))} - aria-label="Volume" + aria-label={t('music.volume')} />
diff --git a/src/components/OnboardingView.tsx b/src/components/OnboardingView.tsx index a944a630..711f79c3 100644 --- a/src/components/OnboardingView.tsx +++ b/src/components/OnboardingView.tsx @@ -1,6 +1,7 @@ import { useState, useCallback } from 'react'; import { ArrowRight, Check, ExternalLink, Key, LogIn } from 'lucide-react'; import beaconLogo from '../assets/beacon-app-icon.svg'; +import { useTranslation } from '../i18n'; // --------------------------------------------------------------------------- // Types @@ -197,6 +198,7 @@ const styles = { // --------------------------------------------------------------------------- export default function OnboardingView({ onComplete, onOAuthStart }: OnboardingViewProps) { + const { t } = useTranslation(); const [step, setStep] = useState(1); const [haUrl, setHaUrl] = useState(''); const [urlError, setUrlError] = useState(''); @@ -216,7 +218,7 @@ export default function OnboardingView({ onComplete, onOAuthStart }: OnboardingV const testConnection = useCallback(async () => { const trimmed = haUrl.trim().replace(/\/+$/, ''); if (!isValidUrl(trimmed)) { - setUrlError('URL must start with http:// or https://'); + setUrlError(t('onboarding.urlError')); return; } setUrlError(''); @@ -228,24 +230,24 @@ export default function OnboardingView({ onComplete, onOAuthStart }: OnboardingV setHaUrl(trimmed); setStep(3); } else { - setUrlError(`Connection failed (HTTP ${response.status}). Check the URL and try again.`); + setUrlError(t('onboarding.connectionFailed', { status: response.status })); } } catch { - setUrlError('Could not reach Home Assistant. Verify the URL and ensure it is accessible.'); + setUrlError(t('onboarding.notReachable')); } finally { setConnecting(false); } - }, [haUrl, isValidUrl]); + }, [haUrl, isValidUrl, t]); // ---- Complete setup ---- const handleComplete = useCallback(() => { if (!token.trim()) { - setTokenError('Please enter your access token.'); + setTokenError(t('onboarding.tokenRequired')); return; } setTokenError(''); onComplete(haUrl, token.trim()); - }, [haUrl, token, onComplete]); + }, [haUrl, token, onComplete, t]); // ---- Render steps ---- @@ -263,13 +265,13 @@ export default function OnboardingView({ onComplete, onOAuthStart }: OnboardingV const renderStep1 = () => (
Beacon -

Welcome to Beacon

-

Connect your Home Assistant to get started.

+

{t('onboarding.welcome')}

+

{t('onboarding.welcomeDesc')}

@@ -277,16 +279,16 @@ export default function OnboardingView({ onComplete, onOAuthStart }: OnboardingV const renderStep2 = () => (
-

Connect to Home Assistant

+

{t('onboarding.connectTitle')}

- Enter the URL of your Home Assistant instance. + {t('onboarding.connectDesc')}

- + { setHaUrl(e.target.value); @@ -311,7 +313,7 @@ export default function OnboardingView({ onComplete, onOAuthStart }: OnboardingV disabled={connecting || !haUrl.trim()} onClick={testConnection} > - {connecting ? 'Connecting...' : 'Connect'} + {connecting ? t('onboarding.connecting') : t('onboarding.connect')} {!connecting && }
@@ -319,9 +321,9 @@ export default function OnboardingView({ onComplete, onOAuthStart }: OnboardingV const renderStep3 = () => (
-

Authentication

+

{t('onboarding.authTitle')}

- Choose how to authenticate with Home Assistant. + {t('onboarding.authDesc')}

@@ -332,17 +334,17 @@ export default function OnboardingView({ onComplete, onOAuthStart }: OnboardingV >

- Long-lived Access Token + {t('onboarding.tokenMethod')}

- Use a token generated from your Home Assistant profile. + {t('onboarding.tokenMethodDesc')}

{authMethod === 'token' && (
{ setToken(e.target.value); @@ -356,9 +358,9 @@ export default function OnboardingView({ onComplete, onOAuthStart }: OnboardingV /> {tokenError &&

{tokenError}

}
- How to create a token: + {t('onboarding.tokenHint')}
- Go to your HA Profile → Long-lived tokens → Create Token + {t('onboarding.tokenHintDetail')}
)} @@ -371,10 +373,10 @@ export default function OnboardingView({ onComplete, onOAuthStart }: OnboardingV >

- Sign in with Home Assistant + {t('onboarding.oauthMethod')}

- Authenticate using your Home Assistant credentials via OAuth2. + {t('onboarding.oauthMethodDesc')}

{authMethod === 'oauth' && ( @@ -386,7 +388,7 @@ export default function OnboardingView({ onComplete, onOAuthStart }: OnboardingV onOAuthStart?.(haUrl); }} > - Open Home Assistant + {t('onboarding.openHA')}
@@ -403,7 +405,7 @@ export default function OnboardingView({ onComplete, onOAuthStart }: OnboardingV disabled={!token.trim()} onClick={handleComplete} > - Complete Setup + {t('onboarding.completeSetup')} )} diff --git a/src/components/StreakBadge.tsx b/src/components/StreakBadge.tsx index 1010ecd0..ac66de92 100644 --- a/src/components/StreakBadge.tsx +++ b/src/components/StreakBadge.tsx @@ -1,5 +1,6 @@ import { useState } from 'react'; import { Streak } from '../types/family'; +import { useTranslation } from '../i18n'; interface StreakBadgeProps { streak: Streak; @@ -7,6 +8,7 @@ interface StreakBadgeProps { } export function StreakBadge({ streak, size = 'md' }: StreakBadgeProps) { + const { t } = useTranslation(); const [showLongest, setShowLongest] = useState(false); if (streak.current === 0 && streak.longest === 0) return null; @@ -20,13 +22,13 @@ export function StreakBadge({ streak, size = 'md' }: StreakBadgeProps) { type="button" className={`streak-badge ${sizeClass} ${hotClass}`} onClick={() => setShowLongest((v) => !v)} - title={`Longest streak: ${streak.longest} days`} + title={t('streak.longestTooltip', { count: streak.longest })} > 🔥 {showLongest ? ( <> - Best: {streak.longest} + {t('streak.best')} {streak.longest} ) : ( streak.current diff --git a/src/components/cards/CardPickerModal.tsx b/src/components/cards/CardPickerModal.tsx index e91abab8..e3882d77 100644 --- a/src/components/cards/CardPickerModal.tsx +++ b/src/components/cards/CardPickerModal.tsx @@ -17,7 +17,7 @@ export function CardPickerModal({ region, onPick, onClose }: CardPickerModalProp
e.stopPropagation()}>
-

Add Card

+

{t('cardPicker.title')}

@@ -34,7 +34,7 @@ export function CardPickerModal({ region, onPick, onClose }: CardPickerModalProp ))} {options.length === 0 && ( -
No card types available for this area.
+
{t('cardPicker.empty')}
)}
diff --git a/src/components/cards/ViewNameModal.tsx b/src/components/cards/ViewNameModal.tsx index e2566474..41f1637a 100644 --- a/src/components/cards/ViewNameModal.tsx +++ b/src/components/cards/ViewNameModal.tsx @@ -1,4 +1,5 @@ import { useState } from 'react'; +import { useTranslation } from '../../i18n'; interface ViewNameModalProps { title: string; @@ -9,6 +10,7 @@ interface ViewNameModalProps { /** Small text-input modal used for adding/renaming a dashboard view (tab). */ export function ViewNameModal({ title, initialValue, onSave, onClose }: ViewNameModalProps) { + const { t } = useTranslation(); const [name, setName] = useState(initialValue); const handleSave = () => { @@ -25,7 +27,7 @@ export function ViewNameModal({ title, initialValue, onSave, onClose }: ViewName
- +
- - + +
diff --git a/src/components/cards/registry.tsx b/src/components/cards/registry.tsx index 1d7e8a16..816f054b 100644 --- a/src/components/cards/registry.tsx +++ b/src/components/cards/registry.tsx @@ -104,10 +104,10 @@ export const cardRegistry: Record = { displayNameKey: 'cards.haEntitiesList.displayName', icon: '📶', component: HaEntitiesListCard, - defaultConfig: { title: 'Entities', subtitle: '', entity_ids: [] }, + defaultConfig: { title: '', subtitle: '', entity_ids: [] }, defaultSize: 'sm', configFields: [ - { type: 'text', key: 'title', labelKey: 'cards.common.titleLabel' }, + { type: 'text', key: 'title', labelKey: 'cards.common.titleLabel', placeholderKey: 'cardConfig.entitiesPlaceholder' }, { type: 'text', key: 'subtitle', labelKey: 'cards.common.subtitleLabel' }, { type: 'entity-list', key: 'entity_ids', labelKey: 'cards.common.entitiesLabel' }, ], diff --git a/src/i18n/translations/de.ts b/src/i18n/translations/de.ts index 184ecd0e..0c7061f7 100644 --- a/src/i18n/translations/de.ts +++ b/src/i18n/translations/de.ts @@ -431,4 +431,108 @@ export const de: Record = { 'photoFrame.pauseSlideshow': 'Diashow pausieren', 'photoFrame.resumeSlideshow': 'Diashow fortsetzen', 'photoFrame.nextPhoto': 'Nächstes Foto', + + // ---- chores view ---- + 'chores.title': 'Aufgaben', + 'chores.addChore': 'Aufgabe hinzufügen', + 'chores.editChore': 'Aufgabe bearbeiten', + 'chores.newChore': 'Neue Aufgabe', + 'chores.claim': 'Übernehmen', + 'chores.complete': 'Erledigen', + 'chores.whoClaims': 'Wer übernimmt dies?', + 'chores.whoCompleted': 'Wer hat das erledigt?', + 'chores.editChoreAria': '{name} bearbeiten', + 'chores.deleteChoreAria': '{name} löschen', + 'chores.name': 'Name', + 'chores.value': 'Wert', + 'chores.frequency': 'Häufigkeit', + 'chores.assignedTo': 'Zugewiesen an', + 'chores.assignTo': 'Zuweisen an', + 'chores.icon': 'Symbol', + 'chores.daily': 'Täglich', + 'chores.weekly': 'Wöchentlich', + 'chores.monthly': 'Monatlich', + 'chores.oneTime': 'Einmalig', + 'chores.namePlaceholder': 'z.B. Wohnzimmer saugen', + 'chores.anyone': 'Jeder', + 'chores.empty': 'Noch keine Aufgaben — füge eine hinzu', + 'chores.addMembersFirst': 'Füge erst Familienmitglieder hinzu, um Aufgaben zuzuweisen.', + 'chores.openChores': 'Offene Aufgaben', + 'chores.noChoresAssigned': 'Keine Aufgaben zugewiesen', + 'chores.addChoreFor': 'Aufgabe für {name} hinzufügen', + 'chores.saveChanges': 'Änderungen speichern', + + // ---- leaderboard ---- + 'leaderboard.title': 'Bestenliste', + 'leaderboard.thisWeek': 'Diese Woche', + 'leaderboard.thisMonth': 'Dieser Monat', + 'leaderboard.empty': 'Füge Familienmitglieder und Aufgaben hinzu, um die Bestenliste zu sehen.', + 'leaderboard.choresCompleted': '{count} Aufgaben', + 'leaderboard.choreCompletedSingular': '{count} Aufgabe', + + // ---- streak badge ---- + 'streak.longestTooltip': 'Längste Serie: {count} Tage', + 'streak.best': 'Beste:', + + // ---- music view ---- + 'music.noPlayers': 'Keine Mediaplayer', + 'music.noPlayersDesc': 'Verbinde einen Mediaplayer in Home Assistant, um hier Musik zu steuern.', + 'music.selectPlayer': 'Player auswählen', + 'music.nowPlaying': 'Läuft gerade', + 'music.volume': 'Lautstärke', + 'music.openMusicView': 'Musikansicht öffnen', + 'music.previousTrack': 'Vorheriger Titel', + 'music.nextTrack': 'Nächster Titel', + 'music.play': 'Abspielen', + 'music.pause': 'Pausieren', + 'music.mute': 'Stumm', + 'music.unmute': 'Laut', + 'music.albumArt': 'Albumcover', + + // ---- grocery / lists view ---- + 'grocery.noListsAvailable': 'Keine Listen verfügbar', + 'grocery.addItem': 'Artikel hinzufügen', + 'grocery.addItemPlaceholder': 'Artikel hinzufügen...', + 'grocery.loadingItems': 'Artikel werden geladen...', + 'grocery.empty': 'Noch keine Artikel — füge oben einen hinzu', + 'grocery.items': 'Artikel ({count})', + 'grocery.completed': 'Erledigt ({count})', + 'grocery.deleteItem': 'Artikel löschen', + 'grocery.haList': '{name} (HA)', + + // ---- card picker modal ---- + 'cardPicker.title': 'Karte hinzufügen', + 'cardPicker.empty': 'Keine Kartentypen für diesen Bereich verfügbar.', + + // ---- card config modal (common) ---- + 'cardConfig.entitiesPlaceholder': 'Entitäten', + + // ---- view name modal ---- + 'viewName.name': 'Name', + + // ---- onboarding ---- + 'onboarding.welcome': 'Willkommen bei Beacon', + 'onboarding.welcomeDesc': 'Verbinde dein Home Assistant, um loszulegen.', + 'onboarding.getStarted': 'Los geht\'s', + 'onboarding.connectTitle': 'Mit Home Assistant verbinden', + 'onboarding.connectDesc': 'Gib die URL deiner Home Assistant-Instanz ein.', + 'onboarding.haUrl': 'Home Assistant URL', + 'onboarding.haUrlPlaceholder': 'https://homeassistant.local:8123', + 'onboarding.connect': 'Verbinden', + 'onboarding.connecting': 'Verbindung wird hergestellt...', + 'onboarding.authTitle': 'Authentifizierung', + 'onboarding.authDesc': 'Wähle, wie du dich bei Home Assistant authentifizieren möchtest.', + 'onboarding.tokenMethod': 'Langlebiges Zugriffstoken', + 'onboarding.tokenMethodDesc': 'Verwende ein Token aus deinem Home Assistant-Profil.', + 'onboarding.tokenPlaceholder': 'Token hier einfügen', + 'onboarding.tokenHint': 'So erstellst du ein Token:', + 'onboarding.tokenHintDetail': 'Gehe zu deinem HA-Profil → Langlebige Zugriffstoken → Token erstellen', + 'onboarding.oauthMethod': 'Mit Home Assistant anmelden', + 'onboarding.oauthMethodDesc': 'Authentifiziere dich mit deinen Home Assistant-Zugangsdaten via OAuth2.', + 'onboarding.openHA': 'Home Assistant öffnen', + 'onboarding.completeSetup': 'Einrichtung abschließen', + 'onboarding.urlError': 'URL muss mit http:// oder https:// beginnen', + 'onboarding.connectionFailed': 'Verbindung fehlgeschlagen (HTTP {status}). Überprüfe die URL und versuche es erneut.', + 'onboarding.notReachable': 'Home Assistant konnte nicht erreicht werden. Überprüfe die URL und stelle sicher, dass sie erreichbar ist.', + 'onboarding.tokenRequired': 'Bitte gib dein Zugriffstoken ein.', }; diff --git a/src/i18n/translations/en.ts b/src/i18n/translations/en.ts index 19e92bc3..d7b8889d 100644 --- a/src/i18n/translations/en.ts +++ b/src/i18n/translations/en.ts @@ -445,6 +445,110 @@ export const en = { 'photoFrame.pauseSlideshow': 'Pause slideshow', 'photoFrame.resumeSlideshow': 'Resume slideshow', 'photoFrame.nextPhoto': 'Next photo', + + // ---- chores view ---- + 'chores.title': 'Chores', + 'chores.addChore': 'Add Chore', + 'chores.editChore': 'Edit Chore', + 'chores.newChore': 'New Chore', + 'chores.claim': 'Claim', + 'chores.complete': 'Complete', + 'chores.whoClaims': 'Who claims this?', + 'chores.whoCompleted': 'Who completed this?', + 'chores.editChoreAria': 'Edit {name}', + 'chores.deleteChoreAria': 'Delete {name}', + 'chores.name': 'Name', + 'chores.value': 'Value', + 'chores.frequency': 'Frequency', + 'chores.assignedTo': 'Assign To', + 'chores.assignTo': 'Assign To', + 'chores.icon': 'Icon', + 'chores.daily': 'Daily', + 'chores.weekly': 'Weekly', + 'chores.monthly': 'Monthly', + 'chores.oneTime': 'One-time', + 'chores.namePlaceholder': 'e.g., Vacuum living room', + 'chores.anyone': 'Anyone', + 'chores.empty': 'No chores yet — add one to get started', + 'chores.addMembersFirst': 'Add family members first to assign chores.', + 'chores.openChores': 'Open Chores', + 'chores.noChoresAssigned': 'No chores assigned', + 'chores.addChoreFor': 'Add chore for {name}', + 'chores.saveChanges': 'Save Changes', + + // ---- leaderboard ---- + 'leaderboard.title': 'Leaderboard', + 'leaderboard.thisWeek': 'This Week', + 'leaderboard.thisMonth': 'This Month', + 'leaderboard.empty': 'Add family members and chores to see the leaderboard.', + 'leaderboard.choresCompleted': '{count} chores', + 'leaderboard.choreCompletedSingular': '{count} chore', + + // ---- streak badge ---- + 'streak.longestTooltip': 'Longest streak: {count} days', + 'streak.best': 'Best:', + + // ---- music view ---- + 'music.noPlayers': 'No Media Players', + 'music.noPlayersDesc': 'Connect a media player in Home Assistant to control music from here.', + 'music.selectPlayer': 'Select Player', + 'music.nowPlaying': 'Now Playing', + 'music.volume': 'Volume', + 'music.openMusicView': 'Open music view', + 'music.previousTrack': 'Previous track', + 'music.nextTrack': 'Next track', + 'music.play': 'Play', + 'music.pause': 'Pause', + 'music.mute': 'Mute', + 'music.unmute': 'Unmute', + 'music.albumArt': 'Album art', + + // ---- grocery / lists view ---- + 'grocery.noListsAvailable': 'No lists available', + 'grocery.addItem': 'Add item', + 'grocery.addItemPlaceholder': 'Add an item...', + 'grocery.loadingItems': 'Loading items...', + 'grocery.empty': 'No items yet — add one above', + 'grocery.items': 'Items ({count})', + 'grocery.completed': 'Completed ({count})', + 'grocery.deleteItem': 'Delete item', + 'grocery.haList': '{name} (HA)', + + // ---- card picker modal ---- + 'cardPicker.title': 'Add Card', + 'cardPicker.empty': 'No card types available for this area.', + + // ---- card config modal (common) ---- + 'cardConfig.entitiesPlaceholder': 'Entities', + + // ---- view name modal ---- + 'viewName.name': 'Name', + + // ---- onboarding ---- + 'onboarding.welcome': 'Welcome to Beacon', + 'onboarding.welcomeDesc': 'Connect your Home Assistant to get started.', + 'onboarding.getStarted': 'Get Started', + 'onboarding.connectTitle': 'Connect to Home Assistant', + 'onboarding.connectDesc': 'Enter the URL of your Home Assistant instance.', + 'onboarding.haUrl': 'Home Assistant URL', + 'onboarding.haUrlPlaceholder': 'https://homeassistant.local:8123', + 'onboarding.connect': 'Connect', + 'onboarding.connecting': 'Connecting...', + 'onboarding.authTitle': 'Authentication', + 'onboarding.authDesc': 'Choose how to authenticate with Home Assistant.', + 'onboarding.tokenMethod': 'Long-lived Access Token', + 'onboarding.tokenMethodDesc': 'Use a token generated from your Home Assistant profile.', + 'onboarding.tokenPlaceholder': 'Paste your token here', + 'onboarding.tokenHint': 'How to create a token:', + 'onboarding.tokenHintDetail': 'Go to your HA Profile → Long-lived tokens → Create Token', + 'onboarding.oauthMethod': 'Sign in with Home Assistant', + 'onboarding.oauthMethodDesc': 'Authenticate using your Home Assistant credentials via OAuth2.', + 'onboarding.openHA': 'Open Home Assistant', + 'onboarding.completeSetup': 'Complete Setup', + 'onboarding.urlError': 'URL must start with http:// or https://', + 'onboarding.connectionFailed': 'Connection failed (HTTP {status}). Check the URL and try again.', + 'onboarding.notReachable': 'Could not reach Home Assistant. Verify the URL and ensure it is accessible.', + 'onboarding.tokenRequired': 'Please enter your access token.', } as const; export type TranslationKey = keyof typeof en; diff --git a/src/i18n/translations/es.ts b/src/i18n/translations/es.ts index 5e981d9d..047c7dbe 100644 --- a/src/i18n/translations/es.ts +++ b/src/i18n/translations/es.ts @@ -431,4 +431,108 @@ export const es: Record = { 'photoFrame.pauseSlideshow': 'Pausar presentación', 'photoFrame.resumeSlideshow': 'Reanudar presentación', 'photoFrame.nextPhoto': 'Siguiente foto', + + // ---- chores view ---- + 'chores.title': 'Tareas', + 'chores.addChore': 'Agregar tarea', + 'chores.editChore': 'Editar tarea', + 'chores.newChore': 'Nueva tarea', + 'chores.claim': 'Reclamar', + 'chores.complete': 'Completar', + 'chores.whoClaims': '¿Quién reclama esto?', + 'chores.whoCompleted': '¿Quién completó esto?', + 'chores.editChoreAria': 'Editar {name}', + 'chores.deleteChoreAria': 'Eliminar {name}', + 'chores.name': 'Nombre', + 'chores.value': 'Valor', + 'chores.frequency': 'Frecuencia', + 'chores.assignedTo': 'Asignado a', + 'chores.assignTo': 'Asignar a', + 'chores.icon': 'Icono', + 'chores.daily': 'Diario', + 'chores.weekly': 'Semanal', + 'chores.monthly': 'Mensual', + 'chores.oneTime': 'Una vez', + 'chores.namePlaceholder': 'p. ej., Aspirar la sala', + 'chores.anyone': 'Cualquiera', + 'chores.empty': 'Aún no hay tareas — agrega una', + 'chores.addMembersFirst': 'Agrega miembros de la familia primero para asignar tareas.', + 'chores.openChores': 'Tareas abiertas', + 'chores.noChoresAssigned': 'No hay tareas asignadas', + 'chores.addChoreFor': 'Agregar tarea para {name}', + 'chores.saveChanges': 'Guardar cambios', + + // ---- leaderboard ---- + 'leaderboard.title': 'Tabla de clasificación', + 'leaderboard.thisWeek': 'Esta semana', + 'leaderboard.thisMonth': 'Este mes', + 'leaderboard.empty': 'Agrega miembros de la familia y tareas para ver la tabla de clasificación.', + 'leaderboard.choresCompleted': '{count} tareas', + 'leaderboard.choreCompletedSingular': '{count} tarea', + + // ---- streak badge ---- + 'streak.longestTooltip': 'Racha más larga: {count} días', + 'streak.best': 'Mejor:', + + // ---- music view ---- + 'music.noPlayers': 'Sin reproductores multimedia', + 'music.noPlayersDesc': 'Conecta un reproductor multimedia en Home Assistant para controlar música desde aquí.', + 'music.selectPlayer': 'Seleccionar reproductor', + 'music.nowPlaying': 'Reproduciendo ahora', + 'music.volume': 'Volumen', + 'music.openMusicView': 'Abrir vista de música', + 'music.previousTrack': 'Pista anterior', + 'music.nextTrack': 'Siguiente pista', + 'music.play': 'Reproducir', + 'music.pause': 'Pausar', + 'music.mute': 'Silenciar', + 'music.unmute': 'Activar sonido', + 'music.albumArt': 'Arte del álbum', + + // ---- grocery / lists view ---- + 'grocery.noListsAvailable': 'No hay listas disponibles', + 'grocery.addItem': 'Agregar artículo', + 'grocery.addItemPlaceholder': 'Agregar un artículo...', + 'grocery.loadingItems': 'Cargando artículos...', + 'grocery.empty': 'Aún no hay artículos — agrega uno arriba', + 'grocery.items': 'Artículos ({count})', + 'grocery.completed': 'Completados ({count})', + 'grocery.deleteItem': 'Eliminar artículo', + 'grocery.haList': '{name} (HA)', + + // ---- card picker modal ---- + 'cardPicker.title': 'Agregar tarjeta', + 'cardPicker.empty': 'No hay tipos de tarjetas disponibles para esta área.', + + // ---- card config modal (common) ---- + 'cardConfig.entitiesPlaceholder': 'Entidades', + + // ---- view name modal ---- + 'viewName.name': 'Nombre', + + // ---- onboarding ---- + 'onboarding.welcome': 'Bienvenido a Beacon', + 'onboarding.welcomeDesc': 'Conecta tu Home Assistant para comenzar.', + 'onboarding.getStarted': 'Comenzar', + 'onboarding.connectTitle': 'Conectar a Home Assistant', + 'onboarding.connectDesc': 'Ingresa la URL de tu instancia de Home Assistant.', + 'onboarding.haUrl': 'URL de Home Assistant', + 'onboarding.haUrlPlaceholder': 'https://homeassistant.local:8123', + 'onboarding.connect': 'Conectar', + 'onboarding.connecting': 'Conectando...', + 'onboarding.authTitle': 'Autenticación', + 'onboarding.authDesc': 'Elige cómo autenticarte con Home Assistant.', + 'onboarding.tokenMethod': 'Token de acceso de larga duración', + 'onboarding.tokenMethodDesc': 'Usa un token generado desde tu perfil de Home Assistant.', + 'onboarding.tokenPlaceholder': 'Pega tu token aquí', + 'onboarding.tokenHint': 'Cómo crear un token:', + 'onboarding.tokenHintDetail': 'Ve a tu Perfil de HA → Tokens de larga duración → Crear token', + 'onboarding.oauthMethod': 'Iniciar sesión con Home Assistant', + 'onboarding.oauthMethodDesc': 'Autentícate usando tus credenciales de Home Assistant vía OAuth2.', + 'onboarding.openHA': 'Abrir Home Assistant', + 'onboarding.completeSetup': 'Completar configuración', + 'onboarding.urlError': 'La URL debe comenzar con http:// o https://', + 'onboarding.connectionFailed': 'Conexión fallida (HTTP {status}). Verifica la URL e intenta nuevamente.', + 'onboarding.notReachable': 'No se pudo alcanzar Home Assistant. Verifica la URL y asegúrate de que sea accesible.', + 'onboarding.tokenRequired': 'Por favor ingresa tu token de acceso.', }; diff --git a/src/i18n/translations/fr.ts b/src/i18n/translations/fr.ts index 0aba3a56..f55d0549 100644 --- a/src/i18n/translations/fr.ts +++ b/src/i18n/translations/fr.ts @@ -431,4 +431,108 @@ export const fr: Record = { 'photoFrame.pauseSlideshow': 'Mettre le diaporama en pause', 'photoFrame.resumeSlideshow': 'Reprendre le diaporama', 'photoFrame.nextPhoto': 'Photo suivante', + + // ---- chores view ---- + 'chores.title': 'Tâches', + 'chores.addChore': 'Ajouter une tâche', + 'chores.editChore': 'Modifier la tâche', + 'chores.newChore': 'Nouvelle tâche', + 'chores.claim': 'Revendiquer', + 'chores.complete': 'Terminer', + 'chores.whoClaims': 'Qui revendique cela ?', + 'chores.whoCompleted': 'Qui a terminé cela ?', + 'chores.editChoreAria': 'Modifier {name}', + 'chores.deleteChoreAria': 'Supprimer {name}', + 'chores.name': 'Nom', + 'chores.value': 'Valeur', + 'chores.frequency': 'Fréquence', + 'chores.assignedTo': 'Assigné à', + 'chores.assignTo': 'Assigner à', + 'chores.icon': 'Icône', + 'chores.daily': 'Quotidien', + 'chores.weekly': 'Hebdomadaire', + 'chores.monthly': 'Mensuel', + 'chores.oneTime': 'Une fois', + 'chores.namePlaceholder': 'p. ex., Passer l\'aspirateur', + 'chores.anyone': 'N\'importe qui', + 'chores.empty': 'Pas encore de tâches — ajoutez-en une', + 'chores.addMembersFirst': 'Ajoutez d\'abord des membres de la famille pour assigner des tâches.', + 'chores.openChores': 'Tâches ouvertes', + 'chores.noChoresAssigned': 'Aucune tâche assignée', + 'chores.addChoreFor': 'Ajouter une tâche pour {name}', + 'chores.saveChanges': 'Enregistrer les modifications', + + // ---- leaderboard ---- + 'leaderboard.title': 'Classement', + 'leaderboard.thisWeek': 'Cette semaine', + 'leaderboard.thisMonth': 'Ce mois-ci', + 'leaderboard.empty': 'Ajoutez des membres de la famille et des tâches pour voir le classement.', + 'leaderboard.choresCompleted': '{count} tâches', + 'leaderboard.choreCompletedSingular': '{count} tâche', + + // ---- streak badge ---- + 'streak.longestTooltip': 'Série la plus longue : {count} jours', + 'streak.best': 'Meilleur :', + + // ---- music view ---- + 'music.noPlayers': 'Aucun lecteur multimédia', + 'music.noPlayersDesc': 'Connectez un lecteur multimédia dans Home Assistant pour contrôler la musique ici.', + 'music.selectPlayer': 'Sélectionner un lecteur', + 'music.nowPlaying': 'En cours de lecture', + 'music.volume': 'Volume', + 'music.openMusicView': 'Ouvrir la vue musicale', + 'music.previousTrack': 'Piste précédente', + 'music.nextTrack': 'Piste suivante', + 'music.play': 'Lecture', + 'music.pause': 'Pause', + 'music.mute': 'Muet', + 'music.unmute': 'Rétablir le son', + 'music.albumArt': 'Pochette d\'album', + + // ---- grocery / lists view ---- + 'grocery.noListsAvailable': 'Aucune liste disponible', + 'grocery.addItem': 'Ajouter un article', + 'grocery.addItemPlaceholder': 'Ajouter un article...', + 'grocery.loadingItems': 'Chargement des articles...', + 'grocery.empty': 'Pas encore d\'articles — ajoutez-en un ci-dessus', + 'grocery.items': 'Articles ({count})', + 'grocery.completed': 'Terminés ({count})', + 'grocery.deleteItem': 'Supprimer l\'article', + 'grocery.haList': '{name} (HA)', + + // ---- card picker modal ---- + 'cardPicker.title': 'Ajouter une carte', + 'cardPicker.empty': 'Aucun type de carte disponible pour cette zone.', + + // ---- card config modal (common) ---- + 'cardConfig.entitiesPlaceholder': 'Entités', + + // ---- view name modal ---- + 'viewName.name': 'Nom', + + // ---- onboarding ---- + 'onboarding.welcome': 'Bienvenue sur Beacon', + 'onboarding.welcomeDesc': 'Connectez votre Home Assistant pour commencer.', + 'onboarding.getStarted': 'Commencer', + 'onboarding.connectTitle': 'Se connecter à Home Assistant', + 'onboarding.connectDesc': 'Entrez l\'URL de votre instance Home Assistant.', + 'onboarding.haUrl': 'URL de Home Assistant', + 'onboarding.haUrlPlaceholder': 'https://homeassistant.local:8123', + 'onboarding.connect': 'Connecter', + 'onboarding.connecting': 'Connexion en cours...', + 'onboarding.authTitle': 'Authentification', + 'onboarding.authDesc': 'Choisissez comment vous authentifier avec Home Assistant.', + 'onboarding.tokenMethod': 'Jeton d\'accès de longue durée', + 'onboarding.tokenMethodDesc': 'Utilisez un jeton généré depuis votre profil Home Assistant.', + 'onboarding.tokenPlaceholder': 'Collez votre jeton ici', + 'onboarding.tokenHint': 'Comment créer un jeton :', + 'onboarding.tokenHintDetail': 'Allez dans votre Profil HA → Jetons de longue durée → Créer un jeton', + 'onboarding.oauthMethod': 'Se connecter avec Home Assistant', + 'onboarding.oauthMethodDesc': 'Authentifiez-vous en utilisant vos identifiants Home Assistant via OAuth2.', + 'onboarding.openHA': 'Ouvrir Home Assistant', + 'onboarding.completeSetup': 'Terminer la configuration', + 'onboarding.urlError': 'L\'URL doit commencer par http:// ou https://', + 'onboarding.connectionFailed': 'Échec de la connexion (HTTP {status}). Vérifiez l\'URL et réessayez.', + 'onboarding.notReachable': 'Impossible d\'atteindre Home Assistant. Vérifiez l\'URL et assurez-vous qu\'elle est accessible.', + 'onboarding.tokenRequired': 'Veuillez entrer votre jeton d\'accès.', }; diff --git a/src/i18n/translations/it.ts b/src/i18n/translations/it.ts index 5fb614f9..b70f375e 100644 --- a/src/i18n/translations/it.ts +++ b/src/i18n/translations/it.ts @@ -431,4 +431,108 @@ export const it: Record = { 'photoFrame.pauseSlideshow': 'Metti in pausa la presentazione', 'photoFrame.resumeSlideshow': 'Riprendi la presentazione', 'photoFrame.nextPhoto': 'Foto successiva', + + // ---- chores view ---- + 'chores.title': 'Faccende', + 'chores.addChore': 'Aggiungi faccenda', + 'chores.editChore': 'Modifica faccenda', + 'chores.newChore': 'Nuova faccenda', + 'chores.claim': 'Rivendica', + 'chores.complete': 'Completa', + 'chores.whoClaims': 'Chi rivendica questo?', + 'chores.whoCompleted': 'Chi ha completato questo?', + 'chores.editChoreAria': 'Modifica {name}', + 'chores.deleteChoreAria': 'Elimina {name}', + 'chores.name': 'Nome', + 'chores.value': 'Valore', + 'chores.frequency': 'Frequenza', + 'chores.assignedTo': 'Assegnato a', + 'chores.assignTo': 'Assegna a', + 'chores.icon': 'Icona', + 'chores.daily': 'Giornaliero', + 'chores.weekly': 'Settimanale', + 'chores.monthly': 'Mensile', + 'chores.oneTime': 'Una volta', + 'chores.namePlaceholder': 'es., Aspirare il soggiorno', + 'chores.anyone': 'Chiunque', + 'chores.empty': 'Nessuna faccenda ancora — aggiungine una', + 'chores.addMembersFirst': 'Aggiungi prima i membri della famiglia per assegnare le faccende.', + 'chores.openChores': 'Faccende aperte', + 'chores.noChoresAssigned': 'Nessuna faccenda assegnata', + 'chores.addChoreFor': 'Aggiungi faccenda per {name}', + 'chores.saveChanges': 'Salva modifiche', + + // ---- leaderboard ---- + 'leaderboard.title': 'Classifica', + 'leaderboard.thisWeek': 'Questa settimana', + 'leaderboard.thisMonth': 'Questo mese', + 'leaderboard.empty': 'Aggiungi membri della famiglia e faccende per vedere la classifica.', + 'leaderboard.choresCompleted': '{count} faccende', + 'leaderboard.choreCompletedSingular': '{count} faccenda', + + // ---- streak badge ---- + 'streak.longestTooltip': 'Serie più lunga: {count} giorni', + 'streak.best': 'Migliore:', + + // ---- music view ---- + 'music.noPlayers': 'Nessun lettore multimediale', + 'music.noPlayersDesc': 'Collega un lettore multimediale in Home Assistant per controllare la musica da qui.', + 'music.selectPlayer': 'Seleziona lettore', + 'music.nowPlaying': 'In riproduzione', + 'music.volume': 'Volume', + 'music.openMusicView': 'Apri vista musica', + 'music.previousTrack': 'Traccia precedente', + 'music.nextTrack': 'Traccia successiva', + 'music.play': 'Riproduci', + 'music.pause': 'Pausa', + 'music.mute': 'Muto', + 'music.unmute': 'Riattiva audio', + 'music.albumArt': 'Copertina album', + + // ---- grocery / lists view ---- + 'grocery.noListsAvailable': 'Nessuna lista disponibile', + 'grocery.addItem': 'Aggiungi articolo', + 'grocery.addItemPlaceholder': 'Aggiungi un articolo...', + 'grocery.loadingItems': 'Caricamento articoli...', + 'grocery.empty': 'Nessun articolo ancora — aggiungine uno sopra', + 'grocery.items': 'Articoli ({count})', + 'grocery.completed': 'Completati ({count})', + 'grocery.deleteItem': 'Elimina articolo', + 'grocery.haList': '{name} (HA)', + + // ---- card picker modal ---- + 'cardPicker.title': 'Aggiungi scheda', + 'cardPicker.empty': 'Nessun tipo di scheda disponibile per quest\'area.', + + // ---- card config modal (common) ---- + 'cardConfig.entitiesPlaceholder': 'Entità', + + // ---- view name modal ---- + 'viewName.name': 'Nome', + + // ---- onboarding ---- + 'onboarding.welcome': 'Benvenuto su Beacon', + 'onboarding.welcomeDesc': 'Collega il tuo Home Assistant per iniziare.', + 'onboarding.getStarted': 'Inizia', + 'onboarding.connectTitle': 'Collega a Home Assistant', + 'onboarding.connectDesc': 'Inserisci l\'URL della tua istanza Home Assistant.', + 'onboarding.haUrl': 'URL di Home Assistant', + 'onboarding.haUrlPlaceholder': 'https://homeassistant.local:8123', + 'onboarding.connect': 'Collega', + 'onboarding.connecting': 'Connessione in corso...', + 'onboarding.authTitle': 'Autenticazione', + 'onboarding.authDesc': 'Scegli come autenticarti con Home Assistant.', + 'onboarding.tokenMethod': 'Token di accesso a lungo termine', + 'onboarding.tokenMethodDesc': 'Usa un token generato dal tuo profilo Home Assistant.', + 'onboarding.tokenPlaceholder': 'Incolla il tuo token qui', + 'onboarding.tokenHint': 'Come creare un token:', + 'onboarding.tokenHintDetail': 'Vai al tuo Profilo HA → Token a lungo termine → Crea token', + 'onboarding.oauthMethod': 'Accedi con Home Assistant', + 'onboarding.oauthMethodDesc': 'Autenticati utilizzando le tue credenziali Home Assistant tramite OAuth2.', + 'onboarding.openHA': 'Apri Home Assistant', + 'onboarding.completeSetup': 'Completa configurazione', + 'onboarding.urlError': 'L\'URL deve iniziare con http:// o https://', + 'onboarding.connectionFailed': 'Connessione fallita (HTTP {status}). Controlla l\'URL e riprova.', + 'onboarding.notReachable': 'Impossibile raggiungere Home Assistant. Verifica l\'URL e assicurati che sia accessibile.', + 'onboarding.tokenRequired': 'Inserisci il tuo token di accesso.', }; diff --git a/src/i18n/translations/ja.ts b/src/i18n/translations/ja.ts index 9ed214b8..d22a8a87 100644 --- a/src/i18n/translations/ja.ts +++ b/src/i18n/translations/ja.ts @@ -431,4 +431,108 @@ export const ja: Record = { 'photoFrame.pauseSlideshow': 'スライドショーを一時停止', 'photoFrame.resumeSlideshow': 'スライドショーを再開', 'photoFrame.nextPhoto': '次の写真', + + // ---- chores view ---- + 'chores.title': '家事', + 'chores.addChore': '家事を追加', + 'chores.editChore': '家事を編集', + 'chores.newChore': '新しい家事', + 'chores.claim': '引き受ける', + 'chores.complete': '完了する', + 'chores.whoClaims': '誰が引き受けますか?', + 'chores.whoCompleted': '誰が完了しましたか?', + 'chores.editChoreAria': '{name}を編集', + 'chores.deleteChoreAria': '{name}を削除', + 'chores.name': '名前', + 'chores.value': '価値', + 'chores.frequency': '頻度', + 'chores.assignedTo': '担当者', + 'chores.assignTo': '割り当て先', + 'chores.icon': 'アイコン', + 'chores.daily': '毎日', + 'chores.weekly': '毎週', + 'chores.monthly': '毎月', + 'chores.oneTime': '一回のみ', + 'chores.namePlaceholder': '例:リビングを掃除機で掃除', + 'chores.anyone': '誰でも', + 'chores.empty': 'まだ家事がありません — 追加してください', + 'chores.addMembersFirst': '家事を割り当てるには、まず家族を追加してください。', + 'chores.openChores': '未完了の家事', + 'chores.noChoresAssigned': '割り当てられた家事はありません', + 'chores.addChoreFor': '{name}の家事を追加', + 'chores.saveChanges': '変更を保存', + + // ---- leaderboard ---- + 'leaderboard.title': 'リーダーボード', + 'leaderboard.thisWeek': '今週', + 'leaderboard.thisMonth': '今月', + 'leaderboard.empty': '家族と家事を追加して、リーダーボードを表示してください。', + 'leaderboard.choresCompleted': '{count}件の家事', + 'leaderboard.choreCompletedSingular': '{count}件の家事', + + // ---- streak badge ---- + 'streak.longestTooltip': '最長連続記録:{count}日', + 'streak.best': '最高:', + + // ---- music view ---- + 'music.noPlayers': 'メディアプレーヤーがありません', + 'music.noPlayersDesc': 'Home Assistantでメディアプレーヤーを接続して、ここから音楽を制御してください。', + 'music.selectPlayer': 'プレーヤーを選択', + 'music.nowPlaying': '再生中', + 'music.volume': '音量', + 'music.openMusicView': '音楽ビューを開く', + 'music.previousTrack': '前のトラック', + 'music.nextTrack': '次のトラック', + 'music.play': '再生', + 'music.pause': '一時停止', + 'music.mute': 'ミュート', + 'music.unmute': 'ミュート解除', + 'music.albumArt': 'アルバムアート', + + // ---- grocery / lists view ---- + 'grocery.noListsAvailable': '利用可能なリストがありません', + 'grocery.addItem': 'アイテムを追加', + 'grocery.addItemPlaceholder': 'アイテムを追加...', + 'grocery.loadingItems': 'アイテムを読み込み中...', + 'grocery.empty': 'まだアイテムがありません — 上に追加してください', + 'grocery.items': 'アイテム({count})', + 'grocery.completed': '完了済み({count})', + 'grocery.deleteItem': 'アイテムを削除', + 'grocery.haList': '{name}(HA)', + + // ---- card picker modal ---- + 'cardPicker.title': 'カードを追加', + 'cardPicker.empty': 'このエリアに利用可能なカードタイプはありません。', + + // ---- card config modal (common) ---- + 'cardConfig.entitiesPlaceholder': 'エンティティ', + + // ---- view name modal ---- + 'viewName.name': '名前', + + // ---- onboarding ---- + 'onboarding.welcome': 'Beaconへようこそ', + 'onboarding.welcomeDesc': 'Home Assistantを接続して始めましょう。', + 'onboarding.getStarted': '始める', + 'onboarding.connectTitle': 'Home Assistantに接続', + 'onboarding.connectDesc': 'Home AssistantインスタンスのURLを入力してください。', + 'onboarding.haUrl': 'Home Assistant URL', + 'onboarding.haUrlPlaceholder': 'https://homeassistant.local:8123', + 'onboarding.connect': '接続', + 'onboarding.connecting': '接続中...', + 'onboarding.authTitle': '認証', + 'onboarding.authDesc': 'Home Assistantへの認証方法を選択してください。', + 'onboarding.tokenMethod': '長期アクセストークン', + 'onboarding.tokenMethodDesc': 'Home Assistantプロファイルから生成されたトークンを使用します。', + 'onboarding.tokenPlaceholder': 'トークンをここに貼り付けてください', + 'onboarding.tokenHint': 'トークンの作成方法:', + 'onboarding.tokenHintDetail': 'HAプロファイル → 長期トークン → トークンを作成', + 'onboarding.oauthMethod': 'Home Assistantでサインイン', + 'onboarding.oauthMethodDesc': 'OAuth2を介してHome Assistant認証情報を使用して認証します。', + 'onboarding.openHA': 'Home Assistantを開く', + 'onboarding.completeSetup': 'セットアップを完了', + 'onboarding.urlError': 'URLはhttp://またはhttps://で始まる必要があります', + 'onboarding.connectionFailed': '接続に失敗しました(HTTP {status})。URLを確認して再試行してください。', + 'onboarding.notReachable': 'Home Assistantに到達できませんでした。URLを確認し、アクセス可能であることを確認してください。', + 'onboarding.tokenRequired': 'アクセストークンを入力してください。', }; diff --git a/src/i18n/translations/ko.ts b/src/i18n/translations/ko.ts index c695306f..35d6cc93 100644 --- a/src/i18n/translations/ko.ts +++ b/src/i18n/translations/ko.ts @@ -431,4 +431,108 @@ export const ko: Record = { 'photoFrame.pauseSlideshow': '슬라이드쇼 일시정지', 'photoFrame.resumeSlideshow': '슬라이드쇼 재개', 'photoFrame.nextPhoto': '다음 사진', + + // ---- chores view ---- + 'chores.title': '집안일', + 'chores.addChore': '집안일 추가', + 'chores.editChore': '집안일 수정', + 'chores.newChore': '새 집안일', + 'chores.claim': '맡기', + 'chores.complete': '완료', + 'chores.whoClaims': '누가 이것을 맡나요?', + 'chores.whoCompleted': '누가 이것을 완료했나요?', + 'chores.editChoreAria': '{name} 수정', + 'chores.deleteChoreAria': '{name} 삭제', + 'chores.name': '이름', + 'chores.value': '가치', + 'chores.frequency': '빈도', + 'chores.assignedTo': '담당자', + 'chores.assignTo': '배정', + 'chores.icon': '아이콘', + 'chores.daily': '매일', + 'chores.weekly': '매주', + 'chores.monthly': '매월', + 'chores.oneTime': '한 번', + 'chores.namePlaceholder': '예: 거실 청소', + 'chores.anyone': '누구나', + 'chores.empty': '아직 집안일이 없습니다 — 추가하세요', + 'chores.addMembersFirst': '집안일을 배정하려면 먼저 가족 구성원을 추가하세요.', + 'chores.openChores': '미완료 집안일', + 'chores.noChoresAssigned': '배정된 집안일이 없습니다', + 'chores.addChoreFor': '{name}의 집안일 추가', + 'chores.saveChanges': '변경사항 저장', + + // ---- leaderboard ---- + 'leaderboard.title': '리더보드', + 'leaderboard.thisWeek': '이번 주', + 'leaderboard.thisMonth': '이번 달', + 'leaderboard.empty': '가족 구성원과 집안일을 추가하여 리더보드를 확인하세요.', + 'leaderboard.choresCompleted': '{count}개 집안일', + 'leaderboard.choreCompletedSingular': '{count}개 집안일', + + // ---- streak badge ---- + 'streak.longestTooltip': '최장 연속 기록: {count}일', + 'streak.best': '최고:', + + // ---- music view ---- + 'music.noPlayers': '미디어 플레이어 없음', + 'music.noPlayersDesc': 'Home Assistant에서 미디어 플레이어를 연결하여 여기에서 음악을 제어하세요.', + 'music.selectPlayer': '플레이어 선택', + 'music.nowPlaying': '재생 중', + 'music.volume': '볼륨', + 'music.openMusicView': '음악 보기 열기', + 'music.previousTrack': '이전 트랙', + 'music.nextTrack': '다음 트랙', + 'music.play': '재생', + 'music.pause': '일시 정지', + 'music.mute': '음소거', + 'music.unmute': '음소거 해제', + 'music.albumArt': '앨범 아트', + + // ---- grocery / lists view ---- + 'grocery.noListsAvailable': '사용 가능한 목록이 없습니다', + 'grocery.addItem': '항목 추가', + 'grocery.addItemPlaceholder': '항목 추가...', + 'grocery.loadingItems': '항목 로딩 중...', + 'grocery.empty': '아직 항목이 없습니다 — 위에 추가하세요', + 'grocery.items': '항목 ({count})', + 'grocery.completed': '완료 ({count})', + 'grocery.deleteItem': '항목 삭제', + 'grocery.haList': '{name} (HA)', + + // ---- card picker modal ---- + 'cardPicker.title': '카드 추가', + 'cardPicker.empty': '이 영역에 사용 가능한 카드 유형이 없습니다.', + + // ---- card config modal (common) ---- + 'cardConfig.entitiesPlaceholder': '엔티티', + + // ---- view name modal ---- + 'viewName.name': '이름', + + // ---- onboarding ---- + 'onboarding.welcome': 'Beacon에 오신 것을 환영합니다', + 'onboarding.welcomeDesc': 'Home Assistant를 연결하여 시작하세요.', + 'onboarding.getStarted': '시작하기', + 'onboarding.connectTitle': 'Home Assistant에 연결', + 'onboarding.connectDesc': 'Home Assistant 인스턴스의 URL을 입력하세요.', + 'onboarding.haUrl': 'Home Assistant URL', + 'onboarding.haUrlPlaceholder': 'https://homeassistant.local:8123', + 'onboarding.connect': '연결', + 'onboarding.connecting': '연결 중...', + 'onboarding.authTitle': '인증', + 'onboarding.authDesc': 'Home Assistant 인증 방법을 선택하세요.', + 'onboarding.tokenMethod': '장기 액세스 토큰', + 'onboarding.tokenMethodDesc': 'Home Assistant 프로필에서 생성된 토큰을 사용하세요.', + 'onboarding.tokenPlaceholder': '여기에 토큰을 붙여넣으세요', + 'onboarding.tokenHint': '토큰 생성 방법:', + 'onboarding.tokenHintDetail': 'HA 프로필 → 장기 토큰 → 토큰 생성', + 'onboarding.oauthMethod': 'Home Assistant로 로그인', + 'onboarding.oauthMethodDesc': 'OAuth2를 통해 Home Assistant 자격 증명을 사용하여 인증하세요.', + 'onboarding.openHA': 'Home Assistant 열기', + 'onboarding.completeSetup': '설정 완료', + 'onboarding.urlError': 'URL은 http:// 또는 https://로 시작해야 합니다', + 'onboarding.connectionFailed': '연결 실패 (HTTP {status}). URL을 확인하고 다시 시도하세요.', + 'onboarding.notReachable': 'Home Assistant에 연결할 수 없습니다. URL을 확인하고 접근 가능한지 확인하세요.', + 'onboarding.tokenRequired': '액세스 토큰을 입력하세요.', }; diff --git a/src/i18n/translations/nl.ts b/src/i18n/translations/nl.ts index 7ceb5248..865e410e 100644 --- a/src/i18n/translations/nl.ts +++ b/src/i18n/translations/nl.ts @@ -431,4 +431,108 @@ export const nl: Record = { 'photoFrame.pauseSlideshow': 'Diavoorstelling pauzeren', 'photoFrame.resumeSlideshow': 'Diavoorstelling hervatten', 'photoFrame.nextPhoto': 'Volgende foto', + + // ---- chores view ---- + 'chores.title': 'Karweitjes', + 'chores.addChore': 'Karweitje toevoegen', + 'chores.editChore': 'Karweitje bewerken', + 'chores.newChore': 'Nieuw karweitje', + 'chores.claim': 'Claimen', + 'chores.complete': 'Voltooien', + 'chores.whoClaims': 'Wie claimt dit?', + 'chores.whoCompleted': 'Wie heeft dit voltooid?', + 'chores.editChoreAria': '{name} bewerken', + 'chores.deleteChoreAria': '{name} verwijderen', + 'chores.name': 'Naam', + 'chores.value': 'Waarde', + 'chores.frequency': 'Frequentie', + 'chores.assignedTo': 'Toegewezen aan', + 'chores.assignTo': 'Toewijzen aan', + 'chores.icon': 'Pictogram', + 'chores.daily': 'Dagelijks', + 'chores.weekly': 'Wekelijks', + 'chores.monthly': 'Maandelijks', + 'chores.oneTime': 'Eenmalig', + 'chores.namePlaceholder': 'bijv. Woonkamer stofzuigen', + 'chores.anyone': 'Iedereen', + 'chores.empty': 'Nog geen karweitjes — voeg er een toe', + 'chores.addMembersFirst': 'Voeg eerst gezinsleden toe om karweitjes toe te wijzen.', + 'chores.openChores': 'Open karweitjes', + 'chores.noChoresAssigned': 'Geen karweitjes toegewezen', + 'chores.addChoreFor': 'Karweitje toevoegen voor {name}', + 'chores.saveChanges': 'Wijzigingen opslaan', + + // ---- leaderboard ---- + 'leaderboard.title': 'Klassement', + 'leaderboard.thisWeek': 'Deze week', + 'leaderboard.thisMonth': 'Deze maand', + 'leaderboard.empty': 'Voeg gezinsleden en karweitjes toe om het klassement te zien.', + 'leaderboard.choresCompleted': '{count} karweitjes', + 'leaderboard.choreCompletedSingular': '{count} karweitje', + + // ---- streak badge ---- + 'streak.longestTooltip': 'Langste reeks: {count} dagen', + 'streak.best': 'Beste:', + + // ---- music view ---- + 'music.noPlayers': 'Geen mediaspelers', + 'music.noPlayersDesc': 'Verbind een mediaspeler in Home Assistant om hier muziek te bedienen.', + 'music.selectPlayer': 'Speler selecteren', + 'music.nowPlaying': 'Nu bezig', + 'music.volume': 'Volume', + 'music.openMusicView': 'Muziekweergave openen', + 'music.previousTrack': 'Vorig nummer', + 'music.nextTrack': 'Volgend nummer', + 'music.play': 'Afspelen', + 'music.pause': 'Pauzeren', + 'music.mute': 'Dempen', + 'music.unmute': 'Geluid aan', + 'music.albumArt': 'Albumhoes', + + // ---- grocery / lists view ---- + 'grocery.noListsAvailable': 'Geen lijsten beschikbaar', + 'grocery.addItem': 'Item toevoegen', + 'grocery.addItemPlaceholder': 'Voeg een item toe...', + 'grocery.loadingItems': 'Items laden...', + 'grocery.empty': 'Nog geen items — voeg er een toe hierboven', + 'grocery.items': 'Items ({count})', + 'grocery.completed': 'Voltooid ({count})', + 'grocery.deleteItem': 'Item verwijderen', + 'grocery.haList': '{name} (HA)', + + // ---- card picker modal ---- + 'cardPicker.title': 'Kaart toevoegen', + 'cardPicker.empty': 'Geen kaarttypen beschikbaar voor dit gebied.', + + // ---- card config modal (common) ---- + 'cardConfig.entitiesPlaceholder': 'Entiteiten', + + // ---- view name modal ---- + 'viewName.name': 'Naam', + + // ---- onboarding ---- + 'onboarding.welcome': 'Welkom bij Beacon', + 'onboarding.welcomeDesc': 'Verbind je Home Assistant om te beginnen.', + 'onboarding.getStarted': 'Aan de slag', + 'onboarding.connectTitle': 'Verbinden met Home Assistant', + 'onboarding.connectDesc': 'Voer de URL van je Home Assistant-instantie in.', + 'onboarding.haUrl': 'Home Assistant URL', + 'onboarding.haUrlPlaceholder': 'https://homeassistant.local:8123', + 'onboarding.connect': 'Verbinden', + 'onboarding.connecting': 'Verbinden...', + 'onboarding.authTitle': 'Authenticatie', + 'onboarding.authDesc': 'Kies hoe je wilt authenticeren met Home Assistant.', + 'onboarding.tokenMethod': 'Langdurig toegangstoken', + 'onboarding.tokenMethodDesc': 'Gebruik een token gegenereerd vanuit je Home Assistant-profiel.', + 'onboarding.tokenPlaceholder': 'Plak je token hier', + 'onboarding.tokenHint': 'Hoe maak je een token:', + 'onboarding.tokenHintDetail': 'Ga naar je HA-profiel → Langdurige tokens → Token aanmaken', + 'onboarding.oauthMethod': 'Inloggen met Home Assistant', + 'onboarding.oauthMethodDesc': 'Authenticeer met je Home Assistant-referenties via OAuth2.', + 'onboarding.openHA': 'Home Assistant openen', + 'onboarding.completeSetup': 'Installatie voltooien', + 'onboarding.urlError': 'URL moet beginnen met http:// of https://', + 'onboarding.connectionFailed': 'Verbinding mislukt (HTTP {status}). Controleer de URL en probeer opnieuw.', + 'onboarding.notReachable': 'Kan Home Assistant niet bereiken. Controleer de URL en zorg ervoor dat het toegankelijk is.', + 'onboarding.tokenRequired': 'Voer je toegangstoken in.', }; diff --git a/src/i18n/translations/pt.ts b/src/i18n/translations/pt.ts index 06b8b67b..352a1af4 100644 --- a/src/i18n/translations/pt.ts +++ b/src/i18n/translations/pt.ts @@ -431,4 +431,108 @@ export const pt: Record = { 'photoFrame.pauseSlideshow': 'Pausar apresentação', 'photoFrame.resumeSlideshow': 'Retomar apresentação', 'photoFrame.nextPhoto': 'Próxima foto', + + // ---- chores view ---- + 'chores.title': 'Tarefas', + 'chores.addChore': 'Adicionar tarefa', + 'chores.editChore': 'Editar tarefa', + 'chores.newChore': 'Nova tarefa', + 'chores.claim': 'Reivindicar', + 'chores.complete': 'Concluir', + 'chores.whoClaims': 'Quem reivindica isto?', + 'chores.whoCompleted': 'Quem concluiu isto?', + 'chores.editChoreAria': 'Editar {name}', + 'chores.deleteChoreAria': 'Excluir {name}', + 'chores.name': 'Nome', + 'chores.value': 'Valor', + 'chores.frequency': 'Frequência', + 'chores.assignedTo': 'Atribuído a', + 'chores.assignTo': 'Atribuir a', + 'chores.icon': 'Ícone', + 'chores.daily': 'Diário', + 'chores.weekly': 'Semanal', + 'chores.monthly': 'Mensal', + 'chores.oneTime': 'Uma vez', + 'chores.namePlaceholder': 'ex: Aspirar a sala', + 'chores.anyone': 'Qualquer um', + 'chores.empty': 'Ainda não há tarefas — adicione uma', + 'chores.addMembersFirst': 'Adicione membros da família primeiro para atribuir tarefas.', + 'chores.openChores': 'Tarefas abertas', + 'chores.noChoresAssigned': 'Nenhuma tarefa atribuída', + 'chores.addChoreFor': 'Adicionar tarefa para {name}', + 'chores.saveChanges': 'Salvar alterações', + + // ---- leaderboard ---- + 'leaderboard.title': 'Classificação', + 'leaderboard.thisWeek': 'Esta semana', + 'leaderboard.thisMonth': 'Este mês', + 'leaderboard.empty': 'Adicione membros da família e tarefas para ver a classificação.', + 'leaderboard.choresCompleted': '{count} tarefas', + 'leaderboard.choreCompletedSingular': '{count} tarefa', + + // ---- streak badge ---- + 'streak.longestTooltip': 'Sequência mais longa: {count} dias', + 'streak.best': 'Melhor:', + + // ---- music view ---- + 'music.noPlayers': 'Nenhum reprodutor de mídia', + 'music.noPlayersDesc': 'Conecte um reprodutor de mídia no Home Assistant para controlar música aqui.', + 'music.selectPlayer': 'Selecionar reprodutor', + 'music.nowPlaying': 'Tocando agora', + 'music.volume': 'Volume', + 'music.openMusicView': 'Abrir visualização de música', + 'music.previousTrack': 'Faixa anterior', + 'music.nextTrack': 'Próxima faixa', + 'music.play': 'Reproduzir', + 'music.pause': 'Pausar', + 'music.mute': 'Silenciar', + 'music.unmute': 'Ativar som', + 'music.albumArt': 'Capa do álbum', + + // ---- grocery / lists view ---- + 'grocery.noListsAvailable': 'Nenhuma lista disponível', + 'grocery.addItem': 'Adicionar item', + 'grocery.addItemPlaceholder': 'Adicionar um item...', + 'grocery.loadingItems': 'Carregando itens...', + 'grocery.empty': 'Ainda não há itens — adicione um acima', + 'grocery.items': 'Itens ({count})', + 'grocery.completed': 'Concluídos ({count})', + 'grocery.deleteItem': 'Excluir item', + 'grocery.haList': '{name} (HA)', + + // ---- card picker modal ---- + 'cardPicker.title': 'Adicionar cartão', + 'cardPicker.empty': 'Nenhum tipo de cartão disponível para esta área.', + + // ---- card config modal (common) ---- + 'cardConfig.entitiesPlaceholder': 'Entidades', + + // ---- view name modal ---- + 'viewName.name': 'Nome', + + // ---- onboarding ---- + 'onboarding.welcome': 'Bem-vindo ao Beacon', + 'onboarding.welcomeDesc': 'Conecte seu Home Assistant para começar.', + 'onboarding.getStarted': 'Começar', + 'onboarding.connectTitle': 'Conectar ao Home Assistant', + 'onboarding.connectDesc': 'Digite a URL da sua instância do Home Assistant.', + 'onboarding.haUrl': 'URL do Home Assistant', + 'onboarding.haUrlPlaceholder': 'https://homeassistant.local:8123', + 'onboarding.connect': 'Conectar', + 'onboarding.connecting': 'Conectando...', + 'onboarding.authTitle': 'Autenticação', + 'onboarding.authDesc': 'Escolha como autenticar com o Home Assistant.', + 'onboarding.tokenMethod': 'Token de acesso de longa duração', + 'onboarding.tokenMethodDesc': 'Use um token gerado do seu perfil do Home Assistant.', + 'onboarding.tokenPlaceholder': 'Cole seu token aqui', + 'onboarding.tokenHint': 'Como criar um token:', + 'onboarding.tokenHintDetail': 'Vá para seu Perfil HA → Tokens de longa duração → Criar token', + 'onboarding.oauthMethod': 'Entrar com Home Assistant', + 'onboarding.oauthMethodDesc': 'Autentique usando suas credenciais do Home Assistant via OAuth2.', + 'onboarding.openHA': 'Abrir Home Assistant', + 'onboarding.completeSetup': 'Concluir configuração', + 'onboarding.urlError': 'URL deve começar com http:// ou https://', + 'onboarding.connectionFailed': 'Conexão falhou (HTTP {status}). Verifique a URL e tente novamente.', + 'onboarding.notReachable': 'Não foi possível alcançar o Home Assistant. Verifique a URL e certifique-se de que esteja acessível.', + 'onboarding.tokenRequired': 'Por favor, insira seu token de acesso.', }; From 82623a1d7691be964b156f12ae7d6e7bf8b143cb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 17 Oct 2026 14:32:28 +0000 Subject: [PATCH 2/2] fix(i18n): remove invalid placeholderKey from registry config CardConfigField type doesn't support placeholderKey property. Changed defaultConfig title to empty string instead of hardcoded "Entities". Signed-off-by: Cursor Agent Co-authored-by: Aaron Sachs --- beacon/src/components/cards/registry.tsx | 2 +- src/components/cards/registry.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/beacon/src/components/cards/registry.tsx b/beacon/src/components/cards/registry.tsx index 816f054b..d5ae6125 100644 --- a/beacon/src/components/cards/registry.tsx +++ b/beacon/src/components/cards/registry.tsx @@ -107,7 +107,7 @@ export const cardRegistry: Record = { defaultConfig: { title: '', subtitle: '', entity_ids: [] }, defaultSize: 'sm', configFields: [ - { type: 'text', key: 'title', labelKey: 'cards.common.titleLabel', placeholderKey: 'cardConfig.entitiesPlaceholder' }, + { type: 'text', key: 'title', labelKey: 'cards.common.titleLabel' }, { type: 'text', key: 'subtitle', labelKey: 'cards.common.subtitleLabel' }, { type: 'entity-list', key: 'entity_ids', labelKey: 'cards.common.entitiesLabel' }, ], diff --git a/src/components/cards/registry.tsx b/src/components/cards/registry.tsx index 816f054b..d5ae6125 100644 --- a/src/components/cards/registry.tsx +++ b/src/components/cards/registry.tsx @@ -107,7 +107,7 @@ export const cardRegistry: Record = { defaultConfig: { title: '', subtitle: '', entity_ids: [] }, defaultSize: 'sm', configFields: [ - { type: 'text', key: 'title', labelKey: 'cards.common.titleLabel', placeholderKey: 'cardConfig.entitiesPlaceholder' }, + { type: 'text', key: 'title', labelKey: 'cards.common.titleLabel' }, { type: 'text', key: 'subtitle', labelKey: 'cards.common.subtitleLabel' }, { type: 'entity-list', key: 'entity_ids', labelKey: 'cards.common.entitiesLabel' }, ],