feat: internationalize toolbar menu items and associated Modals

This commit is contained in:
yozuru 2025-04-20 04:30:45 +08:00
parent 10f59a55a6
commit e6dd34e31d
No known key found for this signature in database
36 changed files with 779 additions and 305 deletions

View file

@ -1,5 +1,5 @@
import { useEffect, useState, useRef, useCallback } from 'react'; import { useEffect, useState, useRef, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Modal } from '@/components/modal'; import { Modal } from '@/components/modal';
import { Slider } from '@/components/slider'; import { Slider } from '@/components/slider';
@ -14,15 +14,46 @@ interface Preset {
baseFrequency: number; baseFrequency: number;
beatFrequency: number; beatFrequency: number;
name: string; name: string;
translationKey: string;
} }
const presets: Preset[] = [ const presets: Preset[] = [
{ baseFrequency: 100, beatFrequency: 2, name: 'Delta (Deep Sleep) 2 Hz' }, {
{ baseFrequency: 100, beatFrequency: 5, name: 'Theta (Meditation) 5 Hz' }, baseFrequency: 100,
{ baseFrequency: 100, beatFrequency: 10, name: 'Alpha (Relaxation) 10 Hz' }, beatFrequency: 2,
{ baseFrequency: 100, beatFrequency: 20, name: 'Beta (Focus) 20 Hz' }, name: 'Delta (Deep Sleep) 2 Hz',
{ baseFrequency: 100, beatFrequency: 40, name: 'Gamma (Cognition) 40 Hz' }, translationKey: 'modals.generators.presets.delta',
{ baseFrequency: 440, beatFrequency: 10, name: 'Custom' }, },
{
baseFrequency: 100,
beatFrequency: 5,
name: 'Theta (Meditation) 5 Hz',
translationKey: 'modals.generators.presets.theta',
},
{
baseFrequency: 100,
beatFrequency: 10,
name: 'Alpha (Relaxation) 10 Hz',
translationKey: 'modals.generators.presets.alpha',
},
{
baseFrequency: 100,
beatFrequency: 20,
name: 'Beta (Focus) 20 Hz',
translationKey: 'modals.generators.presets.beta',
},
{
baseFrequency: 100,
beatFrequency: 40,
name: 'Gamma (Cognition) 40 Hz',
translationKey: 'modals.generators.presets.gamma',
},
{
baseFrequency: 440,
beatFrequency: 10,
name: 'Custom',
translationKey: 'modals.generators.presets.custom',
},
]; ];
function computeBinauralBeatOscillatorFrequencies( function computeBinauralBeatOscillatorFrequencies(
@ -36,6 +67,7 @@ function computeBinauralBeatOscillatorFrequencies(
} }
export function BinauralModal({ onClose, show }: BinauralProps) { export function BinauralModal({ onClose, show }: BinauralProps) {
const { t } = useTranslation();
const [baseFrequency, setBaseFrequency] = useState<number>(440); // Default to A4 note const [baseFrequency, setBaseFrequency] = useState<number>(440); // Default to A4 note
const [beatFrequency, setBeatFrequency] = useState<number>(10); // Default to 10 Hz difference const [beatFrequency, setBeatFrequency] = useState<number>(10); // Default to 10 Hz difference
const [volume, setVolume] = useState<number>(0.5); // Default volume at 50% const [volume, setVolume] = useState<number>(0.5); // Default volume at 50%
@ -145,15 +177,14 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
}, [selectedPreset]); }, [selectedPreset]);
const handlePresetChange = (e: React.ChangeEvent<HTMLSelectElement>) => { const handlePresetChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const selected = e.target.value; const selectedName = e.target.value;
setSelectedPreset(selected); setSelectedPreset(selectedName);
if (selected === 'Custom') { if (selectedName === 'Custom') {
// Allow user to input custom frequencies
return; return;
} }
const preset = presets.find(p => p.name === selected); const preset = presets.find(p => p.name === selectedName);
if (preset) { if (preset) {
setBaseFrequency(preset.baseFrequency); setBaseFrequency(preset.baseFrequency);
setBeatFrequency(preset.beatFrequency); setBeatFrequency(preset.beatFrequency);
@ -163,17 +194,17 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
return ( return (
<Modal show={show} onClose={onClose}> <Modal show={show} onClose={onClose}>
<header className={styles.header}> <header className={styles.header}>
<h2 className={styles.title}>Binaural Beat</h2> <h2 className={styles.title}>{t('modals.binaural.title')}</h2>
<p className={styles.desc}>Binaural beat generator.</p> <p className={styles.desc}>{t('modals.binaural.description')}</p>
</header> </header>
<div className={styles.fieldWrapper}> <div className={styles.fieldWrapper}>
<label> <label>
Presets: {t('modals.generators.presets-label')}
<select value={selectedPreset} onChange={handlePresetChange}> <select value={selectedPreset} onChange={handlePresetChange}>
{presets.map(preset => ( {presets.map(preset => (
<option key={preset.name} value={preset.name}> <option key={preset.name} value={preset.name}>
{preset.name} {t(preset.translationKey)}
</option> </option>
))} ))}
</select> </select>
@ -183,7 +214,7 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
<> <>
<div className={styles.fieldWrapper}> <div className={styles.fieldWrapper}>
<label> <label>
Base Frequency (Hz): {t('modals.generators.base-frequency-label')}
<input <input
max="1500" max="1500"
min="20" min="20"
@ -198,7 +229,7 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
</div> </div>
<div className={styles.fieldWrapper}> <div className={styles.fieldWrapper}>
<label> <label>
Beat Frequency (Hz): {t('modals.binaural.beat-frequency-label')}
<input <input
max="40" max="40"
min="0.1" min="0.1"
@ -213,9 +244,10 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
</div> </div>
</> </>
)} )}
<div className={styles.fieldWrapper}> <div className={styles.fieldWrapper}>
<label> <label>
Volume: {t('modals.generators.volume-label')}
<Slider <Slider
className={styles.volume} className={styles.volume}
max={1} max={1}
@ -232,10 +264,10 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
disabled={isPlaying} disabled={isPlaying}
onClick={startSound} onClick={startSound}
> >
Start {t('common.start')}
</button> </button>
<button disabled={!isPlaying} onClick={stopSound}> <button disabled={!isPlaying} onClick={stopSound}>
Stop {t('common.stop')}
</button> </button>
</div> </div>
</Modal> </Modal>

View file

@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import { Modal } from '@/components/modal'; import { Modal } from '@/components/modal';
import { Exercise } from './exercise'; import { Exercise } from './exercise';
@ -9,9 +10,12 @@ interface TimerProps {
} }
export function BreathingExerciseModal({ onClose, show }: TimerProps) { export function BreathingExerciseModal({ onClose, show }: TimerProps) {
const { t } = useTranslation(); // Get t function
return ( return (
<Modal show={show} onClose={onClose}> <Modal show={show} onClose={onClose}>
<h2 className={styles.title}>Breathing Exercise</h2> <h2 className={styles.title || 'modal-title'}>
{t('modals.breathing.title')}
</h2>
<Exercise /> <Exercise />
</Modal> </Modal>
); );

View file

@ -1,8 +1,7 @@
import { useState, useEffect, useMemo, useCallback } from 'react'; import { useState, useEffect, useMemo, useCallback } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { useTranslation } from 'react-i18next'; // Import
import { padNumber } from '@/helpers/number'; import { padNumber } from '@/helpers/number';
import styles from './exercise.module.css'; import styles from './exercise.module.css';
type Exercise = 'Box Breathing' | 'Resonant Breathing' | '4-7-8 Breathing'; type Exercise = 'Box Breathing' | 'Resonant Breathing' | '4-7-8 Breathing';
@ -17,21 +16,27 @@ const EXERCISE_PHASES: Record<Exercise, Phase[]> = {
const EXERCISE_DURATIONS: Record<Exercise, Partial<Record<Phase, number>>> = { const EXERCISE_DURATIONS: Record<Exercise, Partial<Record<Phase, number>>> = {
'4-7-8 Breathing': { exhale: 8, holdInhale: 7, inhale: 4 }, '4-7-8 Breathing': { exhale: 8, holdInhale: 7, inhale: 4 },
'Box Breathing': { exhale: 4, holdExhale: 4, holdInhale: 4, inhale: 4 }, 'Box Breathing': { exhale: 4, holdExhale: 4, holdInhale: 4, inhale: 4 },
'Resonant Breathing': { exhale: 5, inhale: 5 }, // No holdExhale 'Resonant Breathing': { exhale: 5, inhale: 5 },
}; };
const PHASE_LABELS: Record<Phase, string> = { const PHASE_LABEL_KEYS: Record<Phase, string> = {
exhale: 'Exhale', exhale: 'modals.breathing.phases.exhale',
holdExhale: 'Hold', holdExhale: 'modals.breathing.phases.hold',
holdInhale: 'Hold', holdInhale: 'modals.breathing.phases.hold',
inhale: 'Inhale', inhale: 'modals.breathing.phases.inhale',
}; };
const EXERCISE_SELECT_OPTIONS: { key: string; value: Exercise }[] = [
{ key: 'modals.breathing.exercises.478', value: '4-7-8 Breathing' },
{ key: 'modals.breathing.exercises.box', value: 'Box Breathing' },
{ key: 'modals.breathing.exercises.resonant', value: 'Resonant Breathing' },
];
export function Exercise() { export function Exercise() {
const { t } = useTranslation();
const [selectedExercise, setSelectedExercise] = const [selectedExercise, setSelectedExercise] =
useState<Exercise>('4-7-8 Breathing'); useState<Exercise>('4-7-8 Breathing');
const [phaseIndex, setPhaseIndex] = useState(0); const [phaseIndex, setPhaseIndex] = useState(0);
const phases = useMemo( const phases = useMemo(
() => EXERCISE_PHASES[selectedExercise], () => EXERCISE_PHASES[selectedExercise],
[selectedExercise], [selectedExercise],
@ -92,6 +97,8 @@ export function Exercise() {
return () => clearInterval(interval); return () => clearInterval(interval);
}, []); }, []);
const currentPhaseLabel = t(PHASE_LABEL_KEYS[currentPhase]);
return ( return (
<> <>
<div className={styles.exercise}> <div className={styles.exercise}>
@ -105,7 +112,7 @@ export function Exercise() {
key={selectedExercise} key={selectedExercise}
variants={animationVariants} variants={animationVariants}
/> />
<p className={styles.phase}>{PHASE_LABELS[currentPhase]}</p> <p className={styles.phase}>{currentPhaseLabel}</p>
</div> </div>
<div className={styles.selectWrapper}> <div className={styles.selectWrapper}>
@ -114,9 +121,9 @@ export function Exercise() {
value={selectedExercise} value={selectedExercise}
onChange={e => setSelectedExercise(e.target.value as Exercise)} onChange={e => setSelectedExercise(e.target.value as Exercise)}
> >
{Object.keys(EXERCISE_PHASES).map(exercise => ( {EXERCISE_SELECT_OPTIONS.map(option => (
<option key={exercise} value={exercise}> <option key={option.value} value={option.value}>
{exercise} {t(option.key)}
</option> </option>
))} ))}
</select> </select>

View file

@ -1,5 +1,5 @@
import { useEffect, useState, useRef, useCallback } from 'react'; import { useEffect, useState, useRef, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Modal } from '@/components/modal'; import { Modal } from '@/components/modal';
import { Slider } from '@/components/slider'; import { Slider } from '@/components/slider';
@ -14,18 +14,50 @@ interface Preset {
baseFrequency: number; baseFrequency: number;
beatFrequency: number; beatFrequency: number;
name: string; name: string;
translationKey: string;
} }
const presets: Preset[] = [ const presets: Preset[] = [
{ baseFrequency: 100, beatFrequency: 2, name: 'Delta (Deep Sleep) 2 Hz' }, {
{ baseFrequency: 100, beatFrequency: 5, name: 'Theta (Meditation) 5 Hz' }, baseFrequency: 100,
{ baseFrequency: 100, beatFrequency: 10, name: 'Alpha (Relaxation) 10 Hz' }, beatFrequency: 2,
{ baseFrequency: 100, beatFrequency: 20, name: 'Beta (Focus) 20 Hz' }, name: 'Delta (Deep Sleep) 2 Hz',
{ baseFrequency: 100, beatFrequency: 40, name: 'Gamma (Cognition) 40 Hz' }, translationKey: 'modals.generators.presets.delta',
{ baseFrequency: 440, beatFrequency: 10, name: 'Custom' }, },
{
baseFrequency: 100,
beatFrequency: 5,
name: 'Theta (Meditation) 5 Hz',
translationKey: 'modals.generators.presets.theta',
},
{
baseFrequency: 100,
beatFrequency: 10,
name: 'Alpha (Relaxation) 10 Hz',
translationKey: 'modals.generators.presets.alpha',
},
{
baseFrequency: 100,
beatFrequency: 20,
name: 'Beta (Focus) 20 Hz',
translationKey: 'modals.generators.presets.beta',
},
{
baseFrequency: 100,
beatFrequency: 40,
name: 'Gamma (Cognition) 40 Hz',
translationKey: 'modals.generators.presets.gamma',
},
{
baseFrequency: 440,
beatFrequency: 10,
name: 'Custom',
translationKey: 'modals.generators.presets.custom',
},
]; ];
export function IsochronicModal({ onClose, show }: IsochronicProps) { export function IsochronicModal({ onClose, show }: IsochronicProps) {
const { t } = useTranslation();
const [baseFrequency, setBaseFrequency] = useState<number>(440); // Default A4 note const [baseFrequency, setBaseFrequency] = useState<number>(440); // Default A4 note
const [beatFrequency, setBeatFrequency] = useState<number>(10); // Default 10 Hz beat const [beatFrequency, setBeatFrequency] = useState<number>(10); // Default 10 Hz beat
const [volume, setVolume] = useState<number>(0.5); // Default volume at 50% const [volume, setVolume] = useState<number>(0.5); // Default volume at 50%
@ -164,17 +196,17 @@ export function IsochronicModal({ onClose, show }: IsochronicProps) {
return ( return (
<Modal show={show} onClose={onClose}> <Modal show={show} onClose={onClose}>
<header className={styles.header}> <header className={styles.header}>
<h2 className={styles.title}>Isochronic Tone</h2> <h2 className={styles.title}>{t('modals.isochronic.title')}</h2>
<p className={styles.desc}>Isochronic tone generator.</p> <p className={styles.desc}>{t('modals.isochronic.description')}</p>
</header> </header>
<div className={styles.fieldWrapper}> <div className={styles.fieldWrapper}>
<label> <label>
Presets: {t('modals.generators.presets-label')} {/* Use common key */}
<select value={selectedPreset} onChange={handlePresetChange}> <select value={selectedPreset} onChange={handlePresetChange}>
{presets.map(preset => ( {presets.map(preset => (
<option key={preset.name} value={preset.name}> <option key={preset.name} value={preset.name}>
{preset.name} {t(preset.translationKey)}
</option> </option>
))} ))}
</select> </select>
@ -184,7 +216,8 @@ export function IsochronicModal({ onClose, show }: IsochronicProps) {
<> <>
<div className={styles.fieldWrapper}> <div className={styles.fieldWrapper}>
<label> <label>
Base Frequency (Hz): {t('modals.generators.base-frequency-label')}{' '}
{/* Use common key */}
<input <input
max="2000" max="2000"
min="20" min="20"
@ -199,7 +232,8 @@ export function IsochronicModal({ onClose, show }: IsochronicProps) {
</div> </div>
<div className={styles.fieldWrapper}> <div className={styles.fieldWrapper}>
<label> <label>
Tone Frequency (Hz): {t('modals.isochronic.tone-frequency-label')}{' '}
{/* Use isochronic specific key */}
<input <input
max="40" max="40"
min="0.1" min="0.1"
@ -230,7 +264,7 @@ export function IsochronicModal({ onClose, show }: IsochronicProps) {
)} )}
<div className={styles.fieldWrapper}> <div className={styles.fieldWrapper}>
<label> <label>
Volume: {t('modals.generators.volume-label')} {/* Use common key */}
<Slider <Slider
className={styles.volume} className={styles.volume}
max={1} max={1}
@ -241,16 +275,17 @@ export function IsochronicModal({ onClose, show }: IsochronicProps) {
/> />
</label> </label>
</div> </div>
<div className={styles.buttons}> <div className={styles.buttons}>
<button <button
className={styles.primary} className={styles.primary}
disabled={isPlaying} disabled={isPlaying}
onClick={startSound} onClick={startSound}
> >
Start {t('common.start')}
</button> </button>
<button disabled={!isPlaying} onClick={stopSound}> <button disabled={!isPlaying} onClick={stopSound}>
Stop {t('common.stop')}
</button> </button>
</div> </div>
</Modal> </Modal>

View file

@ -1,15 +1,17 @@
import { FaPlay, FaRegTrashAlt } from 'react-icons/fa/index'; import { FaPlay, FaRegTrashAlt } from 'react-icons/fa/index';
import { useTranslation } from 'react-i18next';
import styles from './list.module.css'; import styles from './list.module.css';
import { useSoundStore } from '@/stores/sound'; import { useSoundStore } from '@/stores/sound';
import { usePresetStore } from '@/stores/preset'; import { usePresetStore } from '@/stores/preset';
import { Tooltip } from '@/components/tooltip';
interface ListProps { interface ListProps {
close: () => void; close: () => void;
} }
export function List({ close }: ListProps) { export function List({ close }: ListProps) {
const { t } = useTranslation();
const presets = usePresetStore(state => state.presets); const presets = usePresetStore(state => state.presets);
const changeName = usePresetStore(state => state.changeName); const changeName = usePresetStore(state => state.changeName);
const deletePreset = usePresetStore(state => state.deletePreset); const deletePreset = usePresetStore(state => state.deletePreset);
@ -19,34 +21,57 @@ export function List({ close }: ListProps) {
return ( return (
<div className={styles.list}> <div className={styles.list}>
<h3 className={styles.title}> <h3 className={styles.title}>
Your Presets {presets.length > 0 && `(${presets.length})`} {t('modals.presets.your-presets-title')}{' '}
{presets.length > 0 && `(${presets.length})`}
</h3> </h3>
{!presets.length && ( {!presets.length && (
<p className={styles.empty}>You don&apos;t have any presets yet.</p> <p className={styles.empty}>{t('modals.presets.empty')}</p>
)} )}
{presets.map(preset => ( {presets.map(preset => (
<div className={styles.preset} key={preset.id}> <div className={styles.preset} key={preset.id}>
<input <input
placeholder="Untitled" placeholder={t('common.untitled')}
type="text" type="text"
value={preset.label} value={preset.label}
onChange={e => changeName(preset.id, e.target.value)} onChange={e => changeName(preset.id, e.target.value)}
/> />
<button onClick={() => deletePreset(preset.id)}> <Tooltip
<FaRegTrashAlt /> showDelay={0}
</button> content={
<button t('modals.presets.delete-button-tooltip') || 'Delete preset'
className={styles.primary} }
onClick={() => {
override(preset.sounds);
play();
close();
}}
> >
<FaPlay /> <button
</button> aria-label={
t('modals.presets.delete-button-aria-label') ||
`Delete preset ${preset.label || t('common.untitled')}`
}
onClick={() => deletePreset(preset.id)}
>
<FaRegTrashAlt />
</button>
</Tooltip>
<Tooltip
content={t('modals.presets.play-button-tooltip') || 'Play preset'}
showDelay={0}
>
<button
className={styles.primary}
aria-label={
t('modals.presets.play-button-aria-label') ||
`Play preset ${preset.label || t('common.untitled')}`
}
onClick={() => {
override(preset.sounds);
play();
close();
}}
>
<FaPlay />
</button>
</Tooltip>
</div> </div>
))} ))}
</div> </div>

View file

@ -1,5 +1,5 @@
import { useState, type FormEvent } from 'react'; import { useState, type FormEvent } from 'react';
import { useTranslation } from 'react-i18next';
import { cn } from '@/helpers/styles'; import { cn } from '@/helpers/styles';
import { useSoundStore } from '@/stores/sound'; import { useSoundStore } from '@/stores/sound';
import { usePresetStore } from '@/stores/preset'; import { usePresetStore } from '@/stores/preset';
@ -7,6 +7,7 @@ import { usePresetStore } from '@/stores/preset';
import styles from './new.module.css'; import styles from './new.module.css';
export function New() { export function New() {
const { t } = useTranslation();
const [name, setName] = useState(''); const [name, setName] = useState('');
const noSelected = useSoundStore(state => state.noSelected()); const noSelected = useSoundStore(state => state.noSelected());
@ -33,7 +34,7 @@ export function New() {
return ( return (
<div className={styles.new}> <div className={styles.new}>
<h3 className={styles.title}>New Preset</h3> <h3 className={styles.title}>{t('modals.presets.new-preset-title')}</h3>
<form <form
className={cn(styles.form, noSelected && styles.disabled)} className={cn(styles.form, noSelected && styles.disabled)}
@ -41,18 +42,18 @@ export function New() {
> >
<input <input
disabled={noSelected} disabled={noSelected}
placeholder="Preset's Name" placeholder={t('modals.presets.placeholder')}
required required
type="text" type="text"
value={name} value={name}
onChange={e => setName(e.target.value)} onChange={e => setName(e.target.value)}
/> />
<button disabled={noSelected}>Save</button> <button disabled={noSelected}>{t('common.save')}</button>
</form> </form>
{noSelected && ( {noSelected && (
<p className={styles.noSelected}> <p className={styles.noSelected}>
To make a preset, first select some sounds. {t('modals.presets.no-selected-warning')}
</p> </p>
)} )}
</div> </div>

View file

@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import { Modal } from '@/components/modal'; import { Modal } from '@/components/modal';
import { New } from './new'; import { New } from './new';
import { List } from './list'; import { List } from './list';
@ -10,9 +11,11 @@ interface PresetsModalProps {
} }
export function PresetsModal({ onClose, show }: PresetsModalProps) { export function PresetsModal({ onClose, show }: PresetsModalProps) {
const { t } = useTranslation();
return ( return (
<Modal show={show} onClose={onClose}> <Modal show={show} onClose={onClose}>
<h2 className={styles.title}>Presets</h2> <h2 className={styles.title}>{t('modals.presets.title')}</h2>
<New /> <New />
<div className={styles.divider} /> <div className={styles.divider} />
<List close={onClose} /> <List close={onClose} />

View file

@ -1,12 +1,13 @@
import { useMemo, useEffect, useState } from 'react'; import { useMemo, useEffect, useState } from 'react';
import { IoCopyOutline, IoCheckmark } from 'react-icons/io5/index'; import { IoCopyOutline, IoCheckmark } from 'react-icons/io5/index';
import { useTranslation } from 'react-i18next';
import { Modal } from '@/components/modal'; import { Modal } from '@/components/modal';
import { useCopy } from '@/hooks/use-copy'; import { useCopy } from '@/hooks/use-copy';
import { useSoundStore } from '@/stores/sound'; import { useSoundStore } from '@/stores/sound';
import styles from './share-link.module.css'; import styles from './share-link.module.css';
import { Tooltip } from '@/components/tooltip'; // Import Tooltip
interface ShareLinkModalProps { interface ShareLinkModalProps {
onClose: () => void; onClose: () => void;
@ -14,6 +15,7 @@ interface ShareLinkModalProps {
} }
export function ShareLinkModal({ onClose, show }: ShareLinkModalProps) { export function ShareLinkModal({ onClose, show }: ShareLinkModalProps) {
const { t } = useTranslation();
const [isMounted, setIsMounted] = useState(false); const [isMounted, setIsMounted] = useState(false);
const sounds = useSoundStore(state => state.sounds); const sounds = useSoundStore(state => state.sounds);
const { copy, copying } = useCopy(); const { copy, copying } = useCopy();
@ -51,16 +53,25 @@ export function ShareLinkModal({ onClose, show }: ShareLinkModalProps) {
return ( return (
<Modal show={show} onClose={onClose}> <Modal show={show} onClose={onClose}>
<h1 className={styles.heading}>Share your sound selection!</h1> <h1 className={styles.heading}>{t('modals.share-link.title')}</h1>
<p className={styles.desc}> <p className={styles.desc}>{t('modals.share-link.description')}</p>
Copy and send the following link to the person you want to share your
selection with.
</p>
<div className={styles.inputWrapper}> <div className={styles.inputWrapper}>
<input readOnly type="text" value={url} /> <input readOnly type="text" value={url} />
<button onClick={() => copy(url)}> <Tooltip
{copying ? <IoCheckmark /> : <IoCopyOutline />} content={copying ? t('common.copied') : t('common.copy')}
</button> showDelay={0}
>
<button
aria-label={
copying
? t('common.copied')
: t('modals.share-link.copy-button-aria-label')
}
onClick={() => copy(url)}
>
{copying ? <IoCheckmark /> : <IoCopyOutline />}
</button>
</Tooltip>
</div> </div>
</Modal> </Modal>
); );

View file

@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Modal } from '@/components/modal'; import { Modal } from '@/components/modal';
import { useSoundStore } from '@/stores/sound'; import { useSoundStore } from '@/stores/sound';
@ -11,14 +11,16 @@ import { sounds } from '@/data/sounds';
import styles from './shared.module.css'; import styles from './shared.module.css';
export function SharedModal() { export function SharedModal() {
const { t } = useTranslation(); // Get t function
const override = useSoundStore(state => state.override); const override = useSoundStore(state => state.override);
const showSnackbar = useSnackbar(); const showSnackbar = useSnackbar();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [sharedSounds, setSharedSounds] = useState< const [sharedSounds, setSharedSounds] = useState<
Array<{ Array<{
id: string; id: string;
label: string; labelKey: string;
volume: number; volume: number;
}> }>
>([]); >([]);
@ -30,26 +32,26 @@ export function SharedModal() {
if (share) { if (share) {
try { try {
const parsed = JSON.parse(decodeURIComponent(share)); const parsed = JSON.parse(decodeURIComponent(share));
const allSounds: Record<string, string> = {}; // Map sound IDs to their labelKeys for quick lookup
const allSoundLabelKeys: Record<string, string> = {};
sounds.categories.forEach(category => { sounds.categories.forEach(category => {
category.sounds.forEach(sound => { category.sounds.forEach(sound => {
allSounds[sound.id] = sound.label; allSoundLabelKeys[sound.id] = sound.labelKey; // Get the labelKey
}); });
}); });
const _sharedSounds: Array<{ const _sharedSounds: Array<{
id: string; id: string;
label: string; labelKey: string;
volume: number; volume: number;
}> = []; }> = [];
Object.keys(parsed).forEach(soundId => {
Object.keys(parsed).forEach(sound => { // Check if the soundId exists and has a labelKey
if (allSounds[sound]) { if (allSoundLabelKeys[soundId]) {
_sharedSounds.push({ _sharedSounds.push({
id: sound, id: soundId,
label: allSounds[sound], labelKey: allSoundLabelKeys[soundId], // Store the key
volume: Number(parsed[sound]), volume: Number(parsed[soundId]),
}); });
} }
}); });
@ -59,12 +61,13 @@ export function SharedModal() {
setSharedSounds(_sharedSounds); setSharedSounds(_sharedSounds);
} }
} catch (error) { } catch (error) {
return; console.error('Error parsing shared URL:', error); // Log error
return; // Stop execution if parsing fails
} finally { } finally {
history.pushState({}, '', location.href.split('?')[0]); history.pushState({}, '', location.href.split('?')[0]);
} }
} }
}, []); }, []); // Run only once on mount
const handleOverride = () => { const handleOverride = () => {
const newSounds: Record<string, number> = {}; const newSounds: Record<string, number> = {};
@ -75,34 +78,31 @@ export function SharedModal() {
override(newSounds); override(newSounds);
setIsOpen(false); setIsOpen(false);
showSnackbar('Done! You can now play the new selection.'); showSnackbar(t('modals.shared.snackbar-message'));
}; };
useCloseListener(() => setIsOpen(false)); useCloseListener(() => setIsOpen(false));
return ( return (
<Modal show={isOpen} onClose={() => setIsOpen(false)}> <Modal show={isOpen} onClose={() => setIsOpen(false)}>
<h1 className={styles.heading}>New sound mix detected!</h1> <h1 className={styles.heading}>{t('modals.shared.title')}</h1>
<p className={styles.desc}> <p className={styles.desc}>{t('modals.shared.description')}</p>
Someone has shared the following mix with you. Would you want to
override your current selection?
</p>
<div className={styles.sounds}> <div className={styles.sounds}>
{sharedSounds.map(sound => ( {sharedSounds.map(sound => (
<div className={styles.sound} key={sound.id}> <div className={styles.sound} key={sound.id}>
{sound.label} {t(sound.labelKey)}
</div> </div>
))} ))}
</div> </div>
<div className={styles.footer}> <div className={styles.footer}>
<button className={cn(styles.button)} onClick={() => setIsOpen(false)}> <button className={cn(styles.button)} onClick={() => setIsOpen(false)}>
Cancel {t('common.cancel')}
</button> </button>
<button <button
className={cn(styles.button, styles.primary)} className={cn(styles.button, styles.primary)}
onClick={handleOverride} onClick={handleOverride}
> >
Override {t('common.override')}
</button> </button>
</div> </div>
</Modal> </Modal>

View file

@ -1,69 +1,47 @@
import { Modal } from '@/components/modal'; // src/components/modals/shortcuts/shortcuts.tsx
import { useTranslation } from 'react-i18next';
import styles from './shortcuts.module.css'; import { Modal } from '@/components/modal'; // Assuming Modal component is stable
import styles from './shortcuts.module.css'; // Assuming styles are correct
interface ShortcutsModalProps { interface ShortcutsModalProps {
onClose: () => void; onClose: () => void; // Function to close the modal
show: boolean; show: boolean; // Boolean to control modal visibility
} }
interface ShortcutItem {
keys: string[];
labelKey: string;
}
const shortcutsList: ShortcutItem[] = [
{ keys: ['Shift', 'H'], labelKey: 'toolbar.items.shortcuts' }, // Reusing toolbar item key
{ keys: ['Shift', 'Alt', 'P'], labelKey: 'toolbar.items.presets' },
{ keys: ['Shift', 'S'], labelKey: 'toolbar.items.share' },
{ keys: ['Shift', 'Alt', 'T'], labelKey: 'toolbar.items.sleep-timer' },
{ keys: ['Shift', 'C'], labelKey: 'toolbar.items.countdown' },
{ keys: ['Shift', 'P'], labelKey: 'toolbar.items.pomodoro' },
{ keys: ['Shift', 'N'], labelKey: 'toolbar.items.notepad' },
{ keys: ['Shift', 'T'], labelKey: 'toolbar.items.todo' },
{ keys: ['Shift', 'B'], labelKey: 'toolbar.items.breathing' },
{ keys: ['Shift', 'Space'], labelKey: 'modals.shortcuts.labels.toggle-play' }, // Specific key
{ keys: ['Shift', 'R'], labelKey: 'modals.shortcuts.labels.unselect-all' }, // Specific key
];
export function ShortcutsModal({ onClose, show }: ShortcutsModalProps) { export function ShortcutsModal({ onClose, show }: ShortcutsModalProps) {
const shortcuts = [ const { t } = useTranslation();
{
keys: ['Shift', 'H'],
label: 'Shortcuts List',
},
{
keys: ['Shift', 'Alt', 'P'],
label: 'Presets',
},
{
keys: ['Shift', 'S'],
label: 'Share Sounds',
},
{
keys: ['Shift', 'Alt', 'T'],
label: 'Sleep Timer',
},
{
keys: ['Shift', 'C'],
label: 'Countdown Timer',
},
{
keys: ['Shift', 'P'],
label: 'Pomodoro',
},
{
keys: ['Shift', 'N'],
label: 'Notepad',
},
{
keys: ['Shift', 'T'],
label: 'Todo Checklist',
},
{
keys: ['Shift', 'B'],
label: 'Breathing Exercise',
},
{
keys: ['Shift', 'Space'],
label: 'Toggle Play',
},
{
keys: ['Shift', 'R'],
label: 'Unselect All Sounds',
},
];
return ( return (
<Modal show={show} onClose={onClose}> <Modal show={show} onClose={onClose}>
<h1 className={styles.heading}>Keyboard Shortcuts</h1> <h1 className={styles.heading}>{t('modals.shortcuts.title')}</h1>
<div className={styles.shortcuts}> <div className={styles.shortcuts}>
{shortcuts.map(shortcut => ( {shortcutsList.map(shortcut => (
// Render a Row for each shortcut item
// Use the labelKey as the React key for stability if IDs aren't available
<Row <Row
key={shortcut.label} key={shortcut.labelKey}
keys={shortcut.keys} keys={shortcut.keys}
label={shortcut.label} // Get the translated label using the defined labelKey
label={t(shortcut.labelKey)}
/> />
))} ))}
</div> </div>
@ -90,10 +68,13 @@ function Row({ keys, label }: RowProps) {
); );
} }
// Props for the Key component
interface KeyProps { interface KeyProps {
children: React.ReactNode; children: React.ReactNode; // The text content (key name)
} }
// Component to render a single keyboard key representation
function Key({ children }: KeyProps) { function Key({ children }: KeyProps) {
// Simple div with styling for a key
return <div className={styles.key}>{children}</div>; return <div className={styles.key}>{children}</div>;
} }

View file

@ -1,5 +1,5 @@
import { useEffect, useState, useRef, useMemo } from 'react'; import { useEffect, useState, useRef, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Modal } from '@/components/modal'; import { Modal } from '@/components/modal';
import { Timer } from './timer'; import { Timer } from './timer';
import { dispatch } from '@/lib/event'; import { dispatch } from '@/lib/event';
@ -15,7 +15,37 @@ interface SleepTimerModalProps {
show: boolean; show: boolean;
} }
interface FieldProps {
labelKey: string;
onChange: (value: string) => void;
value: string;
}
function Field({ labelKey, onChange, value }: FieldProps) {
const { t } = useTranslation();
const label = t(labelKey);
return (
<div className={styles.field}>
<label className={styles.label} htmlFor={labelKey}>
{' '}
{label}
</label>
<input
className={styles.input}
id={labelKey}
max="59"
min="0"
required
type="number"
value={value}
onChange={e => onChange(e.target.value === '' ? '' : e.target.value)}
/>
</div>
);
}
export function SleepTimerModal({ onClose, show }: SleepTimerModalProps) { export function SleepTimerModal({ onClose, show }: SleepTimerModalProps) {
const { t } = useTranslation();
const setActive = useSleepTimerStore(state => state.set); const setActive = useSleepTimerStore(state => state.set);
const noSelected = useSoundStore(state => state.noSelected()); const noSelected = useSoundStore(state => state.noSelected());
@ -91,21 +121,27 @@ export function SleepTimerModal({ onClose, show }: SleepTimerModalProps) {
return ( return (
<Modal show={show} onClose={onClose}> <Modal show={show} onClose={onClose}>
<header className={styles.header}> <header className={styles.header}>
<h2 className={styles.title}>Sleep Timer</h2> <h2 className={styles.title}>{t('modals.sleep-timer.title')}</h2>
<p className={styles.desc}> <p className={styles.desc}>{t('modals.sleep-timer.description')}</p>
Stop sounds after a certain amount of time.
</p>
</header> </header>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className={styles.controls}> <div className={styles.controls}>
<div className={styles.inputs}> <div className={styles.inputs}>
{!running && ( {!running && (
<Field label="Hours" value={hours} onChange={setHours} /> <Field
labelKey="modals.sleep-timer.hours-label"
value={hours}
onChange={setHours}
/>
)} )}
{!running && ( {!running && (
<Field label="Minutes" value={minutes} onChange={setMinutes} /> <Field
labelKey="modals.sleep-timer.minutes-label"
value={minutes}
onChange={setMinutes}
/>
)} )}
</div> </div>
@ -118,7 +154,7 @@ export function SleepTimerModal({ onClose, show }: SleepTimerModalProps) {
type="button" type="button"
onClick={handleReset} onClick={handleReset}
> >
Reset {t('common.reset')}
</button> </button>
)} )}
@ -127,7 +163,7 @@ export function SleepTimerModal({ onClose, show }: SleepTimerModalProps) {
className={cn(styles.button, styles.primary)} className={cn(styles.button, styles.primary)}
type="submit" type="submit"
> >
Start {t('common.start')}
</button> </button>
)} )}
</div> </div>
@ -136,29 +172,3 @@ export function SleepTimerModal({ onClose, show }: SleepTimerModalProps) {
</Modal> </Modal>
); );
} }
interface FieldProps {
label: string;
onChange: (value: string) => void;
value: string;
}
function Field({ label, onChange, value }: FieldProps) {
return (
<div className={styles.field}>
<label className={styles.label} htmlFor={label.toLocaleLowerCase()}>
{label}
</label>
<input
className={styles.input}
id={label.toLocaleLowerCase()}
max="59"
min="0"
required
type="number"
value={value}
onChange={e => onChange(e.target.value === '' ? '' : e.target.value)}
/>
</div>
);
}

View file

@ -1,5 +1,5 @@
import { FaHeadphonesAlt } from 'react-icons/fa/index'; import { FaHeadphonesAlt } from 'react-icons/fa/index';
import { useTranslation } from 'react-i18next';
import { Item } from '../item'; import { Item } from '../item';
interface BinauralProps { interface BinauralProps {
@ -7,7 +7,13 @@ interface BinauralProps {
} }
export function Binaural({ open }: BinauralProps) { export function Binaural({ open }: BinauralProps) {
const { t } = useTranslation();
return ( return (
<Item icon={<FaHeadphonesAlt />} label="Binaural Beats" onClick={open} /> <Item
icon={<FaHeadphonesAlt />}
label={t('toolbar.items.binaural')}
onClick={open}
/>
); );
} }

View file

@ -1,5 +1,5 @@
import { IoMdFlower } from 'react-icons/io/index'; import { IoMdFlower } from 'react-icons/io/index';
import { useTranslation } from 'react-i18next';
import { Item } from '../item'; import { Item } from '../item';
interface BreathingExerciseProps { interface BreathingExerciseProps {
@ -7,10 +7,12 @@ interface BreathingExerciseProps {
} }
export function BreathingExercise({ open }: BreathingExerciseProps) { export function BreathingExercise({ open }: BreathingExerciseProps) {
const { t } = useTranslation();
return ( return (
<Item <Item
icon={<IoMdFlower />} icon={<IoMdFlower />}
label="Breathing Exercise" label={t('toolbar.items.breathing')}
shortcut="Shift + B" shortcut="Shift + B"
onClick={open} onClick={open}
/> />

View file

@ -1,5 +1,5 @@
import { MdOutlineTimer } from 'react-icons/md/index'; import { MdOutlineTimer } from 'react-icons/md/index';
import { useTranslation } from 'react-i18next';
import { Item } from '../item'; import { Item } from '../item';
interface CountdownProps { interface CountdownProps {
@ -7,10 +7,12 @@ interface CountdownProps {
} }
export function Countdown({ open }: CountdownProps) { export function Countdown({ open }: CountdownProps) {
const { t } = useTranslation();
return ( return (
<Item <Item
icon={<MdOutlineTimer />} icon={<MdOutlineTimer />}
label="Countdown Timer" label={t('toolbar.items.countdown')}
shortcut="Shift + C" shortcut="Shift + C"
onClick={open} onClick={open}
/> />

View file

@ -1,13 +1,15 @@
import { SiBuymeacoffee } from 'react-icons/si/index'; import { SiBuymeacoffee } from 'react-icons/si/index';
import { useTranslation } from 'react-i18next';
import { Item } from '../item'; import { Item } from '../item';
export function Donate() { export function Donate() {
const { t } = useTranslation();
return ( return (
<Item <Item
href="https://buymeacoffee.com/remvze" href="https://buymeacoffee.com/remvze"
icon={<SiBuymeacoffee />} icon={<SiBuymeacoffee />} // Icon
label="Buy Me a Coffee" label={t('toolbar.items.buy-me-a-coffee')}
/> />
); );
} }

View file

@ -1,5 +1,5 @@
import { TbWaveSine } from 'react-icons/tb/index'; import { TbWaveSine } from 'react-icons/tb/index';
import { useTranslation } from 'react-i18next';
import { Item } from '../item'; import { Item } from '../item';
interface IsochronicProps { interface IsochronicProps {
@ -7,5 +7,13 @@ interface IsochronicProps {
} }
export function Isochronic({ open }: IsochronicProps) { export function Isochronic({ open }: IsochronicProps) {
return <Item icon={<TbWaveSine />} label="Isochronic Tones" onClick={open} />; const { t } = useTranslation(); // Get t function
return (
<Item
icon={<TbWaveSine />}
label={t('toolbar.items.isochronic')}
onClick={open}
/>
);
} }

View file

@ -1,5 +1,5 @@
import { MdNotes } from 'react-icons/md/index'; import { MdNotes } from 'react-icons/md/index';
import { useTranslation } from 'react-i18next';
import { Item } from '../item'; import { Item } from '../item';
import { useNoteStore } from '@/stores/note'; import { useNoteStore } from '@/stores/note';
@ -9,13 +9,14 @@ interface NotepadProps {
} }
export function Notepad({ open }: NotepadProps) { export function Notepad({ open }: NotepadProps) {
const { t } = useTranslation();
const note = useNoteStore(state => state.note); const note = useNoteStore(state => state.note);
return ( return (
<Item <Item
active={!!note.length} active={!!note.length}
icon={<MdNotes />} icon={<MdNotes />}
label="Notepad" label={t('toolbar.items.notepad')}
shortcut="Shift + N" shortcut="Shift + N"
onClick={open} onClick={open}
/> />

View file

@ -1,7 +1,6 @@
import { MdOutlineAvTimer } from 'react-icons/md/index'; import { MdOutlineAvTimer } from 'react-icons/md/index';
import { useTranslation } from 'react-i18next';
import { Item } from '../item'; import { Item } from '../item';
import { usePomodoroStore } from '@/stores/pomodoro'; import { usePomodoroStore } from '@/stores/pomodoro';
interface PomodoroProps { interface PomodoroProps {
@ -9,13 +8,14 @@ interface PomodoroProps {
} }
export function Pomodoro({ open }: PomodoroProps) { export function Pomodoro({ open }: PomodoroProps) {
const { t } = useTranslation();
const running = usePomodoroStore(state => state.running); const running = usePomodoroStore(state => state.running);
return ( return (
<Item <Item
active={running} active={running}
icon={<MdOutlineAvTimer />} icon={<MdOutlineAvTimer />}
label="Pomodoro" label={t('toolbar.items.pomodoro')}
shortcut="Shift + P" shortcut="Shift + P"
onClick={open} onClick={open}
/> />

View file

@ -1,5 +1,5 @@
import { RiPlayListFill } from 'react-icons/ri/index'; import { RiPlayListFill } from 'react-icons/ri/index';
import { useTranslation } from 'react-i18next';
import { Item } from '../item'; import { Item } from '../item';
interface PresetsProps { interface PresetsProps {
@ -7,10 +7,11 @@ interface PresetsProps {
} }
export function Presets({ open }: PresetsProps) { export function Presets({ open }: PresetsProps) {
const { t } = useTranslation();
return ( return (
<Item <Item
icon={<RiPlayListFill />} icon={<RiPlayListFill />}
label="Your Presets" label={t('toolbar.items.presets')}
shortcut="Shift + Alt + P" shortcut="Shift + Alt + P"
onClick={open} onClick={open}
/> />

View file

@ -1,5 +1,5 @@
import { IoShareSocialSharp } from 'react-icons/io5/index'; import { IoShareSocialSharp } from 'react-icons/io5/index';
import { useTranslation } from 'react-i18next';
import { Item } from '../item'; import { Item } from '../item';
import { useSoundStore } from '@/stores/sound'; import { useSoundStore } from '@/stores/sound';
@ -9,13 +9,14 @@ interface ShareProps {
} }
export function Share({ open }: ShareProps) { export function Share({ open }: ShareProps) {
const { t } = useTranslation();
const noSelected = useSoundStore(state => state.noSelected()); const noSelected = useSoundStore(state => state.noSelected());
return ( return (
<Item <Item
disabled={noSelected} disabled={noSelected}
icon={<IoShareSocialSharp />} icon={<IoShareSocialSharp />}
label="Share Sounds" label={t('toolbar.items.share')}
shortcut="Shift + S" shortcut="Shift + S"
onClick={open} onClick={open}
/> />

View file

@ -1,5 +1,5 @@
import { MdKeyboardCommandKey } from 'react-icons/md/index'; import { MdKeyboardCommandKey } from 'react-icons/md/index';
import { useTranslation } from 'react-i18next';
import { Item } from '../item'; import { Item } from '../item';
interface ShortcutsProps { interface ShortcutsProps {
@ -7,10 +7,12 @@ interface ShortcutsProps {
} }
export function Shortcuts({ open }: ShortcutsProps) { export function Shortcuts({ open }: ShortcutsProps) {
const { t } = useTranslation();
return ( return (
<Item <Item
icon={<MdKeyboardCommandKey />} icon={<MdKeyboardCommandKey />}
label="Shortcuts" label={t('toolbar.items.shortcuts')}
shortcut="Shift + H" shortcut="Shift + H"
onClick={open} onClick={open}
/> />

View file

@ -1,10 +1,10 @@
import { BiShuffle } from 'react-icons/bi/index'; import { BiShuffle } from 'react-icons/bi/index';
import { useTranslation } from 'react-i18next';
import { useSoundStore } from '@/stores/sound'; import { useSoundStore } from '@/stores/sound';
import { Item } from '../item'; import { Item } from '../item';
export function Shuffle() { export function Shuffle() {
const { t } = useTranslation();
const shuffle = useSoundStore(state => state.shuffle); const shuffle = useSoundStore(state => state.shuffle);
const locked = useSoundStore(state => state.locked); const locked = useSoundStore(state => state.locked);
@ -12,7 +12,7 @@ export function Shuffle() {
<Item <Item
disabled={locked} disabled={locked}
icon={<BiShuffle />} icon={<BiShuffle />}
label="Shuffle Sounds" label={t('toolbar.items.shuffle')}
onClick={shuffle} onClick={shuffle}
/> />
); );

View file

@ -1,5 +1,5 @@
import { IoMoonSharp } from 'react-icons/io5/index'; import { IoMoonSharp } from 'react-icons/io5/index';
import { useTranslation } from 'react-i18next';
import { useSleepTimerStore } from '@/stores/sleep-timer'; import { useSleepTimerStore } from '@/stores/sleep-timer';
import { Item } from '../item'; import { Item } from '../item';
@ -8,13 +8,14 @@ interface SleepTimerProps {
} }
export function SleepTimer({ open }: SleepTimerProps) { export function SleepTimer({ open }: SleepTimerProps) {
const { t } = useTranslation();
const active = useSleepTimerStore(state => state.active); const active = useSleepTimerStore(state => state.active);
return ( return (
<Item <Item
active={active} active={active}
icon={<IoMoonSharp />} icon={<IoMoonSharp />}
label="Sleep Timer" label={t('toolbar.items.sleep-timer')}
shortcut="Shift + Alt + T" shortcut="Shift + Alt + T"
onClick={open} onClick={open}
/> />

View file

@ -1,13 +1,15 @@
import { LuGithub } from 'react-icons/lu/index'; import { LuGithub } from 'react-icons/lu/index';
import { useTranslation } from 'react-i18next';
import { Item } from '../item'; import { Item } from '../item';
export function Source() { export function Source() {
const { t } = useTranslation();
return ( return (
<Item <Item
href="https://github.com/remvze/moodist" href="https://github.com/remvze/moodist"
icon={<LuGithub />} icon={<LuGithub />}
label="Source Code" label={t('toolbar.items.source-code')}
/> />
); );
} }

View file

@ -1,5 +1,5 @@
import { MdTaskAlt } from 'react-icons/md/index'; import { MdTaskAlt } from 'react-icons/md/index';
import { useTranslation } from 'react-i18next';
import { Item } from '../item'; import { Item } from '../item';
interface TodoProps { interface TodoProps {
@ -7,10 +7,12 @@ interface TodoProps {
} }
export function Todo({ open }: TodoProps) { export function Todo({ open }: TodoProps) {
const { t } = useTranslation();
return ( return (
<Item <Item
icon={<MdTaskAlt />} icon={<MdTaskAlt />}
label="Todo Checklist" label={t('toolbar.items.todo')}
shortcut="Shift + T" shortcut="Shift + T"
onClick={open} onClick={open}
/> />

View file

@ -3,7 +3,7 @@ import { IoMenu, IoClose } from 'react-icons/io5/index';
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { useTranslation } from 'react-i18next';
import { import {
ShuffleItem, ShuffleItem,
ShareItem, ShareItem,
@ -39,6 +39,7 @@ import { useCloseListener } from '@/hooks/use-close-listener';
import { closeModals } from '@/lib/modal'; import { closeModals } from '@/lib/modal';
export function Menu() { export function Menu() {
const { t } = useTranslation();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const noSelected = useSoundStore(state => state.noSelected()); const noSelected = useSoundStore(state => state.noSelected());
@ -100,7 +101,10 @@ export function Menu() {
<div className={styles.wrapper}> <div className={styles.wrapper}>
<DropdownMenu.Root open={isOpen} onOpenChange={o => setIsOpen(o)}> <DropdownMenu.Root open={isOpen} onOpenChange={o => setIsOpen(o)}>
<DropdownMenu.Trigger asChild> <DropdownMenu.Trigger asChild>
<button aria-label="Menu" className={styles.menuButton}> <button
aria-label={t('toolbar.menu-aria-label')}
className={styles.menuButton}
>
{isOpen ? <IoClose /> : <IoMenu />} {isOpen ? <IoClose /> : <IoMenu />}
</button> </button>
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
@ -143,7 +147,9 @@ export function Menu() {
<Divider /> <Divider />
<div className={styles.globalVolume}> <div className={styles.globalVolume}>
<label htmlFor="global-volume">Global Volume</label> <label htmlFor="global-volume">
{t('toolbar.global-volume-label')}
</label>
<Slider <Slider
max={100} max={100}
min={0} min={0}

View file

@ -1,5 +1,5 @@
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { Modal } from '@/components/modal'; import { Modal } from '@/components/modal';
import { useSoundEffect } from '@/hooks/use-sound-effect'; import { useSoundEffect } from '@/hooks/use-sound-effect';
@ -14,6 +14,7 @@ interface CountdownProps {
} }
export function Countdown({ onClose, show }: CountdownProps) { export function Countdown({ onClose, show }: CountdownProps) {
const { t } = useTranslation();
const [hours, setHours] = useState(0); const [hours, setHours] = useState(0);
const [minutes, setMinutes] = useState(0); const [minutes, setMinutes] = useState(0);
const [seconds, setSeconds] = useState(0); const [seconds, setSeconds] = useState(0);
@ -73,8 +74,8 @@ export function Countdown({ onClose, show }: CountdownProps) {
return ( return (
<Modal show={show} onClose={onClose}> <Modal show={show} onClose={onClose}>
<header className={styles.header}> <header className={styles.header}>
<h2 className={styles.title}>Countdown Timer</h2> <h2 className={styles.title}>{t('modals.countdown.title')}</h2>
<p className={styles.desc}>Super simple countdown timer.</p> <p className={styles.desc}>{t('modals.countdown.description')}</p>
</header> </header>
{isFormVisible ? ( {isFormVisible ? (
@ -82,21 +83,11 @@ export function Countdown({ onClose, show }: CountdownProps) {
<div className={styles.inputContainer}> <div className={styles.inputContainer}>
<input <input
className={styles.input} className={styles.input}
placeholder="HH" placeholder={t('modals.countdown.placeholder-hh') || 'HH'} // Placeholder
type="number" type="number"
value={hours} value={hours === 0 ? '' : hours} // Show empty if 0
onChange={e => setHours(Math.max(0, parseInt(e.target.value)))}
/>
<span>:</span>
<input
className={styles.input}
placeholder="MM"
type="number"
value={minutes}
onChange={e => onChange={e =>
setMinutes(Math.max(0, Math.min(59, parseInt(e.target.value)))) setHours(Math.max(0, parseInt(e.target.value || '0')))
} }
/> />
@ -104,21 +95,34 @@ export function Countdown({ onClose, show }: CountdownProps) {
<input <input
className={styles.input} className={styles.input}
placeholder="SS" placeholder={t('modals.countdown.placeholder-mm') || 'MM'}
type="number" type="number"
value={seconds} value={minutes === 0 ? '' : minutes} // Show empty if 0
onChange={e => onChange={e =>
setSeconds(Math.max(0, Math.min(59, parseInt(e.target.value)))) setMinutes(
Math.max(0, Math.min(59, parseInt(e.target.value || '0'))),
)
}
/>
<span>:</span>
<input
className={styles.input}
placeholder={t('modals.countdown.placeholder-ss') || 'SS'}
type="number"
value={seconds === 0 ? '' : seconds} // Show empty if 0
onChange={e =>
setSeconds(
Math.max(0, Math.min(59, parseInt(e.target.value || '0'))),
)
} }
/> />
</div> </div>
<div className={styles.buttonContainer}> <div className={styles.buttonContainer}>
<button <button
className={cn(styles.button, styles.primary)} className={cn(styles.button, styles.primary)}
onClick={handleStart} onClick={handleStart}
> >
Start {t('common.start')}
</button> </button>
</div> </div>
</div> </div>
@ -128,17 +132,15 @@ export function Countdown({ onClose, show }: CountdownProps) {
<p className={styles.reverse}>- {formatTime(elapsedTime)}</p> <p className={styles.reverse}>- {formatTime(elapsedTime)}</p>
<span>{formatTime(timeLeft)}</span> <span>{formatTime(timeLeft)}</span>
</div> </div>
<div className={styles.buttonContainer}> <div className={styles.buttonContainer}>
<button className={styles.button} onClick={handleBack}> <button className={styles.button} onClick={handleBack}>
Back {t('common.back')}
</button> </button>
<button <button
className={cn(styles.button, styles.primary)} className={cn(styles.button, styles.primary)}
onClick={toggleTimer} onClick={toggleTimer}
> >
{isActive ? 'Pause' : 'Start'} {isActive ? t('common.pause') : t('common.start')}
</button> </button>
</div> </div>
</div> </div>

View file

@ -3,7 +3,7 @@ import { BiTrash } from 'react-icons/bi/index';
import { LuCopy, LuDownload } from 'react-icons/lu/index'; import { LuCopy, LuDownload } from 'react-icons/lu/index';
import { FaCheck } from 'react-icons/fa6/index'; import { FaCheck } from 'react-icons/fa6/index';
import { FaUndo } from 'react-icons/fa/index'; import { FaUndo } from 'react-icons/fa/index';
import { useTranslation } from 'react-i18next';
import { Modal } from '@/components/modal'; import { Modal } from '@/components/modal';
import { Button } from './button'; import { Button } from './button';
@ -19,6 +19,7 @@ interface NotepadProps {
} }
export function Notepad({ onClose, show }: NotepadProps) { export function Notepad({ onClose, show }: NotepadProps) {
const { t } = useTranslation();
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
const note = useNoteStore(state => state.note); const note = useNoteStore(state => state.note);
@ -45,26 +46,42 @@ export function Notepad({ onClose, show }: NotepadProps) {
if (e.key === 'Escape') onClose(); if (e.key === 'Escape') onClose();
}; };
const counterOptions = {
chars: characters,
chars_plural:
characters !== 1 ? t('common.plural-suffix', { defaultValue: 's' }) : '',
words: words,
words_plural:
words !== 1 ? t('common.plural-suffix', { defaultValue: 's' }) : '',
};
const clearOrRestoreTooltip = history
? t('modals.notepad.restore-tooltip')
: t('modals.notepad.clear-tooltip');
const copyTooltip = copying
? t('common.copied')
: t('modals.notepad.copy-tooltip');
return ( return (
<Modal show={show} wide onClose={onClose}> <Modal show={show} wide onClose={onClose}>
<header className={styles.header}> <header className={styles.header}>
<h2 className={styles.label}>Your Note</h2> <h2 className={styles.label}>{t('modals.notepad.title-label')}</h2>
<div className={styles.buttons}> <div className={styles.buttons}>
<Button <Button
icon={copying ? <FaCheck /> : <LuCopy />} icon={copying ? <FaCheck /> : <LuCopy />}
tooltip="Copy Note" tooltip={copyTooltip}
onClick={() => copy(note)} onClick={() => copy(note)}
/> />
<Button <Button
icon={<LuDownload />} icon={<LuDownload />}
tooltip="Download Note" tooltip={t('modals.notepad.download-tooltip')}
onClick={() => download('Moodit Note.txt', note)} onClick={() => download('Moodist Note.txt', note)}
/> />
<Button <Button
critical={!history} critical={!history}
icon={history ? <FaUndo /> : <BiTrash />} icon={history ? <FaUndo /> : <BiTrash />}
recommended={!!history} recommended={!!history}
tooltip={history ? 'Restore Note' : 'Clear Note'} tooltip={clearOrRestoreTooltip}
onClick={() => (history ? restore() : clear())} onClick={() => (history ? restore() : clear())}
/> />
</div> </div>
@ -73,7 +90,7 @@ export function Notepad({ onClose, show }: NotepadProps) {
<textarea <textarea
className={styles.textarea} className={styles.textarea}
dir="auto" dir="auto"
placeholder="What is on your mind?" placeholder={t('modals.notepad.placeholder')}
ref={textareaRef} ref={textareaRef}
spellCheck={false} spellCheck={false}
value={note} value={note}
@ -82,8 +99,7 @@ export function Notepad({ onClose, show }: NotepadProps) {
/> />
<p className={styles.counter}> <p className={styles.counter}>
{characters} character{characters !== 1 && 's'} {words} word {t('modals.notepad.counter-stats', counterOptions)}
{words !== 1 && 's'}
</p> </p>
</Modal> </Modal>
); );

View file

@ -1,7 +1,7 @@
import { useState, useEffect, useRef, useMemo } from 'react'; import { useState, useEffect, useRef, useMemo } from 'react';
import { FaUndo, FaPlay, FaPause } from 'react-icons/fa/index'; import { FaUndo, FaPlay, FaPause } from 'react-icons/fa/index';
import { IoMdSettings } from 'react-icons/io/index'; import { IoMdSettings } from 'react-icons/io/index';
import { useTranslation } from 'react-i18next';
import { Modal } from '@/components/modal'; import { Modal } from '@/components/modal';
import { Button } from '../generics/button'; import { Button } from '../generics/button';
import { Timer } from './timer'; import { Timer } from './timer';
@ -12,7 +12,6 @@ import { useLocalStorage } from '@/hooks/use-local-storage';
import { useSoundEffect } from '@/hooks/use-sound-effect'; import { useSoundEffect } from '@/hooks/use-sound-effect';
import { usePomodoroStore } from '@/stores/pomodoro'; import { usePomodoroStore } from '@/stores/pomodoro';
import { useCloseListener } from '@/hooks/use-close-listener'; import { useCloseListener } from '@/hooks/use-close-listener';
import styles from './pomodoro.module.css'; import styles from './pomodoro.module.css';
interface PomodoroProps { interface PomodoroProps {
@ -22,6 +21,7 @@ interface PomodoroProps {
} }
export function Pomodoro({ onClose, open, show }: PomodoroProps) { export function Pomodoro({ onClose, open, show }: PomodoroProps) {
const { t } = useTranslation();
const [showSetting, setShowSetting] = useState(false); const [showSetting, setShowSetting] = useState(false);
const [selectedTab, setSelectedTab] = useState('pomodoro'); const [selectedTab, setSelectedTab] = useState('pomodoro');
@ -56,11 +56,11 @@ export function Pomodoro({ onClose, open, show }: PomodoroProps) {
const tabs = useMemo( const tabs = useMemo(
() => [ () => [
{ id: 'pomodoro', label: 'Pomodoro' }, { id: 'pomodoro', label: t('modals.pomodoro.tabs.pomodoro') },
{ id: 'short', label: 'Break' }, { id: 'short', label: t('modals.pomodoro.tabs.short-break') },
{ id: 'long', label: 'Long Break' }, { id: 'long', label: t('modals.pomodoro.tabs.long-break') },
], ],
[], [t],
); );
useCloseListener(() => setShowSetting(false)); useCloseListener(() => setShowSetting(false));
@ -123,12 +123,11 @@ export function Pomodoro({ onClose, open, show }: PomodoroProps) {
<> <>
<Modal show={show} onClose={onClose}> <Modal show={show} onClose={onClose}>
<header className={styles.header}> <header className={styles.header}>
<h2 className={styles.title}>Pomodoro Timer</h2> <h2 className={styles.title}>{t('modals.pomodoro.title')}</h2>
<div className={styles.button}> <div className={styles.button}>
<Button <Button
icon={<IoMdSettings />} icon={<IoMdSettings />}
tooltip="Change Times" tooltip={t('modals.pomodoro.settings-tooltip')}
onClick={() => { onClick={() => {
onClose(); onClose();
setShowSetting(true); setShowSetting(true);
@ -142,19 +141,21 @@ export function Pomodoro({ onClose, open, show }: PomodoroProps) {
<div className={styles.control}> <div className={styles.control}>
<p className={styles.completed}> <p className={styles.completed}>
{completions[selectedTab] || 0} completed {t('modals.pomodoro.completed', {
count: completions[selectedTab] || 0,
})}
</p> </p>
<div className={styles.buttons}> <div className={styles.buttons}>
<Button <Button
icon={<FaUndo />} icon={<FaUndo />}
smallIcon smallIcon
tooltip="Restart" tooltip={t('common.restart')}
onClick={restart} onClick={restart}
/> />
<Button <Button
icon={running ? <FaPause /> : <FaPlay />} icon={running ? <FaPause /> : <FaPlay />}
smallIcon smallIcon
tooltip={running ? 'Pause' : 'Start'} tooltip={running ? t('common.pause') : t('common.start')}
onClick={toggleRunning} onClick={toggleRunning}
/> />
</div> </div>

View file

@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Modal } from '@/components/modal'; import { Modal } from '@/components/modal';
import styles from './setting.module.css'; import styles from './setting.module.css';
@ -12,6 +12,7 @@ interface SettingProps {
} }
export function Setting({ onChange, onClose, show, times }: SettingProps) { export function Setting({ onChange, onClose, show, times }: SettingProps) {
const { t } = useTranslation();
const [values, setValues] = useState<Record<string, number | string>>(times); const [values, setValues] = useState<Record<string, number | string>>(times);
useEffect(() => { useEffect(() => {
@ -46,34 +47,34 @@ export function Setting({ onChange, onClose, show, times }: SettingProps) {
return ( return (
<Modal lockBody={false} show={show} onClose={onClose}> <Modal lockBody={false} show={show} onClose={onClose}>
<h2 className={styles.title}>Change Times</h2> <h2 className={styles.title}>{t('modals.pomodoro.settings.title')}</h2>
<form className={styles.form} onSubmit={handleSubmit}> <form className={styles.form} onSubmit={handleSubmit}>
<Field <Field
id="pomodoro" id="pomodoro"
label="Pomodoro" labelKey="modals.pomodoro.settings.pomodoro-label"
value={values.pomodoro} value={values.pomodoro}
onChange={handleChange('pomodoro')} onChange={handleChange('pomodoro')}
/> />
<Field <Field
id="short" id="short"
label="Short Break" labelKey="modals.pomodoro.settings.short-break-label"
value={values.short} value={values.short}
onChange={handleChange('short')} onChange={handleChange('short')}
/> />
<Field <Field
id="long" id="long"
label="Long Break" labelKey="modals.pomodoro.settings.long-break-label"
value={values.long} value={values.long}
onChange={handleChange('long')} onChange={handleChange('long')}
/> />
<div className={styles.buttons}> <div className={styles.buttons}>
<button type="button" onClick={handleCancel}> <button type="button" onClick={handleCancel}>
Cancel {t('common.cancel')}
</button> </button>
<button className={styles.primary} type="submit"> <button className={styles.primary} type="submit">
Save {t('common.save')}
</button> </button>
</div> </div>
</form> </form>
@ -83,19 +84,22 @@ export function Setting({ onChange, onClose, show, times }: SettingProps) {
interface FieldProps { interface FieldProps {
id: string; id: string;
label: string; labelKey: string;
onChange: (value: number | string) => void; onChange: (value: number | string) => void;
value: number | string; value: number | string;
} }
function Field({ id, label, onChange, value }: FieldProps) { function Field({ id, labelKey, onChange, value }: FieldProps) {
const { t } = useTranslation(); // 获取翻译函数
return ( return (
<div className={styles.field}> <div className={styles.field}>
<label className={styles.label} htmlFor={id}> <label className={styles.label} htmlFor={id}>
{label} <span>(minutes)</span> {t(labelKey)}{' '}
<span>({t('modals.pomodoro.settings.minutes-unit')})</span>{' '}
</label> </label>
<input <input
className={styles.input} className={styles.input}
id={id}
max={120} max={120}
min={1} min={1}
required required

View file

@ -1,10 +1,11 @@
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useTodoStore } from '@/stores/todo'; import { useTodoStore } from '@/stores/todo';
import styles from './form.module.css'; import styles from './form.module.css';
export function Form() { export function Form() {
const { t } = useTranslation();
const [value, setValue] = useState(''); const [value, setValue] = useState('');
const addTodo = useTodoStore(state => state.addTodo); const addTodo = useTodoStore(state => state.addTodo);
@ -22,12 +23,12 @@ export function Form() {
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className={styles.wrapper}> <div className={styles.wrapper}>
<input <input
placeholder="I have to ..." placeholder={t('modals.todo.add-placeholder')}
type="text" type="text"
value={value} value={value}
onChange={e => setValue(e.target.value)} onChange={e => setValue(e.target.value)}
/> />
<button type="submit">Add</button> <button type="submit">{t('modals.todo.add-button')}</button>
</div> </div>
</form> </form>
); );

View file

@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import { Modal } from '@/components/modal'; import { Modal } from '@/components/modal';
import { Form } from './form'; import { Form } from './form';
import { Todos } from './todos'; import { Todos } from './todos';
@ -10,11 +11,13 @@ interface TodoProps {
} }
export function Todo({ onClose, show }: TodoProps) { export function Todo({ onClose, show }: TodoProps) {
const { t } = useTranslation();
return ( return (
<Modal show={show} onClose={onClose}> <Modal show={show} onClose={onClose}>
<header className={styles.header}> <header className={styles.header}>
<h2 className={styles.title}>Todo Checklist</h2> <h2 className={styles.title}>{t('modals.todo.title')}</h2>
<p className={styles.desc}>Super simple todo list.</p> <p className={styles.desc}>{t('modals.todo.description')}</p>
</header> </header>
<Form /> <Form />

View file

@ -1,11 +1,12 @@
import { FaRegTrashAlt } from 'react-icons/fa/index'; import { FaRegTrashAlt } from 'react-icons/fa/index';
import { useTranslation } from 'react-i18next';
import { Checkbox } from '@/components/checkbox'; import { Checkbox } from '@/components/checkbox';
import { useTodoStore } from '@/stores/todo'; import { useTodoStore } from '@/stores/todo';
import { cn } from '@/helpers/styles'; import { cn } from '@/helpers/styles';
import styles from './todo.module.css'; import styles from './todo.module.css';
import { Tooltip } from '@/components/tooltip';
interface TodoProps { interface TodoProps {
done: boolean; done: boolean;
@ -14,6 +15,7 @@ interface TodoProps {
} }
export function Todo({ done, id, todo }: TodoProps) { export function Todo({ done, id, todo }: TodoProps) {
const { t } = useTranslation();
const deleteTodo = useTodoStore(state => state.deleteTodo); const deleteTodo = useTodoStore(state => state.deleteTodo);
const toggleTodo = useTodoStore(state => state.toggleTodo); const toggleTodo = useTodoStore(state => state.toggleTodo);
const editTodo = useTodoStore(state => state.editTodo); const editTodo = useTodoStore(state => state.editTodo);
@ -32,9 +34,17 @@ export function Todo({ done, id, todo }: TodoProps) {
value={todo} value={todo}
onChange={e => editTodo(id, e.target.value)} onChange={e => editTodo(id, e.target.value)}
/> />
<button className={styles.delete} onClick={handleDelete}> <Tooltip content={t('common.delete')} showDelay={0}>
<FaRegTrashAlt /> <button
</button> className={styles.delete}
aria-label={
t('modals.todo.delete-button-aria-label') || `Delete todo: ${todo}`
}
onClick={handleDelete}
>
<FaRegTrashAlt />
</button>
</Tooltip>
</div> </div>
); );
} }

View file

@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import { Todo } from './todo'; import { Todo } from './todo';
import { useTodoStore } from '@/stores/todo'; import { useTodoStore } from '@/stores/todo';
@ -5,13 +6,14 @@ import { useTodoStore } from '@/stores/todo';
import styles from './todos.module.css'; import styles from './todos.module.css';
export function Todos() { export function Todos() {
const { t } = useTranslation();
const todos = useTodoStore(state => state.todos); const todos = useTodoStore(state => state.todos);
const doneCount = useTodoStore(state => state.doneCount()); const doneCount = useTodoStore(state => state.doneCount());
return ( return (
<div className={styles.todos}> <div className={styles.todos}>
<header> <header>
<p className={styles.label}>Your Todos</p> <p className={styles.label}>{t('modals.todo.your-todos-label')}</p>
<div className={styles.divider} /> <div className={styles.divider} />
<p className={styles.counter}> <p className={styles.counter}>
{doneCount} / {todos.length} {doneCount} / {todos.length}
@ -30,7 +32,7 @@ export function Todos() {
))} ))}
</> </>
) : ( ) : (
<p className={styles.empty}>You don&apos;t have any todos.</p> <p className={styles.empty}>{t('modals.todo.empty')}</p>
)} )}
</div> </div>
); );

View file

@ -8,11 +8,156 @@
"en": "English", "en": "English",
"zh": "简体中文" "zh": "简体中文"
}, },
"common": {
"save": "Save",
"untitled": "Untitled",
"copy": "Copy",
"copied": "Copied!",
"override": "Override",
"done": "Done!",
"reset": "Reset",
"start": "Start",
"pause": "Pause",
"stop": "Stop",
"back": "Back",
"restart": "Restart",
"cancel": "Cancel",
"delete": "Delete",
"plural-suffix": "s"
},
"toolbar": {
"menu-aria-label": "Menu",
"global-volume-label": "Global Volume",
"items": {
"presets": "Your Presets",
"share": "Share Sounds",
"shuffle": "Shuffle Sounds",
"sleep-timer": "Sleep Timer",
"countdown": "Countdown Timer",
"pomodoro": "Pomodoro",
"notepad": "Notepad",
"todo": "Todo Checklist",
"breathing": "Breathing Exercise",
"binaural": "Binaural Beats",
"isochronic": "Isochronic Tones",
"shortcuts": "Shortcuts",
"buy-me-a-coffee": "Buy Me a Coffee",
"source-code": "Source Code"
}
},
"modals": { "modals": {
"reload": { "presets": {
"title": "New Content", "title": "Presets",
"description": "New content available, click on reload button to update.", "your-presets-title": "Your Presets",
"reloadButton": "Reload" "empty": "You don't have any presets yet.",
"new-preset-title": "New Preset",
"placeholder": "Preset's Name",
"play-button-tooltip": "Play preset",
"play-button-aria-label": "Play preset {{label}}",
"delete-button-tooltip": "Delete preset",
"delete-button-aria-label": "Delete preset {{label}}",
"no-selected-warning": "To make a preset, first select some sounds."
},
"share-link": {
"title": "Share your sound selection!",
"description": "Copy and send the following link to the person you want to share your selection with.",
"copy-button-aria-label": "Copy link"
},
"shared": {
"title": "New sound mix detected!",
"description": "Someone has shared the following mix with you. Would you want to override your current selection?",
"snackbar-message": "Done! You can now play the new selection."
},
"sleep-timer": {
"title": "Sleep Timer",
"description": "Stop sounds after a certain amount of time.",
"hours-label": "Hours",
"minutes-label": "Minutes"
},
"countdown": {
"title": "Countdown Timer",
"description": "Super simple countdown timer.",
"placeholder-hh": "HH",
"placeholder-mm": "MM",
"placeholder-ss": "SS"
},
"pomodoro": {
"title": "Pomodoro Timer",
"settings-tooltip": "Change Times",
"completed": "{{count}} completed",
"tabs": {
"pomodoro": "Pomodoro",
"short-break": "Break",
"long-break": "Long Break"
},
"settings": {
"title": "Change Times",
"pomodoro-label": "Pomodoro",
"short-break-label": "Short Break",
"long-break-label": "Long Break",
"minutes-unit": "minutes"
}
},
"notepad": {
"title-label": "Your Note",
"copy-tooltip": "Copy Note",
"download-tooltip": "Download Note",
"clear-tooltip": "Clear Note",
"restore-tooltip": "Restore Note",
"placeholder": "What is on your mind?",
"counter-stats": "{{chars}} character{{chars_plural}} • {{words}} word{{words_plural}}"
},
"todo": {
"title": "Todo Checklist",
"description": "Super simple todo list.",
"add-placeholder": "I have to ...",
"add-button": "Add",
"your-todos-label": "Your Todos",
"empty": "You don't have any todos.",
"delete-button-aria-label": "Delete todo"
},
"breathing": {
"title": "Breathing Exercise",
"phases": {
"inhale": "Inhale",
"exhale": "Exhale",
"hold": "Hold"
},
"exercises": {
"box": "Box Breathing",
"resonant": "Resonant Breathing",
"478": "4-7-8 Breathing"
}
},
"generators": {
"presets-label": "Presets:",
"base-frequency-label": "Base Frequency (Hz):",
"volume-label": "Volume:",
"presets": {
"delta": "Delta (Deep Sleep) 2 Hz",
"theta": "Theta (Meditation) 5 Hz",
"alpha": "Alpha (Relaxation) 10 Hz",
"beta": "Beta (Focus) 20 Hz",
"gamma": "Gamma (Cognition) 40 Hz",
"custom": "Custom"
}
},
"binaural": {
"title": "Binaural Beat",
"description": "Binaural beat generator.",
"beat-frequency-label": "Beat Frequency (Hz):"
},
"isochronic": {
"title": "Isochronic Tone",
"description": "Isochronic tone generator.",
"tone-frequency-label": "Tone Frequency (Hz):"
},
"shortcuts": {
"title": "Keyboard Shortcuts",
"labels": {
"toggle-play": "Toggle Play",
"unselect-all": "Unselect All Sounds"
}
} }
}, },
"buttons": { "buttons": {

View file

@ -8,11 +8,156 @@
"en": "English", "en": "English",
"zh": "简体中文" "zh": "简体中文"
}, },
"common": {
"save": "保存",
"untitled": "未命名",
"copy": "复制",
"copied": "已复制!",
"override": "覆盖",
"done": "完成!",
"reset": "重置",
"start": "开始",
"pause": "暂停",
"stop": "停止",
"back": "返回",
"restart": "重新开始",
"cancel": "取消",
"delete": "删除",
"plural-suffix": ""
},
"toolbar": {
"menu-aria-label": "菜单",
"global-volume-label": "全局音量",
"items": {
"presets": "预设",
"share": "分享声音",
"shuffle": "随机播放",
"sleep-timer": "睡眠定时器",
"countdown": "倒计时器",
"pomodoro": "番茄钟",
"notepad": "记事本",
"todo": "待办清单",
"breathing": "呼吸练习",
"binaural": "双耳节拍",
"isochronic": "等时声频",
"shortcuts": "快捷键",
"buy-me-a-coffee": "请我喝咖啡",
"source-code": "源代码"
}
},
"modals": { "modals": {
"reload": { "presets": {
"title": "发现新内容", "title": "预设",
"description": "检测到可用新内容,点击“重新加载”按钮进行更新。", "your-presets-title": "您的预设",
"reloadButton": "重新加载" "empty": "您还没有任何预设。",
"new-preset-title": "新建预设",
"placeholder": "预设名称",
"play-button-tooltip": "播放预设",
"play-button-aria-label": "播放预设 {{label}}",
"delete-button-tooltip": "删除预设",
"delete-button-aria-label": "删除预设 {{label}}",
"no-selected-warning": "要创建预设,请先选择一些声音。"
},
"share-link": {
"title": "分享您的声音组合!",
"description": "复制下面的链接并发送给您想与之分享的人。",
"copy-button-aria-label": "复制链接"
},
"shared": {
"title": "检测到新的声音组合!",
"description": "有人与您分享了以下声音组合。您想覆盖当前的选择吗?",
"snackbar-message": "完成!您现在可以播放新的选择了。"
},
"sleep-timer": {
"title": "睡眠定时器",
"description": "在指定时间后停止播放声音。",
"hours-label": "小时",
"minutes-label": "分钟"
},
"countdown": {
"title": "倒计时器",
"description": "超级简单的倒计时器。",
"placeholder-hh": "时",
"placeholder-mm": "分",
"placeholder-ss": "秒"
},
"pomodoro": {
"title": "番茄钟",
"settings-tooltip": "更改时间",
"completed": "已完成 {{count}} 次",
"tabs": {
"pomodoro": "番茄钟",
"short-break": "短休息",
"long-break": "长休息"
},
"settings": {
"title": "更改时间",
"pomodoro-label": "番茄钟",
"short-break-label": "短休息",
"long-break-label": "长休息",
"minutes-unit": "分钟"
}
},
"notepad": {
"title-label": "您的笔记",
"copy-tooltip": "复制笔记",
"download-tooltip": "下载笔记",
"clear-tooltip": "清空笔记",
"restore-tooltip": "恢复笔记",
"placeholder": "您在想什么?",
"counter-stats": "共 {{chars}} 个字符"
},
"todo": {
"title": "待办清单",
"description": "超级简单的待办事项列表。",
"add-placeholder": "我需要做...",
"add-button": "添加",
"your-todos-label": "您的待办事项",
"empty": "您还没有任何待办事项。",
"delete-button-aria-label": "删除待办事项"
},
"breathing": {
"title": "呼吸练习",
"phases": {
"inhale": "吸气",
"exhale": "呼气",
"hold": "屏息"
},
"exercises": {
"box": "方形呼吸",
"resonant": "共振呼吸",
"478": "4-7-8 呼吸"
}
},
"generators": {
"presets-label": "预设:",
"base-frequency-label": "基础频率 (Hz):",
"volume-label": "音量:",
"presets": {
"delta": "Delta (深度睡眠) 2 Hz",
"theta": "Theta (冥想) 5 Hz",
"alpha": "Alpha (放松) 10 Hz",
"beta": "Beta (专注) 20 Hz",
"gamma": "Gamma (认知) 40 Hz",
"custom": "自定义"
}
},
"binaural": {
"title": "双耳节拍",
"description": "双耳节拍生成器。",
"beat-frequency-label": "节拍频率 (Hz):"
},
"isochronic": {
"title": "等时声频",
"description": "等时声频生成器。",
"tone-frequency-label": "声频频率 (Hz):"
},
"shortcuts": {
"title": "键盘快捷键",
"labels": {
"toggle-play": "播放/暂停",
"unselect-all": "取消全选声音"
}
} }
}, },
"buttons": { "buttons": {