feat: 添加多语言支持,包含中英文界面切换功能

This commit is contained in:
LEIJM 2025-08-17 10:25:43 +08:00
parent 8360ab1ca4
commit 8cb93380d0
54 changed files with 2109 additions and 246 deletions

View file

@ -22,11 +22,11 @@ const config: StorybookConfig = {
docs: {
autodocs: 'tag',
},
viteFinal(config) {
return {
...config,
define: {
'process.env.NODE_DEBUG': false, // https://github.com/storybookjs/storybook/issues/18920
},
@ -39,8 +39,8 @@ const config: StorybookConfig = {
},
],
},
}
}
};
},
};
export default config;

View file

@ -1,58 +1,119 @@
---
import { Container } from '@/components/container';
import { Container } from './container';
import { count as soundCount } from '@/lib/sounds';
const count = soundCount();
const paragraphs = [
const enParagraphs = [
{
body: 'Craving a calming escape from the daily grind? Do you need the perfect soundscape to boost your focus or lull you into peaceful sleep? Look no further than Moodist, your free and open-source ambient sound generator! Ditch the subscriptions and registrations with Moodist, you unlock a world of soothing and immersive audio experiences, entirely for free.',
counter: '01',
title: 'Free Ambient Sounds',
body: 'Craving a calming escape from the daily grind? Do you need the perfect soundscape to boost your focus or lull you into peaceful sleep? Look no further than Moodist, your free and open-source ambient sound generator! Ditch the subscriptions and registrations with Moodist, you unlock a world of soothing and immersive audio experiences, entirely for free.'
},
{
body: `Dive into an expansive library of ${count} carefully curated sounds. Nature lovers will find solace in the gentle murmur of streams, the rhythmic crash of waves, or the crackling warmth of a campfire. Cityscapes come alive with the soft hum of cafes, the rhythmic clatter of trains, or the calming white noise of traffic. And for those seeking deeper focus or relaxation, Moodist offers binaural beats and color noise designed to enhance your state of mind.`,
counter: '02',
title: 'Carefully Curated Sounds',
body: 'Dive into an expansive library of carefully curated sounds. Nature lovers will find solace in the gentle murmur of streams, the rhythmic crash of waves, or the crackling warmth of a campfire. Cityscapes come alive with the soft hum of cafes, the rhythmic clatter of trains, or the calming white noise of traffic. And for those seeking deeper focus or relaxation, Moodist offers binaural beats and color noise designed to enhance your state of mind.'
},
{
body: 'The beauty of Moodist lies in its simplicity and customization. No complex menus or confusing options just choose your desired sounds, adjust the volume balance, and hit play. Want to blend the gentle chirping of birds with the soothing sound of rain? No problem! Layer as many sounds as you like to create your personalized soundscape oasis.',
counter: '03',
title: 'Create Your Soundscape',
body: 'The beauty of Moodist lies in its simplicity and customization. No complex menus or confusing options just choose your desired sounds, adjust the volume balance, and hit play. Want to blend the gentle chirping of birds with the soothing sound of rain? No problem! Layer as many sounds as you like to create your personalized soundscape oasis.'
},
{
body: "Whether you're looking to unwind after a long day, enhance your focus during work, or lull yourself into a peaceful sleep, Moodist has the perfect soundscape waiting for you. The best part? It's completely free and open-source, so you can enjoy its benefits without any strings attached. Start using Moodist today and discover your new haven of tranquility and focus!",
counter: '04',
title: 'Sounds for Every Moment',
body: "Whether you're looking to unwind after a long day, enhance your focus during work, or lull yourself into a peaceful sleep, Moodist has the perfect soundscape waiting for you. The best part? It's completely free and open-source, so you can enjoy its benefits without any strings attached. Start using Moodist today and discover your new haven of tranquility and focus!"
}
];
const zhParagraphs = [
{
counter: '01',
title: '免费环境音效',
body: '渴望从日常忙碌中逃离寻找平静需要完美的音景来提升专注力或帮助您安然入睡Moodist就是您的答案这是一个免费且开源的环境音效生成器告别订阅和注册——使用Moodist您可以免费解锁一个舒缓且沉浸式的音频体验世界。'
},
{
counter: '02',
title: '精心策划的音效',
body: '深入探索精心策划的音效库。自然爱好者会在溪流的轻柔低语、海浪的节奏拍打或篝火的温暖噼啪声中找到慰藉。城市景观因咖啡馆的柔和嗡嗡声、火车的节奏咔嗒声或交通的平静白噪音而生动起来。对于那些寻求更深层专注或放松的人来说Moodist提供双耳节拍和彩色噪音旨在增强您的心理状态。'
},
{
counter: '03',
title: '创建您的音景',
body: 'Moodist的美在于其简单性和可定制性。没有复杂的菜单或令人困惑的选项——只需选择您想要的音效调整音量平衡然后点击播放。想要将鸟儿的轻柔啁啾声与雨声的舒缓声混合没问题叠加任意数量的音效来创建您个性化的音景绿洲。'
},
{
counter: '04',
title: '每个时刻的音效',
body: '无论您是想在漫长的一天后放松在工作时提升专注力还是让自己安然入睡Moodist都有完美的音景等着您。最好的部分它完全免费且开源所以您可以享受其好处而无需任何附加条件。今天就开始使用Moodist发现您新的宁静和专注天堂'
}
];
---
<section class="about">
<div class="effect"></div>
<div class="about">
<Container>
<div class="effect"></div>
<Container tight>
{
paragraphs.map((paragraph, index) => (
<div class="paragraph">
<div class="counter">
<span>0{index + 1}</span> / 0{paragraphs.length}
</div>
<h2 class="title">{paragraph.title}</h2>
<p class="body">{paragraph.body}</p>
{enParagraphs.map((paragraph, index) => (
<div class="paragraph">
<div class="counter">
<span data-lang="en">{paragraph.counter}</span>
<span data-lang="zh">{zhParagraphs[index].counter}</span>
</div>
))
}
<h3 class="title">
<span data-lang="en">{paragraph.title}</span>
<span data-lang="zh">{zhParagraphs[index].title}</span>
</h3>
<p class="body">
<span data-lang="en">{paragraph.body}</span>
<span data-lang="zh">{zhParagraphs[index].body}</span>
</p>
</div>
))}
<button class="button" id="use-moodist"> Use Moodist</button>
<div class="button" id="use-moodist">
<span data-lang="en">Use Moodist</span>
<span data-lang="zh">使用Moodist</span>
</div>
</Container>
</section>
</div>
<script lang="ts">
const button = document.getElementById('use-moodist');
<script>
// Language switching logic
function getCurrentLanguage() {
if (typeof window !== 'undefined' && window.localStorage) {
try {
return localStorage.getItem('moodist-language') || 'en';
} catch {
return 'en';
}
}
return 'en';
}
button.addEventListener('click', () => {
const app = document.getElementById('app');
function updateLanguageDisplay(lang) {
const contents = document.querySelectorAll('.paragraph [data-lang]');
contents.forEach(content => {
if (content instanceof HTMLElement) {
content.style.display = content.getAttribute('data-lang') === lang ? 'block' : 'none';
}
});
app?.scrollIntoView();
const buttonTexts = document.querySelectorAll('#use-moodist [data-lang]');
buttonTexts.forEach(text => {
if (text instanceof HTMLElement) {
text.style.display = text.getAttribute('data-lang') === lang ? 'inline' : 'none';
}
});
}
// Initialize with current language
document.addEventListener('DOMContentLoaded', () => {
const currentLang = getCurrentLanguage();
updateLanguageDisplay(currentLang);
// Listen for language changes from other components
window.addEventListener('languageChanged', (event) => {
updateLanguageDisplay(event.detail.language);
});
});
</script>

View file

@ -1,4 +1,4 @@
import { useMemo, useEffect } from 'react';
import { useMemo, useEffect, useState } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { BiSolidHeart } from 'react-icons/bi/index';
import { Howler } from 'howler';
@ -20,7 +20,20 @@ import { FADE_OUT } from '@/constants/events';
import type { Sound } from '@/data/types';
import { subscribe } from '@/lib/event';
// 安全地获取localStorage
function getLocalStorageItem(key: string, defaultValue: string = 'en'): string {
if (typeof window !== 'undefined' && window.localStorage) {
try {
return localStorage.getItem(key) || defaultValue;
} catch {
return defaultValue;
}
}
return defaultValue;
}
export function App() {
const [currentLang, setCurrentLang] = useState('en');
const categories = useMemo(() => sounds.categories, []);
const favorites = useSoundStore(useShallow(state => state.getFavorites()));
@ -28,6 +41,23 @@ export function App() {
const lock = useSoundStore(state => state.lock);
const unlock = useSoundStore(state => state.unlock);
// 在客户端初始化语言
useEffect(() => {
const lang = getLocalStorageItem('moodist-language');
setCurrentLang(lang);
// 监听语言变化
const handleLanguageChange = (event: CustomEvent) => {
setCurrentLang(event.detail.language);
};
window.addEventListener('languageChanged', handleLanguageChange as EventListener);
return () => {
window.removeEventListener('languageChanged', handleLanguageChange as EventListener);
};
}, []);
const favoriteSounds = useMemo(() => {
const favoriteSounds = categories
.map(category => category.sounds)
@ -75,16 +105,18 @@ export function App() {
const favorites = [];
if (favoriteSounds.length) {
const favoritesTitle = currentLang === 'zh' ? '收藏' : 'Favorites';
favorites.push({
icon: <BiSolidHeart />,
id: 'favorites',
sounds: favoriteSounds as Array<Sound>,
title: 'Favorites',
title: favoritesTitle,
});
}
return [...favorites, ...categories];
}, [favoriteSounds, categories]);
}, [favoriteSounds, categories, currentLang]);
return (
<SnackbarProvider>

View file

@ -1,4 +1,4 @@
import { useCallback, useEffect } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { BiPause, BiPlay } from 'react-icons/bi/index';
import { useHotkeys } from 'react-hotkeys-hook';
@ -8,7 +8,20 @@ import { cn } from '@/helpers/styles';
import styles from './play.module.css';
// 安全地获取localStorage
function getLocalStorageItem(key: string, defaultValue: string = 'en'): string {
if (typeof window !== 'undefined' && window.localStorage) {
try {
return localStorage.getItem(key) || defaultValue;
} catch {
return defaultValue;
}
}
return defaultValue;
}
export function PlayButton() {
const [currentLang, setCurrentLang] = useState('en');
const isPlaying = useSoundStore(state => state.isPlaying);
const pause = useSoundStore(state => state.pause);
const toggle = useSoundStore(state => state.togglePlay);
@ -17,13 +30,33 @@ export function PlayButton() {
const showSnackbar = useSnackbar();
// 在客户端初始化语言
useEffect(() => {
const lang = getLocalStorageItem('moodist-language');
setCurrentLang(lang);
// 监听语言变化
const handleLanguageChange = (event: CustomEvent) => {
setCurrentLang(event.detail.language);
};
window.addEventListener('languageChanged', handleLanguageChange as EventListener);
return () => {
window.removeEventListener('languageChanged', handleLanguageChange as EventListener);
};
}, []);
const handleToggle = useCallback(() => {
if (locked) return;
if (noSelected) return showSnackbar('Please first select a sound to play.');
if (noSelected) {
const message = currentLang === 'zh' ? '请先选择一个音效来播放。' : 'Please first select a sound to play.';
return showSnackbar(message);
}
toggle();
}, [showSnackbar, toggle, noSelected, locked]);
}, [showSnackbar, toggle, noSelected, locked, currentLang]);
useEffect(() => {
if (isPlaying && noSelected) pause();
@ -31,6 +64,9 @@ export function PlayButton() {
useHotkeys('shift+space', handleToggle, {}, [handleToggle]);
const playText = currentLang === 'zh' ? '播放' : 'Play';
const pauseText = currentLang === 'zh' ? '暂停' : 'Pause';
return (
<button
aria-disabled={noSelected}
@ -42,14 +78,14 @@ export function PlayButton() {
<span aria-hidden="true">
<BiPause />
</span>{' '}
Pause
{pauseText}
</>
) : (
<>
<span aria-hidden="true">
<BiPlay />
</span>{' '}
Play
{playText}
</>
)}
</button>

View file

@ -1,4 +1,4 @@
import { useCallback } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { BiUndo, BiTrash } from 'react-icons/bi/index';
import { AnimatePresence, motion } from 'motion/react';
import { useHotkeys } from 'react-hotkeys-hook';
@ -11,13 +11,43 @@ import { fade, mix, slideX } from '@/lib/motion';
import styles from './unselect.module.css';
// 安全地获取localStorage
function getLocalStorageItem(key: string, defaultValue: string = 'en'): string {
if (typeof window !== 'undefined' && window.localStorage) {
try {
return localStorage.getItem(key) || defaultValue;
} catch {
return defaultValue;
}
}
return defaultValue;
}
export function UnselectButton() {
const [currentLang, setCurrentLang] = useState('en');
const noSelected = useSoundStore(state => state.noSelected());
const restoreHistory = useSoundStore(state => state.restoreHistory);
const hasHistory = useSoundStore(state => !!state.history);
const unselectAll = useSoundStore(state => state.unselectAll);
const locked = useSoundStore(state => state.locked);
// 在客户端初始化语言
useEffect(() => {
const lang = getLocalStorageItem('moodist-language');
setCurrentLang(lang);
// 监听语言变化
const handleLanguageChange = (event: CustomEvent) => {
setCurrentLang(event.detail.language);
};
window.addEventListener('languageChanged', handleLanguageChange as EventListener);
return () => {
window.removeEventListener('languageChanged', handleLanguageChange as EventListener);
};
}, []);
const variants = {
...mix(fade(), slideX(15)),
exit: { opacity: 0 },
@ -31,6 +61,14 @@ export function UnselectButton() {
useHotkeys('shift+r', handleToggle, {}, [handleToggle]);
const tooltipContent = hasHistory
? (currentLang === 'zh' ? '恢复未选择的音效。' : 'Restore unselected sounds.')
: (currentLang === 'zh' ? '取消选择所有音效。' : 'Unselect all sounds.');
const ariaLabel = hasHistory
? (currentLang === 'zh' ? '恢复未选择的音效' : 'Restore Unselected Sounds')
: (currentLang === 'zh' ? '取消选择所有音效' : 'Unselect All Sounds');
return (
<>
<AnimatePresence mode="wait">
@ -43,19 +81,11 @@ export function UnselectButton() {
>
<Tooltip
showDelay={0}
content={
hasHistory
? 'Restore unselected sounds.'
: 'Unselect all sounds.'
}
content={tooltipContent}
>
<button
disabled={noSelected && !hasHistory}
aria-label={
hasHistory
? 'Restore Unselected Sounds'
: 'Unselect All Sounds'
}
aria-label={ariaLabel}
className={cn(
styles.unselectButton,
noSelected && !hasHistory && styles.disabled,

View file

@ -1,4 +1,6 @@
import { useState, useEffect } from 'react';
import { Sounds } from '@/components/sounds';
import { getLocalizedCategoryTitle } from '@/utils/language';
import styles from './category.module.css';
@ -8,6 +10,18 @@ interface CategoryProps extends Category {
functional?: boolean;
}
// 安全地获取localStorage
function getLocalStorageItem(key: string, defaultValue: string = 'en'): string {
if (typeof window !== 'undefined' && window.localStorage) {
try {
return localStorage.getItem(key) || defaultValue;
} catch {
return defaultValue;
}
}
return defaultValue;
}
export function Category({
functional = true,
icon,
@ -15,6 +29,28 @@ export function Category({
sounds,
title,
}: CategoryProps) {
const [currentLang, setCurrentLang] = useState('en');
// 在客户端初始化语言
useEffect(() => {
const lang = getLocalStorageItem('moodist-language');
setCurrentLang(lang);
// 监听语言变化
const handleLanguageChange = (event: CustomEvent) => {
setCurrentLang(event.detail.language);
};
window.addEventListener('languageChanged', handleLanguageChange as EventListener);
return () => {
window.removeEventListener('languageChanged', handleLanguageChange as EventListener);
};
}, []);
// 获取本地化的分类标题
const localizedTitle = getLocalizedCategoryTitle(id as any) || title;
return (
<div className={styles.category} id={`category-${id}`}>
<div className={styles.iconContainer}>
@ -24,7 +60,7 @@ export function Category({
</div>
</div>
<div className={styles.title}>{title}</div>
<div className={styles.title}>{localizedTitle}</div>
<Sounds functional={functional} id={id} sounds={sounds} />
</div>

View file

@ -1,10 +1,46 @@
import { FaCoffee } from 'react-icons/fa/index';
import { useState, useEffect } from 'react';
import { SpecialButton } from '@/components/special-button';
import styles from './donate.module.css';
// 安全地获取localStorage
function getLocalStorageItem(key: string, defaultValue: string = 'en'): string {
if (typeof window !== 'undefined' && window.localStorage) {
try {
return localStorage.getItem(key) || defaultValue;
} catch {
return defaultValue;
}
}
return defaultValue;
}
export function Donate() {
const [currentLang, setCurrentLang] = useState('en');
// 在客户端初始化语言
useEffect(() => {
const lang = getLocalStorageItem('moodist-language');
setCurrentLang(lang);
// 监听语言变化
const handleLanguageChange = (event: CustomEvent) => {
setCurrentLang(event.detail.language);
};
window.addEventListener('languageChanged', handleLanguageChange as EventListener);
return () => {
window.removeEventListener('languageChanged', handleLanguageChange as EventListener);
};
}, []);
const supportMeText = currentLang === 'zh' ? '支持我' : 'Support Me';
const helpText = currentLang === 'zh' ? '帮助我保持Moodist无广告。' : 'Help me keep Moodist ad-free.';
const donateText = currentLang === 'zh' ? '立即捐赠' : 'Donate Today';
return (
<div className={styles.donate}>
<div className={styles.iconContainer}>
@ -15,14 +51,14 @@ export function Donate() {
</div>
<div className={styles.title}>
<span>Support Me</span>
<span>{supportMeText}</span>
</div>
<p className={styles.desc}>Help me keep Moodist ad-free.</p>
<p className={styles.desc}>{helpText}</p>
<SpecialButton
className={styles.button}
href="https://buymeacoffee.com/remvze"
>
Donate Today
{donateText}
</SpecialButton>
</div>
);

View file

@ -1,22 +1,66 @@
---
import { Container } from './container';
import { Container } from '@/components/container';
---
<Container>
<section class="wrapper">
<p class="text">
Enjoy Moodist?{' '}
<span data-lang="en">Enjoy Moodist?</span>
<span data-lang="zh">喜欢Moodist</span>
{' '}
<a
href="https://buymeacoffee.com/remvze"
rel="noreferrer"
target="_blank"
>
Support with a donation!
<span data-lang="en">Support with a donation!</span>
<span data-lang="zh">支持捐赠!</span>
</a>
</p>
</section>
</Container>
<script>
// Language switching logic
function getCurrentLanguage() {
if (typeof window !== 'undefined' && window.localStorage) {
try {
return localStorage.getItem('moodist-language') || 'en';
} catch {
return 'en';
}
}
return 'en';
}
function updateLanguageDisplay(lang) {
const elements = document.querySelectorAll(`[data-lang="${lang}"]`);
elements.forEach(el => {
if (el instanceof HTMLElement) {
el.style.display = 'block';
}
});
const otherElements = document.querySelectorAll(`[data-lang="${lang === 'en' ? 'zh' : 'en'}"]`);
otherElements.forEach(el => {
if (el instanceof HTMLElement) {
el.style.display = 'none';
}
});
}
// Initialize with current language
document.addEventListener('DOMContentLoaded', () => {
const currentLang = getCurrentLanguage();
updateLanguageDisplay(currentLang);
// Listen for language changes from other components
window.addEventListener('languageChanged', (event) => {
updateLanguageDisplay(event.detail.language);
});
});
</script>
<style>
.wrapper {
position: relative;

View file

@ -1,6 +1,7 @@
import { BiMoney, BiUserCircle, BiLogoGithub } from 'react-icons/bi/index';
import { BsSoundwave, BsStars } from 'react-icons/bs/index';
import { RxMixerHorizontal } from 'react-icons/rx/index';
import { useState, useEffect } from 'react';
import { Balancer } from 'react-wrap-balancer';
@ -9,61 +10,95 @@ import { count as soundCount } from '@/lib/sounds';
import styles from './features.module.css';
// 安全地获取localStorage
function getLocalStorageItem(key: string, defaultValue: string = 'en'): string {
if (typeof window !== 'undefined' && window.localStorage) {
try {
return localStorage.getItem(key) || defaultValue;
} catch {
return defaultValue;
}
}
return defaultValue;
}
export function Features() {
const [currentLang, setCurrentLang] = useState('en');
const count = soundCount();
// 在客户端初始化语言
useEffect(() => {
const lang = getLocalStorageItem('moodist-language');
setCurrentLang(lang);
// 监听语言变化
const handleLanguageChange = (event: CustomEvent) => {
setCurrentLang(event.detail.language);
};
window.addEventListener('languageChanged', handleLanguageChange as EventListener);
return () => {
window.removeEventListener('languageChanged', handleLanguageChange as EventListener);
};
}, []);
// 获取本地化文本
const comingSoonText = currentLang === 'zh' ? '即将推出' : 'Coming Soon';
const featuresTitle = currentLang === 'zh' ? '功能特性' : 'Features';
const features = [
{
Icon: BiMoney,
body: 'Immerse yourself in sound without spending a dime.',
body: currentLang === 'zh' ? '完全免费使用,无需花费一分钱即可沉浸在声音中。' : 'Immerse yourself in sound without spending a dime.',
id: 'free-access',
label: 'Free Access',
label: currentLang === 'zh' ? '免费使用' : 'Free Access',
},
{
Icon: BiUserCircle,
body: 'Dive right in, no sign-up hoops to jump through.',
body: currentLang === 'zh' ? '直接开始使用,无需注册,没有繁琐的步骤。' : 'Dive right in, no sign-up hoops to jump through.',
id: 'no-registration',
label: 'No Registration',
label: currentLang === 'zh' ? '无需注册' : 'No Registration',
},
{
Icon: BsSoundwave,
body: `Explore ${count} unique soundscapes, from rainforests to cityscapes.`,
body: currentLang === 'zh' ? `探索${count}个独特的音景,从雨林到城市景观。` : `Explore ${count} unique soundscapes, from rainforests to cityscapes.`,
id: 'diverse-sounds',
label: 'Diverse Sounds',
label: currentLang === 'zh' ? '丰富音效' : 'Diverse Sounds',
},
{
Icon: RxMixerHorizontal,
body: 'Craft your perfect soundscape by blending and adjusting sounds.',
body: currentLang === 'zh' ? '通过混合和调整声音,打造您完美的音景。' : 'Craft your perfect soundscape by blending and adjusting sounds.',
id: 'customizable-mixes',
label: 'Customizable Mixes',
label: currentLang === 'zh' ? '自定义混音' : 'Customizable Mixes',
},
{
Icon: BiLogoGithub,
body: 'Contribute and collaborate, making the best even better.',
body: currentLang === 'zh' ? '贡献和协作,让最好的变得更好。' : 'Contribute and collaborate, making the best even better.',
id: 'open-source',
label: 'Open-Source',
label: currentLang === 'zh' ? '开源项目' : 'Open-Source',
link: {
label: 'Source Code',
label: currentLang === 'zh' ? '源代码' : 'Source Code',
url: 'https://github.com/remvze/moodist',
},
},
{
Icon: BsStars,
body: 'Uninterrupted immersion, focus on the sounds, not the tech.',
body: currentLang === 'zh' ? '不间断的沉浸体验,专注于声音,而不是技术。' : 'Uninterrupted immersion, focus on the sounds, not the tech.',
id: 'seamless-experience',
label: 'Seamless Experience',
label: currentLang === 'zh' ? '流畅体验' : 'Seamless Experience',
},
{
Icon: BsStars,
body: 'Spread the calm, easily share your customized sound blends.',
body: currentLang === 'zh' ? '传播平静,轻松分享您定制的音效组合。' : 'Spread the calm, easily share your customized sound blends.',
id: 'share-selections',
label: 'Share Selections',
label: currentLang === 'zh' ? '分享选择' : 'Share Selections',
},
{
Icon: BsStars,
body: 'Lock in your favorite mixes for instant return to your sonic haven.',
body: currentLang === 'zh' ? '锁定您最喜爱的混音,立即回到您的音效天堂。' : 'Lock in your favorite mixes for instant return to your sonic haven.',
id: 'save-presets',
label: 'Save Presets',
label: currentLang === 'zh' ? '保存预设' : 'Save Presets',
soon: true,
},
];
@ -78,7 +113,7 @@ export function Features() {
</div>
</div>
<h2 className={styles.title}>Features</h2>
<h2 className={styles.title}>{featuresTitle}</h2>
<div className={styles.features}>
{features.map(feature => (
@ -97,7 +132,7 @@ export function Features() {
</a>
)}
{feature.soon && <div className={styles.soon}>Coming Soon</div>}
{feature.soon && <div className={styles.soon}>{comingSoonText}</div>}
</div>
))}
</div>

View file

@ -5,11 +5,58 @@ import { Container } from './container';
<footer class="footer">
<Container>
<p>
Created by <a href="https://twitter.com/remvze">Maze ✦</a>
<span data-lang="en">Created by</span>
<span data-lang="zh">由</span>
{' '}
<a href="https://twitter.com/remvze">
<span data-lang="en">Maze ✦</span>
<span data-lang="zh">Maze ✦ 创建</span>
</a>
</p>
</Container>
</footer>
<script>
// Language switching logic
function getCurrentLanguage() {
if (typeof window !== 'undefined' && window.localStorage) {
try {
return localStorage.getItem('moodist-language') || 'en';
} catch {
return 'en';
}
}
return 'en';
}
function updateLanguageDisplay(lang) {
const elements = document.querySelectorAll(`[data-lang="${lang}"]`);
elements.forEach(el => {
if (el instanceof HTMLElement) {
el.style.display = 'block';
}
});
const otherElements = document.querySelectorAll(`[data-lang="${lang === 'en' ? 'zh' : 'en'}"]`);
otherElements.forEach(el => {
if (el instanceof HTMLElement) {
el.style.display = 'none';
}
});
}
// Initialize with current language
document.addEventListener('DOMContentLoaded', () => {
const currentLang = getCurrentLanguage();
updateLanguageDisplay(currentLang);
// Listen for language changes from other components
window.addEventListener('languageChanged', (event) => {
updateLanguageDisplay(event.detail.language);
});
});
</script>
<style>
.footer {
display: flex;

View file

@ -2,6 +2,7 @@
import { BsSoundwave } from 'react-icons/bs/index';
import { Container } from './container';
import { LanguageSwitcher } from './language-switcher';
import { count as soundCount } from '@/lib/sounds';
@ -12,6 +13,12 @@ const count = soundCount();
<Container>
<div class="wrapper">
<div class="pattern"></div>
<!-- Language Switcher -->
<div class="language-switcher-container">
<LanguageSwitcher client:load />
</div>
<div class="logo-wrapper">
<img
alt="Faded Moodist Logo"
@ -24,20 +31,71 @@ const count = soundCount();
</div>
<h1 class="title">
Ambient Sounds<span class="line">For Focus and Calm</span>
<span class="title-text" data-lang="en">Ambient Sounds</span>
<span class="title-text" data-lang="zh">环境音效</span>
<span class="line">
<span class="line-text" data-lang="en">For Focus and Calm</span>
<span class="line-text" data-lang="zh">专注与平静</span>
</span>
</h1>
<h2 class="desc">Free and Open-Source.</h2>
<h2 class="desc">
<span class="desc-text" data-lang="en">Free and Open-Source.</span>
<span class="desc-text" data-lang="zh">免费且开源。</span>
</h2>
<p class="sounds">
<span aria-hidden="true" class="icon">
<BsSoundwave />
</span>
<span>{count} Sounds</span>
<span class="sounds-text">
<span class="sounds-count" data-lang="en">{count} Sounds</span>
<span class="sounds-count" data-lang="zh">{count} 种音效</span>
</span>
</p>
</div>
</Container>
</div>
<script>
// Language switching logic
function getCurrentLanguage() {
if (typeof window !== 'undefined' && window.localStorage) {
try {
return localStorage.getItem('moodist-language') || 'en';
} catch {
return 'en';
}
}
return 'en';
}
function updateLanguageDisplay(lang) {
// 更新所有文本元素
const allElements = document.querySelectorAll('[data-lang]');
allElements.forEach(el => {
if (el instanceof HTMLElement) {
if (el.getAttribute('data-lang') === lang) {
el.style.display = 'block';
} else {
el.style.display = 'none';
}
}
});
}
// Initialize with current language
document.addEventListener('DOMContentLoaded', () => {
const currentLang = getCurrentLanguage();
updateLanguageDisplay(currentLang);
// Listen for language changes from other components
window.addEventListener('languageChanged', (event) => {
updateLanguageDisplay(event.detail.language);
});
});
</script>
<style>
.hero {
text-align: center;
@ -78,6 +136,13 @@ const count = soundCount();
}
}
.language-switcher-container {
position: absolute;
top: 20px;
right: 20px;
z-index: 10;
}
& .logo-wrapper {
mask-image: linear-gradient(#000, rgb(0 0 0 / 40%), rgb(0 0 0 / 5%));
@ -99,6 +164,14 @@ const count = soundCount();
font-weight: 600;
line-height: 1.1;
& .title-text {
display: block;
&[data-lang="zh"] {
display: none;
}
}
& .line {
display: block;
margin-top: 2px;
@ -108,6 +181,14 @@ const count = soundCount();
);
background-clip: text;
-webkit-text-fill-color: transparent;
& .line-text {
display: block;
&[data-lang="zh"] {
display: none;
}
}
}
}
@ -115,6 +196,14 @@ const count = soundCount();
margin-top: 12px;
line-height: 1.6;
color: var(--color-foreground-subtle);
& .desc-text {
display: block;
&[data-lang="zh"] {
display: none;
}
}
}
& .sounds {
@ -143,6 +232,16 @@ const count = soundCount();
border-right: 1px solid var(--color-neutral-200);
border-radius: 0 100px 100px 0;
}
& .sounds-text {
& .sounds-count {
display: block;
&[data-lang="zh"] {
display: none;
}
}
}
&::before {
position: absolute;

View file

@ -0,0 +1,17 @@
import { useLanguage } from '@/hooks/use-language';
import '@/styles/language-switcher.css';
export function LanguageSwitcher() {
const { currentLanguage, toggleLanguage } = useLanguage();
return (
<button
onClick={toggleLanguage}
className="language-switcher"
aria-label={`Switch to ${currentLanguage === 'en' ? 'Chinese' : 'English'}`}
title={`Switch to ${currentLanguage === 'en' ? 'Chinese' : 'English'}`}
>
{currentLanguage === 'en' ? 'ZH' : 'EN'}
</button>
);
}

View file

@ -5,6 +5,18 @@ import { Slider } from '@/components/slider';
import styles from './binaural.module.css';
// 安全地获取localStorage
function getLocalStorageItem(key: string, defaultValue: string = 'en'): string {
if (typeof window !== 'undefined' && window.localStorage) {
try {
return localStorage.getItem(key) || defaultValue;
} catch {
return defaultValue;
}
}
return defaultValue;
}
interface BinauralProps {
onClose: () => void;
show: boolean;
@ -41,12 +53,30 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
const [volume, setVolume] = useState<number>(0.5); // Default volume at 50%
const [isPlaying, setIsPlaying] = useState<boolean>(false);
const [selectedPreset, setSelectedPreset] = useState<string>('Custom');
const [currentLang, setCurrentLang] = useState('en');
const audioContextRef = useRef<AudioContext | null>(null);
const leftOscillatorRef = useRef<OscillatorNode | null>(null);
const rightOscillatorRef = useRef<OscillatorNode | null>(null);
const gainNodeRef = useRef<GainNode | null>(null);
// 在客户端初始化语言
useEffect(() => {
const lang = getLocalStorageItem('moodist-language');
setCurrentLang(lang);
// 监听语言变化
const handleLanguageChange = (event: CustomEvent) => {
setCurrentLang(event.detail.language);
};
window.addEventListener('languageChanged', handleLanguageChange as EventListener);
return () => {
window.removeEventListener('languageChanged', handleLanguageChange as EventListener);
};
}, []);
const startSound = () => {
if (isPlaying) return;
@ -100,22 +130,41 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
const stopSound = useCallback(() => {
if (!isPlaying) return;
leftOscillatorRef.current?.stop();
rightOscillatorRef.current?.stop();
audioContextRef.current?.close();
// Stop oscillators
if (leftOscillatorRef.current) {
leftOscillatorRef.current.stop();
leftOscillatorRef.current = null;
}
if (rightOscillatorRef.current) {
rightOscillatorRef.current.stop();
rightOscillatorRef.current = null;
}
// Close audio context
if (audioContextRef.current) {
audioContextRef.current.close();
audioContextRef.current = null;
}
setIsPlaying(false);
}, [isPlaying]);
// Cleanup on unmount
useEffect(() => {
return () => {
stopSound();
};
}, [stopSound]);
// Update volume when volume state changes
useEffect(() => {
// Update gain node when volume changes
if (gainNodeRef.current) {
gainNodeRef.current.gain.value = volume;
}
}, [volume]);
// Update frequencies when baseFrequency or beatFrequency changes
useEffect(() => {
// Update base frequency for both left and right oscillators when it changes
if (leftOscillatorRef.current && rightOscillatorRef.current) {
const { leftFrequency, rightFrequency } =
computeBinauralBeatOscillatorFrequencies(baseFrequency, beatFrequency);
@ -124,26 +173,6 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
}
}, [baseFrequency, beatFrequency]);
useEffect(() => {
// Cleanup when component unmounts
return () => {
if (isPlaying) {
stopSound();
}
};
}, [isPlaying, stopSound]);
useEffect(() => {
// Update frequencies when a preset is selected
if (selectedPreset !== 'Custom') {
const preset = presets.find(p => p.name === selectedPreset);
if (preset) {
setBaseFrequency(preset.baseFrequency);
setBeatFrequency(preset.beatFrequency);
}
}
}, [selectedPreset]);
const handlePresetChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const selected = e.target.value;
setSelectedPreset(selected);
@ -160,20 +189,53 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
}
};
// 获取本地化文本
const titleText = currentLang === 'zh' ? '双耳节拍' : 'Binaural Beat';
const descText = currentLang === 'zh' ? '双耳节拍发生器。' : 'Binaural beat generator.';
const presetsText = currentLang === 'zh' ? '预设:' : 'Presets:';
const baseFreqText = currentLang === 'zh' ? '基础频率 (Hz):' : 'Base Frequency (Hz):';
const beatFreqText = currentLang === 'zh' ? '节拍频率 (Hz):' : 'Beat Frequency (Hz):';
const volumeText = currentLang === 'zh' ? '音量:' : 'Volume:';
const startText = currentLang === 'zh' ? '开始' : 'Start';
const stopText = currentLang === 'zh' ? '停止' : 'Stop';
// 获取本地化的预设名称
const getLocalizedPresetName = (preset: Preset) => {
if (currentLang === 'zh') {
switch (preset.name) {
case 'Delta (Deep Sleep) 2 Hz':
return 'Delta (深度睡眠) 2 Hz';
case 'Theta (Meditation) 5 Hz':
return 'Theta (冥想) 5 Hz';
case 'Alpha (Relaxation) 10 Hz':
return 'Alpha (放松) 10 Hz';
case 'Beta (Focus) 20 Hz':
return 'Beta (专注) 20 Hz';
case 'Gamma (Cognition) 40 Hz':
return 'Gamma (认知) 40 Hz';
case 'Custom':
return '自定义';
default:
return preset.name;
}
}
return preset.name;
};
return (
<Modal show={show} onClose={onClose}>
<header className={styles.header}>
<h2 className={styles.title}>Binaural Beat</h2>
<p className={styles.desc}>Binaural beat generator.</p>
<h2 className={styles.title}>{titleText}</h2>
<p className={styles.desc}>{descText}</p>
</header>
<div className={styles.fieldWrapper}>
<label>
Presets:
{presetsText}
<select value={selectedPreset} onChange={handlePresetChange}>
{presets.map(preset => (
<option key={preset.name} value={preset.name}>
{preset.name}
{getLocalizedPresetName(preset)}
</option>
))}
</select>
@ -183,7 +245,7 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
<>
<div className={styles.fieldWrapper}>
<label>
Base Frequency (Hz):
{baseFreqText}
<input
max="1500"
min="20"
@ -198,11 +260,11 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
</div>
<div className={styles.fieldWrapper}>
<label>
Beat Frequency (Hz):
{beatFreqText}
<input
max="40"
min="0.1"
step="0.1"
step="0.01"
type="number"
value={beatFrequency}
onChange={e =>
@ -215,7 +277,7 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
)}
<div className={styles.fieldWrapper}>
<label>
Volume:
{volumeText}
<Slider
className={styles.volume}
max={1}
@ -232,10 +294,10 @@ export function BinauralModal({ onClose, show }: BinauralProps) {
disabled={isPlaying}
onClick={startSound}
>
Start
{startText}
</button>
<button disabled={!isPlaying} onClick={stopSound}>
Stop
{stopText}
</button>
</div>
</Modal>

View file

@ -1,17 +1,51 @@
import { useState, useEffect } from 'react';
import { Modal } from '@/components/modal';
import { Exercise } from './exercise';
import styles from './breathing.module.css';
// 安全地获取localStorage
function getLocalStorageItem(key: string, defaultValue: string = 'en'): string {
if (typeof window !== 'undefined' && window.localStorage) {
try {
return localStorage.getItem(key) || defaultValue;
} catch {
return defaultValue;
}
}
return defaultValue;
}
interface TimerProps {
onClose: () => void;
show: boolean;
}
export function BreathingExerciseModal({ onClose, show }: TimerProps) {
const [currentLang, setCurrentLang] = useState('en');
// 在客户端初始化语言
useEffect(() => {
const lang = getLocalStorageItem('moodist-language');
setCurrentLang(lang);
// 监听语言变化
const handleLanguageChange = (event: CustomEvent) => {
setCurrentLang(event.detail.language);
};
window.addEventListener('languageChanged', handleLanguageChange as EventListener);
return () => {
window.removeEventListener('languageChanged', handleLanguageChange as EventListener);
};
}, []);
const titleText = currentLang === 'zh' ? '呼吸练习' : 'Breathing Exercise';
return (
<Modal show={show} onClose={onClose}>
<h2 className={styles.title}>Breathing Exercise</h2>
<h2 className={styles.title}>{titleText}</h2>
<Exercise />
</Modal>
);

View file

@ -8,6 +8,18 @@ import styles from './exercise.module.css';
type Exercise = 'Box Breathing' | 'Resonant Breathing' | '4-7-8 Breathing';
type Phase = 'inhale' | 'exhale' | 'holdInhale' | 'holdExhale';
// 安全地获取localStorage
function getLocalStorageItem(key: string, defaultValue: string = 'en'): string {
if (typeof window !== 'undefined' && window.localStorage) {
try {
return localStorage.getItem(key) || defaultValue;
} catch {
return defaultValue;
}
}
return defaultValue;
}
const EXERCISE_PHASES: Record<Exercise, Phase[]> = {
'4-7-8 Breathing': ['inhale', 'holdInhale', 'exhale'],
'Box Breathing': ['inhale', 'holdInhale', 'exhale', 'holdExhale'],
@ -20,17 +32,69 @@ const EXERCISE_DURATIONS: Record<Exercise, Partial<Record<Phase, number>>> = {
'Resonant Breathing': { exhale: 5, inhale: 5 }, // No holdExhale
};
const PHASE_LABELS: Record<Phase, string> = {
exhale: 'Exhale',
holdExhale: 'Hold',
holdInhale: 'Hold',
inhale: 'Inhale',
};
// 获取本地化的阶段标签
function getLocalizedPhaseLabels(): Record<Phase, string> {
const currentLang = getLocalStorageItem('moodist-language');
if (currentLang === 'zh') {
return {
exhale: '呼气',
holdExhale: '保持',
holdInhale: '保持',
inhale: '吸气',
};
}
return {
exhale: 'Exhale',
holdExhale: 'Hold',
holdInhale: 'Hold',
inhale: 'Inhale',
};
}
// 获取本地化的练习名称
function getLocalizedExerciseName(exercise: Exercise): string {
const currentLang = getLocalStorageItem('moodist-language');
if (currentLang === 'zh') {
switch (exercise) {
case 'Box Breathing':
return '方形呼吸';
case 'Resonant Breathing':
return '共振呼吸';
case '4-7-8 Breathing':
return '4-7-8呼吸';
default:
return exercise;
}
}
return exercise;
}
export function Exercise() {
const [selectedExercise, setSelectedExercise] =
useState<Exercise>('4-7-8 Breathing');
const [phaseIndex, setPhaseIndex] = useState(0);
const [currentLang, setCurrentLang] = useState('en');
// 在客户端初始化语言
useEffect(() => {
const lang = getLocalStorageItem('moodist-language');
setCurrentLang(lang);
// 监听语言变化
const handleLanguageChange = (event: CustomEvent) => {
setCurrentLang(event.detail.language);
};
window.addEventListener('languageChanged', handleLanguageChange as EventListener);
return () => {
window.removeEventListener('languageChanged', handleLanguageChange as EventListener);
};
}, []);
const phases = useMemo(
() => EXERCISE_PHASES[selectedExercise],
@ -92,6 +156,9 @@ export function Exercise() {
return () => clearInterval(interval);
}, []);
// 获取本地化的阶段标签
const phaseLabels = getLocalizedPhaseLabels();
return (
<>
<div className={styles.exercise}>
@ -105,7 +172,7 @@ export function Exercise() {
key={selectedExercise}
variants={animationVariants}
/>
<p className={styles.phase}>{PHASE_LABELS[currentPhase]}</p>
<p className={styles.phase}>{phaseLabels[currentPhase]}</p>
</div>
<div className={styles.selectWrapper}>
@ -116,7 +183,7 @@ export function Exercise() {
>
{Object.keys(EXERCISE_PHASES).map(exercise => (
<option key={exercise} value={exercise}>
{exercise}
{getLocalizedExerciseName(exercise as Exercise)}
</option>
))}
</select>

View file

@ -5,6 +5,18 @@ import { Slider } from '@/components/slider';
import styles from './isochornic.module.css';
// 安全地获取localStorage
function getLocalStorageItem(key: string, defaultValue: string = 'en'): string {
if (typeof window !== 'undefined' && window.localStorage) {
try {
return localStorage.getItem(key) || defaultValue;
} catch {
return defaultValue;
}
}
return defaultValue;
}
interface IsochronicProps {
onClose: () => void;
show: boolean;
@ -32,6 +44,7 @@ export function IsochronicModal({ onClose, show }: IsochronicProps) {
const [waveform] = useState<OscillatorType>('sine'); // Default waveform
const [isPlaying, setIsPlaying] = useState<boolean>(false);
const [selectedPreset, setSelectedPreset] = useState<string>('Custom');
const [currentLang, setCurrentLang] = useState('en');
const audioContextRef = useRef<AudioContext | null>(null);
const oscillatorRef = useRef<OscillatorNode | null>(null);
@ -39,6 +52,23 @@ export function IsochronicModal({ onClose, show }: IsochronicProps) {
const beatGainRef = useRef<GainNode | null>(null);
const modulatorRef = useRef<OscillatorNode | null>(null);
// 在客户端初始化语言
useEffect(() => {
const lang = getLocalStorageItem('moodist-language');
setCurrentLang(lang);
// 监听语言变化
const handleLanguageChange = (event: CustomEvent) => {
setCurrentLang(event.detail.language);
};
window.addEventListener('languageChanged', handleLanguageChange as EventListener);
return () => {
window.removeEventListener('languageChanged', handleLanguageChange as EventListener);
};
}, []);
const startSound = () => {
if (isPlaying) return;
@ -90,9 +120,21 @@ export function IsochronicModal({ onClose, show }: IsochronicProps) {
const stopSound = useCallback(() => {
if (!isPlaying) return;
oscillatorRef.current?.stop();
modulatorRef.current?.stop();
audioContextRef.current?.close();
// Stop oscillators
if (oscillatorRef.current) {
oscillatorRef.current.stop();
oscillatorRef.current = null;
}
if (modulatorRef.current) {
modulatorRef.current.stop();
modulatorRef.current = null;
}
// Close audio context
if (audioContextRef.current) {
audioContextRef.current.close();
audioContextRef.current = null;
}
setIsPlaying(false);
}, [isPlaying]);
@ -161,20 +203,53 @@ export function IsochronicModal({ onClose, show }: IsochronicProps) {
}
};
// 获取本地化文本
const titleText = currentLang === 'zh' ? '等时音调' : 'Isochronic Tone';
const descText = currentLang === 'zh' ? '等时音调发生器。' : 'Isochronic tone generator.';
const presetsText = currentLang === 'zh' ? '预设:' : 'Presets:';
const baseFreqText = currentLang === 'zh' ? '基础频率 (Hz):' : 'Base Frequency (Hz):';
const toneFreqText = currentLang === 'zh' ? '音调频率 (Hz):' : 'Tone Frequency (Hz):';
const volumeText = currentLang === 'zh' ? '音量:' : 'Volume:';
const startText = currentLang === 'zh' ? '开始' : 'Start';
const stopText = currentLang === 'zh' ? '停止' : 'Stop';
// 获取本地化的预设名称
const getLocalizedPresetName = (preset: Preset) => {
if (currentLang === 'zh') {
switch (preset.name) {
case 'Delta (Deep Sleep) 2 Hz':
return 'Delta (深度睡眠) 2 Hz';
case 'Theta (Meditation) 5 Hz':
return 'Theta (冥想) 5 Hz';
case 'Alpha (Relaxation) 10 Hz':
return 'Alpha (放松) 10 Hz';
case 'Beta (Focus) 20 Hz':
return 'Beta (专注) 20 Hz';
case 'Gamma (Cognition) 40 Hz':
return 'Gamma (认知) 40 Hz';
case 'Custom':
return '自定义';
default:
return preset.name;
}
}
return preset.name;
};
return (
<Modal show={show} onClose={onClose}>
<header className={styles.header}>
<h2 className={styles.title}>Isochronic Tone</h2>
<p className={styles.desc}>Isochronic tone generator.</p>
<h2 className={styles.title}>{titleText}</h2>
<p className={styles.desc}>{descText}</p>
</header>
<div className={styles.fieldWrapper}>
<label>
Presets:
{presetsText}
<select value={selectedPreset} onChange={handlePresetChange}>
{presets.map(preset => (
<option key={preset.name} value={preset.name}>
{preset.name}
{getLocalizedPresetName(preset)}
</option>
))}
</select>
@ -184,7 +259,7 @@ export function IsochronicModal({ onClose, show }: IsochronicProps) {
<>
<div className={styles.fieldWrapper}>
<label>
Base Frequency (Hz):
{baseFreqText}
<input
max="2000"
min="20"
@ -199,7 +274,7 @@ export function IsochronicModal({ onClose, show }: IsochronicProps) {
</div>
<div className={styles.fieldWrapper}>
<label>
Tone Frequency (Hz):
{toneFreqText}
<input
max="40"
min="0.1"
@ -230,7 +305,7 @@ export function IsochronicModal({ onClose, show }: IsochronicProps) {
)}
<div className={styles.fieldWrapper}>
<label>
Volume:
{volumeText}
<Slider
className={styles.volume}
max={1}
@ -247,10 +322,10 @@ export function IsochronicModal({ onClose, show }: IsochronicProps) {
disabled={isPlaying}
onClick={startSound}
>
Start
{startText}
</button>
<button disabled={!isPlaying} onClick={stopSound}>
Stop
{stopText}
</button>
</div>
</Modal>

View file

@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import YouTube from 'react-youtube';
import { Modal } from '@/components/modal/modal';
@ -6,6 +6,18 @@ import { Modal } from '@/components/modal/modal';
import styles from './lofi.module.css';
import { padNumber } from '@/helpers/number';
// 安全地获取localStorage
function getLocalStorageItem(key: string, defaultValue: string = 'en'): string {
if (typeof window !== 'undefined' && window.localStorage) {
try {
return localStorage.getItem(key) || defaultValue;
} catch {
return defaultValue;
}
}
return defaultValue;
}
interface LofiProps {
onClose: () => void;
show: boolean;
@ -41,27 +53,48 @@ const videos = [
export function LofiModal({ onClose, show }: LofiProps) {
const [isAccepted, setIsAccepted] = useState(false);
const [currentLang, setCurrentLang] = useState('en');
// 在客户端初始化语言
useEffect(() => {
const lang = getLocalStorageItem('moodist-language');
setCurrentLang(lang);
// 监听语言变化
const handleLanguageChange = (event: CustomEvent) => {
setCurrentLang(event.detail.language);
};
window.addEventListener('languageChanged', handleLanguageChange as EventListener);
return () => {
window.removeEventListener('languageChanged', handleLanguageChange as EventListener);
};
}, []);
// 获取本地化文本
const titleText = currentLang === 'zh' ? 'Lofi音乐播放器' : 'Lofi Music Player';
const noticeText = currentLang === 'zh'
? '此功能使用嵌入式YouTube视频播放音乐。继续使用即表示您同意连接到YouTubeYouTube可能会根据其隐私政策收集数据。我们不控制或跟踪这些数据。'
: 'This feature plays music using embedded YouTube videos. By continuing, you agree to connect to YouTube, which may collect data in accordance with their privacy policy. We do not control or track this data.';
const cancelText = currentLang === 'zh' ? '取消' : 'Cancel';
const continueText = currentLang === 'zh' ? '继续' : 'Continue';
return (
<Modal persist show={show} onClose={onClose}>
<h1 className={styles.title}>Lofi Music Player</h1>
<h1 className={styles.title}>{titleText}</h1>
{!isAccepted ? (
<div className={styles.notice}>
<p>
This feature plays music using embedded YouTube videos. By
continuing, you agree to connect to YouTube, which may collect data
in accordance with their privacy policy. We do not control or track
this data.
</p>
<p>{noticeText}</p>
<div className={styles.buttons}>
<button onClick={onClose}>Cancel</button>
<button onClick={onClose}>{cancelText}</button>
<button
className={styles.primary}
onClick={() => setIsAccepted(true)}
>
Continue
{continueText}
</button>
</div>
</div>

View file

@ -1,3 +1,4 @@
import { useState, useEffect } from 'react';
import { FaPlay, FaRegTrashAlt } from 'react-icons/fa/index';
import styles from './list.module.css';
@ -5,31 +6,66 @@ import styles from './list.module.css';
import { useSoundStore } from '@/stores/sound';
import { usePresetStore } from '@/stores/preset';
// 安全地获取localStorage
function getLocalStorageItem(key: string, defaultValue: string = 'en'): string {
if (typeof window !== 'undefined' && window.localStorage) {
try {
return localStorage.getItem(key) || defaultValue;
} catch {
return defaultValue;
}
}
return defaultValue;
}
interface ListProps {
close: () => void;
}
export function List({ close }: ListProps) {
const [currentLang, setCurrentLang] = useState('en');
const presets = usePresetStore(state => state.presets);
const changeName = usePresetStore(state => state.changeName);
const deletePreset = usePresetStore(state => state.deletePreset);
const override = useSoundStore(state => state.override);
const play = useSoundStore(state => state.play);
// 在客户端初始化语言
useEffect(() => {
const lang = getLocalStorageItem('moodist-language');
setCurrentLang(lang);
// 监听语言变化
const handleLanguageChange = (event: CustomEvent) => {
setCurrentLang(event.detail.language);
};
window.addEventListener('languageChanged', handleLanguageChange as EventListener);
return () => {
window.removeEventListener('languageChanged', handleLanguageChange as EventListener);
};
}, []);
// 获取本地化文本
const titleText = currentLang === 'zh' ? '您的预设' : 'Your Presets';
const emptyText = currentLang === 'zh' ? '您还没有任何预设。' : "You don't have any presets yet.";
const untitledText = currentLang === 'zh' ? '未命名' : 'Untitled';
return (
<div className={styles.list}>
<h3 className={styles.title}>
Your Presets {presets.length > 0 && `(${presets.length})`}
{titleText} {presets.length > 0 && `(${presets.length})`}
</h3>
{!presets.length && (
<p className={styles.empty}>You don&apos;t have any presets yet.</p>
<p className={styles.empty}>{emptyText}</p>
)}
{presets.map(preset => (
<div className={styles.preset} key={preset.id}>
<input
placeholder="Untitled"
placeholder={untitledText}
type="text"
value={preset.label}
onChange={e => changeName(preset.id, e.target.value)}

View file

@ -1,4 +1,4 @@
import { useState, type FormEvent } from 'react';
import { useState, type FormEvent, useEffect } from 'react';
import { cn } from '@/helpers/styles';
import { useSoundStore } from '@/stores/sound';
@ -6,13 +6,43 @@ import { usePresetStore } from '@/stores/preset';
import styles from './new.module.css';
// 安全地获取localStorage
function getLocalStorageItem(key: string, defaultValue: string = 'en'): string {
if (typeof window !== 'undefined' && window.localStorage) {
try {
return localStorage.getItem(key) || defaultValue;
} catch {
return defaultValue;
}
}
return defaultValue;
}
export function New() {
const [name, setName] = useState('');
const [currentLang, setCurrentLang] = useState('en');
const noSelected = useSoundStore(state => state.noSelected());
const sounds = useSoundStore(state => state.sounds);
const addPreset = usePresetStore(state => state.addPreset);
// 在客户端初始化语言
useEffect(() => {
const lang = getLocalStorageItem('moodist-language');
setCurrentLang(lang);
// 监听语言变化
const handleLanguageChange = (event: CustomEvent) => {
setCurrentLang(event.detail.language);
};
window.addEventListener('languageChanged', handleLanguageChange as EventListener);
return () => {
window.removeEventListener('languageChanged', handleLanguageChange as EventListener);
};
}, []);
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
@ -31,9 +61,15 @@ export function New() {
setName('');
};
// 获取本地化文本
const titleText = currentLang === 'zh' ? '新建预设' : 'New Preset';
const placeholderText = currentLang === 'zh' ? '预设名称' : "Preset's Name";
const saveText = currentLang === 'zh' ? '保存' : 'Save';
const noSelectedText = currentLang === 'zh' ? '要创建预设,请先选择一些音效。' : 'To make a preset, first select some sounds.';
return (
<div className={styles.new}>
<h3 className={styles.title}>New Preset</h3>
<h3 className={styles.title}>{titleText}</h3>
<form
className={cn(styles.form, noSelected && styles.disabled)}
@ -41,18 +77,18 @@ export function New() {
>
<input
disabled={noSelected}
placeholder="Preset's Name"
placeholder={placeholderText}
required
type="text"
value={name}
onChange={e => setName(e.target.value)}
/>
<button disabled={noSelected}>Save</button>
<button disabled={noSelected}>{saveText}</button>
</form>
{noSelected && (
<p className={styles.noSelected}>
To make a preset, first select some sounds.
{noSelectedText}
</p>
)}
</div>

View file

@ -1,18 +1,52 @@
import { useState, useEffect } from 'react';
import { Modal } from '@/components/modal';
import { New } from './new';
import { List } from './list';
import styles from './presets.module.css';
// 安全地获取localStorage
function getLocalStorageItem(key: string, defaultValue: string = 'en'): string {
if (typeof window !== 'undefined' && window.localStorage) {
try {
return localStorage.getItem(key) || defaultValue;
} catch {
return defaultValue;
}
}
return defaultValue;
}
interface PresetsModalProps {
onClose: () => void;
show: boolean;
}
export function PresetsModal({ onClose, show }: PresetsModalProps) {
const [currentLang, setCurrentLang] = useState('en');
// 在客户端初始化语言
useEffect(() => {
const lang = getLocalStorageItem('moodist-language');
setCurrentLang(lang);
// 监听语言变化
const handleLanguageChange = (event: CustomEvent) => {
setCurrentLang(event.detail.language);
};
window.addEventListener('languageChanged', handleLanguageChange as EventListener);
return () => {
window.removeEventListener('languageChanged', handleLanguageChange as EventListener);
};
}, []);
const titleText = currentLang === 'zh' ? '预设' : 'Presets';
return (
<Modal show={show} onClose={onClose}>
<h2 className={styles.title}>Presets</h2>
<h2 className={styles.title}>{titleText}</h2>
<New />
<div className={styles.divider} />
<List close={onClose} />

View file

@ -8,6 +8,18 @@ import { useSoundStore } from '@/stores/sound';
import styles from './share-link.module.css';
// 安全地获取localStorage
function getLocalStorageItem(key: string, defaultValue: string = 'en'): string {
if (typeof window !== 'undefined' && window.localStorage) {
try {
return localStorage.getItem(key) || defaultValue;
} catch {
return defaultValue;
}
}
return defaultValue;
}
interface ShareLinkModalProps {
onClose: () => void;
show: boolean;
@ -15,9 +27,27 @@ interface ShareLinkModalProps {
export function ShareLinkModal({ onClose, show }: ShareLinkModalProps) {
const [isMounted, setIsMounted] = useState(false);
const [currentLang, setCurrentLang] = useState('en');
const sounds = useSoundStore(state => state.sounds);
const { copy, copying } = useCopy();
// 在客户端初始化语言
useEffect(() => {
const lang = getLocalStorageItem('moodist-language');
setCurrentLang(lang);
// 监听语言变化
const handleLanguageChange = (event: CustomEvent) => {
setCurrentLang(event.detail.language);
};
window.addEventListener('languageChanged', handleLanguageChange as EventListener);
return () => {
window.removeEventListener('languageChanged', handleLanguageChange as EventListener);
};
}, []);
const selected = useMemo(() => {
return Object.keys(sounds)
.map(sound => ({
@ -49,12 +79,17 @@ export function ShareLinkModal({ onClose, show }: ShareLinkModalProps) {
useEffect(() => setIsMounted(true), []);
// 获取本地化文本
const headingText = currentLang === 'zh' ? '分享您的音效选择!' : 'Share your sound selection!';
const descText = currentLang === 'zh'
? '复制以下链接并发送给您想要分享选择的人。'
: 'Copy and send the following link to the person you want to share your selection with.';
return (
<Modal show={show} onClose={onClose}>
<h1 className={styles.heading}>Share your sound selection!</h1>
<h1 className={styles.heading}>{headingText}</h1>
<p className={styles.desc}>
Copy and send the following link to the person you want to share your
selection with.
{descText}
</p>
<div className={styles.inputWrapper}>
<input readOnly type="text" value={url} />

View file

@ -1,63 +1,97 @@
import { useState, useEffect } from 'react';
import { Modal } from '@/components/modal';
import styles from './shortcuts.module.css';
// 安全地获取localStorage
function getLocalStorageItem(key: string, defaultValue: string = 'en'): string {
if (typeof window !== 'undefined' && window.localStorage) {
try {
return localStorage.getItem(key) || defaultValue;
} catch {
return defaultValue;
}
}
return defaultValue;
}
interface ShortcutsModalProps {
onClose: () => void;
show: boolean;
}
export function ShortcutsModal({ onClose, show }: ShortcutsModalProps) {
const [currentLang, setCurrentLang] = useState('en');
// 在客户端初始化语言
useEffect(() => {
const lang = getLocalStorageItem('moodist-language');
setCurrentLang(lang);
// 监听语言变化
const handleLanguageChange = (event: CustomEvent) => {
setCurrentLang(event.detail.language);
};
window.addEventListener('languageChanged', handleLanguageChange as EventListener);
return () => {
window.removeEventListener('languageChanged', handleLanguageChange as EventListener);
};
}, []);
const shortcuts = [
{
keys: ['Shift', 'H'],
label: 'Shortcuts List',
label: currentLang === 'zh' ? '快捷键列表' : 'Shortcuts List',
},
{
keys: ['Shift', 'Alt', 'P'],
label: 'Presets',
label: currentLang === 'zh' ? '预设' : 'Presets',
},
{
keys: ['Shift', 'S'],
label: 'Share Sounds',
label: currentLang === 'zh' ? '分享音效' : 'Share Sounds',
},
{
keys: ['Shift', 'Alt', 'T'],
label: 'Sleep Timer',
label: currentLang === 'zh' ? '睡眠定时器' : 'Sleep Timer',
},
{
keys: ['Shift', 'C'],
label: 'Countdown Timer',
label: currentLang === 'zh' ? '倒计时' : 'Countdown Timer',
},
{
keys: ['Shift', 'P'],
label: 'Pomodoro',
label: currentLang === 'zh' ? '番茄钟' : 'Pomodoro',
},
{
keys: ['Shift', 'N'],
label: 'Notepad',
label: currentLang === 'zh' ? '记事本' : 'Notepad',
},
{
keys: ['Shift', 'T'],
label: 'Todo Checklist',
label: currentLang === 'zh' ? '待办清单' : 'Todo Checklist',
},
{
keys: ['Shift', 'B'],
label: 'Breathing Exercise',
label: currentLang === 'zh' ? '呼吸练习' : 'Breathing Exercise',
},
{
keys: ['Shift', 'Space'],
label: 'Toggle Play',
label: currentLang === 'zh' ? '切换播放' : 'Toggle Play',
},
{
keys: ['Shift', 'R'],
label: 'Unselect All Sounds',
label: currentLang === 'zh' ? '取消全选音效' : 'Unselect All Sounds',
},
];
const headingText = currentLang === 'zh' ? '键盘快捷键' : 'Keyboard Shortcuts';
return (
<Modal show={show} onClose={onClose}>
<h1 className={styles.heading}>Keyboard Shortcuts</h1>
<h1 className={styles.heading}>{headingText}</h1>
<div className={styles.shortcuts}>
{shortcuts.map(shortcut => (
<Row

View file

@ -10,6 +10,18 @@ import { useSleepTimerStore } from '@/stores/sleep-timer';
import styles from './sleep-timer.module.css';
// 安全地获取localStorage
function getLocalStorageItem(key: string, defaultValue: string = 'en'): string {
if (typeof window !== 'undefined' && window.localStorage) {
try {
return localStorage.getItem(key) || defaultValue;
} catch {
return defaultValue;
}
}
return defaultValue;
}
interface SleepTimerModalProps {
onClose: () => void;
show: boolean;
@ -18,9 +30,27 @@ interface SleepTimerModalProps {
export function SleepTimerModal({ onClose, show }: SleepTimerModalProps) {
const setActive = useSleepTimerStore(state => state.set);
const noSelected = useSoundStore(state => state.noSelected());
const [currentLang, setCurrentLang] = useState('en');
const [running, setRunning] = useState(false);
// 在客户端初始化语言
useEffect(() => {
const lang = getLocalStorageItem('moodist-language');
setCurrentLang(lang);
// 监听语言变化
const handleLanguageChange = (event: CustomEvent) => {
setCurrentLang(event.detail.language);
};
window.addEventListener('languageChanged', handleLanguageChange as EventListener);
return () => {
window.removeEventListener('languageChanged', handleLanguageChange as EventListener);
};
}, []);
useEffect(() => setActive(running), [running, setActive]);
const [hours, setHours] = useState<string>('0');
@ -88,12 +118,20 @@ export function SleepTimerModal({ onClose, show }: SleepTimerModalProps) {
handleStart();
};
// 获取本地化文本
const titleText = currentLang === 'zh' ? '睡眠定时器' : 'Sleep Timer';
const descText = currentLang === 'zh' ? '在指定时间后停止音效。' : 'Stop sounds after a certain amount of time.';
const hoursText = currentLang === 'zh' ? '小时' : 'Hours';
const minutesText = currentLang === 'zh' ? '分钟' : 'Minutes';
const resetText = currentLang === 'zh' ? '重置' : 'Reset';
const startText = currentLang === 'zh' ? '开始' : 'Start';
return (
<Modal show={show} onClose={onClose}>
<header className={styles.header}>
<h2 className={styles.title}>Sleep Timer</h2>
<h2 className={styles.title}>{titleText}</h2>
<p className={styles.desc}>
Stop sounds after a certain amount of time.
{descText}
</p>
</header>
@ -101,11 +139,11 @@ export function SleepTimerModal({ onClose, show }: SleepTimerModalProps) {
<div className={styles.controls}>
<div className={styles.inputs}>
{!running && (
<Field label="Hours" value={hours} onChange={setHours} />
<Field label={hoursText} value={hours} onChange={setHours} />
)}
{!running && (
<Field label="Minutes" value={minutes} onChange={setMinutes} />
<Field label={minutesText} value={minutes} onChange={setMinutes} />
)}
</div>
@ -118,7 +156,7 @@ export function SleepTimerModal({ onClose, show }: SleepTimerModalProps) {
type="button"
onClick={handleReset}
>
Reset
{resetText}
</button>
)}
@ -127,7 +165,7 @@ export function SleepTimerModal({ onClose, show }: SleepTimerModalProps) {
className={cn(styles.button, styles.primary)}
type="submit"
>
Start
{startText}
</button>
)}
</div>

View file

@ -1,8 +1,9 @@
import { useCallback, useEffect, forwardRef, useMemo } from 'react';
import { useCallback, useEffect, forwardRef, useMemo, useState } from 'react';
import { ImSpinner9 } from 'react-icons/im/index';
import { Range } from './range';
import { Favorite } from './favorite';
import { getLocalizedSoundLabel } from '@/utils/sound-labels';
import { useSound } from '@/hooks/use-sound';
import { useSoundStore } from '@/stores/sound';
@ -22,10 +23,23 @@ interface SoundProps extends SoundType {
unselectHidden: (key: string) => void;
}
// 安全地获取localStorage
function getLocalStorageItem(key: string, defaultValue: string = 'en'): string {
if (typeof window !== 'undefined' && window.localStorage) {
try {
return localStorage.getItem(key) || defaultValue;
} catch {
return defaultValue;
}
}
return defaultValue;
}
export const Sound = forwardRef<HTMLDivElement, SoundProps>(function Sound(
{ functional, hidden, icon, id, label, selectHidden, src, unselectHidden },
ref,
) {
const [currentLang, setCurrentLang] = useState('en');
const isPlaying = useSoundStore(state => state.isPlaying);
const play = useSoundStore(state => state.play);
const selectSound = useSoundStore(state => state.select);
@ -45,6 +59,23 @@ export const Sound = forwardRef<HTMLDivElement, SoundProps>(function Sound(
const sound = useSound(src, { loop: true, volume: adjustedVolume });
// 在客户端初始化语言
useEffect(() => {
const lang = getLocalStorageItem('moodist-language');
setCurrentLang(lang);
// 监听语言变化
const handleLanguageChange = (event: CustomEvent) => {
setCurrentLang(event.detail.language);
};
window.addEventListener('languageChanged', handleLanguageChange as EventListener);
return () => {
window.removeEventListener('languageChanged', handleLanguageChange as EventListener);
};
}, []);
useEffect(() => {
if (locked) return;
@ -86,9 +117,12 @@ export const Sound = forwardRef<HTMLDivElement, SoundProps>(function Sound(
toggle();
});
// 获取本地化的音效标签
const localizedLabel = getLocalizedSoundLabel(id);
return (
<div
aria-label={`${label} sound`}
aria-label={`${localizedLabel} sound`}
ref={ref}
role="button"
tabIndex={0}
@ -100,7 +134,7 @@ export const Sound = forwardRef<HTMLDivElement, SoundProps>(function Sound(
onClick={handleClick}
onKeyDown={handleKeyDown}
>
<Favorite id={id} label={label} />
<Favorite id={id} label={localizedLabel} />
<div className={styles.icon}>
{isLoading ? (
<span aria-hidden="true" className={styles.spinner}>
@ -111,9 +145,9 @@ export const Sound = forwardRef<HTMLDivElement, SoundProps>(function Sound(
)}
</div>
<div className={styles.label} id={id}>
{label}
{localizedLabel}
</div>
<Range id={id} label={label} />
<Range id={id} label={localizedLabel} />
</div>
);
});

View file

@ -1,22 +1,35 @@
import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { AnimatePresence, motion } from 'motion/react';
import { Sound } from './sound';
import { useLocalStorage } from '@/hooks/use-local-storage';
import { cn } from '@/helpers/styles';
import { fade, scale, mix } from '@/lib/motion';
import { fade, mix, scale } from '@/lib/motion';
import styles from './sounds.module.css';
import type { Sounds } from '@/data/types';
import type { Sound as SoundType } from '@/data/types';
interface SoundsProps {
functional: boolean;
id: string;
sounds: Sounds;
sounds: SoundType[];
}
// 安全地获取localStorage
function getLocalStorageItem(key: string, defaultValue: string = 'en'): string {
if (typeof window !== 'undefined' && window.localStorage) {
try {
return localStorage.getItem(key) || defaultValue;
} catch {
return defaultValue;
}
}
return defaultValue;
}
export function Sounds({ functional, id, sounds }: SoundsProps) {
const [currentLang, setCurrentLang] = useState('en');
const [showAll, setShowAll] = useLocalStorage(`${id}-show-more`, false);
const [clickedMore, setClickedMore] = useState(false);
@ -24,6 +37,23 @@ export function Sounds({ functional, id, sounds }: SoundsProps) {
const firstNewSound = useRef<HTMLDivElement>(null);
// 在客户端初始化语言
useEffect(() => {
const lang = getLocalStorageItem('moodist-language');
setCurrentLang(lang);
// 监听语言变化
const handleLanguageChange = (event: CustomEvent) => {
setCurrentLang(event.detail.language);
};
window.addEventListener('languageChanged', handleLanguageChange as EventListener);
return () => {
window.removeEventListener('languageChanged', handleLanguageChange as EventListener);
};
}, []);
useEffect(() => {
if (showAll && clickedMore) {
firstNewSound.current?.focus();
@ -66,6 +96,10 @@ export function Sounds({ functional, id, sounds }: SoundsProps) {
const variants = mix(fade(), scale(0.9));
// 获取本地化文本
const showMoreText = currentLang === 'zh' ? '显示更多' : 'Show More';
const showLessText = currentLang === 'zh' ? '收起' : 'Show Less';
return (
<div>
<div className={styles.sounds}>
@ -106,7 +140,7 @@ export function Sounds({ functional, id, sounds }: SoundsProps) {
onAnimationComplete={() => setIsAnimating(false)}
onAnimationStart={() => setIsAnimating(true)}
>
{showAll ? 'Show Less' : 'Show More'}
{showAll ? showLessText : showMoreText}
</motion.span>
</AnimatePresence>
</button>

View file

@ -16,12 +16,19 @@ import Binary from './binary.astro';
</div>
</div>
<h2 class="title"><span>Open Source</span></h2>
<p class="desc">Moodist is free and open-source!</p>
<h2 class="title">
<span data-lang="en">Open Source</span>
<span data-lang="zh">开源项目</span>
</h2>
<p class="desc">
<span data-lang="en">Moodist is free and open-source!</span>
<span data-lang="zh">Moodist免费且开源</span>
</p>
<div class="button">
<SpecialButton href="https://github.com/remvze/moodist">
Source Code
<span data-lang="en">Source Code</span>
<span data-lang="zh">源代码</span>
</SpecialButton>
</div>
@ -30,6 +37,47 @@ import Binary from './binary.astro';
</Container>
</div>
<script>
// Language switching logic
function getCurrentLanguage() {
if (typeof window !== 'undefined' && window.localStorage) {
try {
return localStorage.getItem('moodist-language') || 'en';
} catch {
return 'en';
}
}
return 'en';
}
function updateLanguageDisplay(lang) {
const elements = document.querySelectorAll(`[data-lang="${lang}"]`);
elements.forEach(el => {
if (el instanceof HTMLElement) {
el.style.display = 'block';
}
});
const otherElements = document.querySelectorAll(`[data-lang="${lang === 'en' ? 'zh' : 'en'}"]`);
otherElements.forEach(el => {
if (el instanceof HTMLElement) {
el.style.display = 'none';
}
});
}
// Initialize with current language
document.addEventListener('DOMContentLoaded', () => {
const currentLang = getCurrentLanguage();
updateLanguageDisplay(currentLang);
// Listen for language changes from other components
window.addEventListener('languageChanged', (event) => {
updateLanguageDisplay(event.detail.language);
});
});
</script>
<style>
.source {
margin-top: 40px;

View file

@ -1,13 +1,16 @@
import { FaHeadphonesAlt } from 'react-icons/fa/index';
import { Item } from '../item';
import { getLocalizedText } from '@/utils/language';
interface BinauralProps {
open: () => void;
}
export function Binaural({ open }: BinauralProps) {
const label = getLocalizedText('binaural');
return (
<Item icon={<FaHeadphonesAlt />} label="Binaural Beats" onClick={open} />
<Item icon={<FaHeadphonesAlt />} label={label} onClick={open} />
);
}

View file

@ -1,16 +1,19 @@
import { IoMdFlower } from 'react-icons/io/index';
import { Item } from '../item';
import { getLocalizedText } from '@/utils/language';
interface BreathingExerciseProps {
open: () => void;
}
export function BreathingExercise({ open }: BreathingExerciseProps) {
const label = getLocalizedText('breathingExercise');
return (
<Item
icon={<IoMdFlower />}
label="Breathing Exercise"
label={label}
shortcut="Shift + B"
onClick={open}
/>

View file

@ -1,16 +1,19 @@
import { MdOutlineTimer } from 'react-icons/md/index';
import { Item } from '../item';
import { getLocalizedText } from '@/utils/language';
interface CountdownProps {
open: () => void;
}
export function Countdown({ open }: CountdownProps) {
const label = getLocalizedText('countdown');
return (
<Item
icon={<MdOutlineTimer />}
label="Countdown Timer"
label={label}
shortcut="Shift + C"
onClick={open}
/>

View file

@ -1,13 +1,16 @@
import { SiBuymeacoffee } from 'react-icons/si/index';
import { Item } from '../item';
import { getLocalizedText } from '@/utils/language';
export function Donate() {
const label = getLocalizedText('buyMeCoffee');
return (
<Item
href="https://buymeacoffee.com/remvze"
icon={<SiBuymeacoffee />}
label="Buy Me a Coffee"
label={label}
/>
);
}

View file

@ -1,11 +1,14 @@
import { TbWaveSine } from 'react-icons/tb/index';
import { Item } from '../item';
import { getLocalizedText } from '@/utils/language';
interface IsochronicProps {
open: () => void;
}
export function Isochronic({ open }: IsochronicProps) {
return <Item icon={<TbWaveSine />} label="Isochronic Tones" onClick={open} />;
const label = getLocalizedText('isochronic');
return <Item icon={<TbWaveSine />} label={label} onClick={open} />;
}

View file

@ -1,16 +1,19 @@
import { IoIosMusicalNote } from 'react-icons/io/index';
import { Item } from '../item';
import { getLocalizedText } from '@/utils/language';
interface LofiProps {
open: () => void;
}
export function Lofi({ open }: LofiProps) {
const label = getLocalizedText('lofi');
return (
<Item
icon={<IoIosMusicalNote />}
label="Lofi Music Player"
label={label}
onClick={open}
/>
);

View file

@ -1,6 +1,7 @@
import { MdNotes } from 'react-icons/md/index';
import { Item } from '../item';
import { getLocalizedText } from '@/utils/language';
import { useNoteStore } from '@/stores/note';
@ -10,12 +11,13 @@ interface NotepadProps {
export function Notepad({ open }: NotepadProps) {
const note = useNoteStore(state => state.note);
const label = getLocalizedText('notepad');
return (
<Item
active={!!note.length}
icon={<MdNotes />}
label="Notepad"
label={label}
shortcut="Shift + N"
onClick={open}
/>

View file

@ -1,6 +1,7 @@
import { MdOutlineAvTimer } from 'react-icons/md/index';
import { Item } from '../item';
import { getLocalizedText } from '@/utils/language';
import { usePomodoroStore } from '@/stores/pomodoro';
@ -10,12 +11,13 @@ interface PomodoroProps {
export function Pomodoro({ open }: PomodoroProps) {
const running = usePomodoroStore(state => state.running);
const label = getLocalizedText('pomodoro');
return (
<Item
active={running}
icon={<MdOutlineAvTimer />}
label="Pomodoro"
label={label}
shortcut="Shift + P"
onClick={open}
/>

View file

@ -1,16 +1,19 @@
import { RiPlayListFill } from 'react-icons/ri/index';
import { Item } from '../item';
import { getLocalizedText } from '@/utils/language';
interface PresetsProps {
open: () => void;
}
export function Presets({ open }: PresetsProps) {
const label = getLocalizedText('presets');
return (
<Item
icon={<RiPlayListFill />}
label="Your Presets"
label={label}
shortcut="Shift + Alt + P"
onClick={open}
/>

View file

@ -1,6 +1,7 @@
import { IoShareSocialSharp } from 'react-icons/io5/index';
import { Item } from '../item';
import { getLocalizedText } from '@/utils/language';
import { useSoundStore } from '@/stores/sound';
@ -10,12 +11,13 @@ interface ShareProps {
export function Share({ open }: ShareProps) {
const noSelected = useSoundStore(state => state.noSelected());
const label = getLocalizedText('share');
return (
<Item
disabled={noSelected}
icon={<IoShareSocialSharp />}
label="Share Sounds"
label={label}
shortcut="Shift + S"
onClick={open}
/>

View file

@ -1,16 +1,19 @@
import { MdKeyboardCommandKey } from 'react-icons/md/index';
import { Item } from '../item';
import { getLocalizedText } from '@/utils/language';
interface ShortcutsProps {
open: () => void;
}
export function Shortcuts({ open }: ShortcutsProps) {
const label = getLocalizedText('shortcuts');
return (
<Item
icon={<MdKeyboardCommandKey />}
label="Shortcuts"
label={label}
shortcut="Shift + H"
onClick={open}
/>

View file

@ -1,18 +1,20 @@
import { BiShuffle } from 'react-icons/bi/index';
import { useSoundStore } from '@/stores/sound';
import { getLocalizedText } from '@/utils/language';
import { Item } from '../item';
export function Shuffle() {
const shuffle = useSoundStore(state => state.shuffle);
const locked = useSoundStore(state => state.locked);
const label = getLocalizedText('shuffle');
return (
<Item
disabled={locked}
icon={<BiShuffle />}
label="Shuffle Sounds"
label={label}
onClick={shuffle}
/>
);

View file

@ -2,6 +2,7 @@ import { IoMoonSharp } from 'react-icons/io5/index';
import { useSleepTimerStore } from '@/stores/sleep-timer';
import { Item } from '../item';
import { getLocalizedText } from '@/utils/language';
interface SleepTimerProps {
open: () => void;
@ -9,12 +10,13 @@ interface SleepTimerProps {
export function SleepTimer({ open }: SleepTimerProps) {
const active = useSleepTimerStore(state => state.active);
const label = getLocalizedText('sleepTimer');
return (
<Item
active={active}
icon={<IoMoonSharp />}
label="Sleep Timer"
label={label}
shortcut="Shift + Alt + T"
onClick={open}
/>

View file

@ -1,13 +1,16 @@
import { LuGithub } from 'react-icons/lu/index';
import { Item } from '../item';
import { getLocalizedText } from '@/utils/language';
export function Source() {
const label = getLocalizedText('sourceCode');
return (
<Item
href="https://github.com/remvze/moodist"
icon={<LuGithub />}
label="Source Code"
label={label}
/>
);
}

View file

@ -1,16 +1,19 @@
import { MdTaskAlt } from 'react-icons/md/index';
import { Item } from '../item';
import { getLocalizedText } from '@/utils/language';
interface TodoProps {
open: () => void;
}
export function Todo({ open }: TodoProps) {
const label = getLocalizedText('todo');
return (
<Item
icon={<MdTaskAlt />}
label="Todo Checklist"
label={label}
shortcut="Shift + T"
onClick={open}
/>

View file

@ -35,6 +35,7 @@ import { Slider } from '@/components/slider';
import { fade, mix, slideY } from '@/lib/motion';
import { useSoundStore } from '@/stores/sound';
import { getLocalizedText } from '@/utils/language';
import styles from './menu.module.css';
import { useCloseListener } from '@/hooks/use-close-listener';
@ -147,7 +148,7 @@ export function Menu() {
<Divider />
<div className={styles.globalVolume}>
<label htmlFor="global-volume">Global Volume</label>
<label htmlFor="global-volume">{getLocalizedText('globalVolume')}</label>
<Slider
max={100}
min={0}

View file

@ -8,6 +8,18 @@ import { padNumber } from '@/helpers/number';
import styles from './countdown.module.css';
// 安全地获取localStorage
function getLocalStorageItem(key: string, defaultValue: string = 'en'): string {
if (typeof window !== 'undefined' && window.localStorage) {
try {
return localStorage.getItem(key) || defaultValue;
} catch {
return defaultValue;
}
}
return defaultValue;
}
interface CountdownProps {
onClose: () => void;
show: boolean;
@ -21,9 +33,27 @@ export function Countdown({ onClose, show }: CountdownProps) {
const [initialTime, setInitialTime] = useState(0);
const [isActive, setIsActive] = useState(false);
const [isFormVisible, setIsFormVisible] = useState(true);
const [currentLang, setCurrentLang] = useState('en');
const alarm = useSoundEffect('/sounds/alarm.mp3');
// 在客户端初始化语言
useEffect(() => {
const lang = getLocalStorageItem('moodist-language');
setCurrentLang(lang);
// 监听语言变化
const handleLanguageChange = (event: CustomEvent) => {
setCurrentLang(event.detail.language);
};
window.addEventListener('languageChanged', handleLanguageChange as EventListener);
return () => {
window.removeEventListener('languageChanged', handleLanguageChange as EventListener);
};
}, []);
useEffect(() => {
let timer: NodeJS.Timeout;
@ -70,11 +100,18 @@ export function Countdown({ onClose, show }: CountdownProps) {
const elapsedTime = initialTime - timeLeft;
// 获取本地化文本
const titleText = currentLang === 'zh' ? '倒计时定时器' : 'Countdown Timer';
const descText = currentLang === 'zh' ? '超级简单的倒计时定时器。' : 'Super simple countdown timer.';
const startText = currentLang === 'zh' ? '开始' : 'Start';
const backText = currentLang === 'zh' ? '返回' : 'Back';
const pauseText = currentLang === 'zh' ? '暂停' : 'Pause';
return (
<Modal show={show} onClose={onClose}>
<header className={styles.header}>
<h2 className={styles.title}>Countdown Timer</h2>
<p className={styles.desc}>Super simple countdown timer.</p>
<h2 className={styles.title}>{titleText}</h2>
<p className={styles.desc}>{descText}</p>
</header>
{isFormVisible ? (
@ -118,7 +155,7 @@ export function Countdown({ onClose, show }: CountdownProps) {
className={cn(styles.button, styles.primary)}
onClick={handleStart}
>
Start
{startText}
</button>
</div>
</div>
@ -131,14 +168,14 @@ export function Countdown({ onClose, show }: CountdownProps) {
<div className={styles.buttonContainer}>
<button className={styles.button} onClick={handleBack}>
Back
{backText}
</button>
<button
className={cn(styles.button, styles.primary)}
onClick={toggleTimer}
>
{isActive ? 'Pause' : 'Start'}
{isActive ? pauseText : startText}
</button>
</div>
</div>

View file

@ -1,4 +1,4 @@
import { useRef, useEffect } from 'react';
import { useRef, useEffect, useState } from 'react';
import { BiTrash } from 'react-icons/bi/index';
import { LuCopy, LuDownload } from 'react-icons/lu/index';
import { FaCheck } from 'react-icons/fa6/index';
@ -13,12 +13,25 @@ import { download } from '@/helpers/download';
import styles from './notepad.module.css';
// 安全地获取localStorage
function getLocalStorageItem(key: string, defaultValue: string = 'en'): string {
if (typeof window !== 'undefined' && window.localStorage) {
try {
return localStorage.getItem(key) || defaultValue;
} catch {
return defaultValue;
}
}
return defaultValue;
}
interface NotepadProps {
onClose: () => void;
show: boolean;
}
export function Notepad({ onClose, show }: NotepadProps) {
const [currentLang, setCurrentLang] = useState('en');
const textareaRef = useRef<HTMLTextAreaElement>(null);
const note = useNoteStore(state => state.note);
@ -31,6 +44,23 @@ export function Notepad({ onClose, show }: NotepadProps) {
const { copy, copying } = useCopy();
// 在客户端初始化语言
useEffect(() => {
const lang = getLocalStorageItem('moodist-language');
setCurrentLang(lang);
// 监听语言变化
const handleLanguageChange = (event: CustomEvent) => {
setCurrentLang(event.detail.language);
};
window.addEventListener('languageChanged', handleLanguageChange as EventListener);
return () => {
window.removeEventListener('languageChanged', handleLanguageChange as EventListener);
};
}, []);
useEffect(() => {
if (show && textareaRef.current) {
setTimeout(() => {
@ -45,26 +75,39 @@ export function Notepad({ onClose, show }: NotepadProps) {
if (e.key === 'Escape') onClose();
};
// 获取本地化文本
const labelText = currentLang === 'zh' ? '您的笔记' : 'Your Note';
const copyTooltip = currentLang === 'zh' ? '复制笔记' : 'Copy Note';
const downloadTooltip = currentLang === 'zh' ? '下载笔记' : 'Download Note';
const restoreTooltip = currentLang === 'zh' ? '恢复笔记' : 'Restore Note';
const clearTooltip = currentLang === 'zh' ? '清空笔记' : 'Clear Note';
const downloadFileName = currentLang === 'zh' ? 'Moodist笔记.txt' : 'Moodist Note.txt';
const placeholderText = currentLang === 'zh' ? '您在想什么?' : 'What is on your mind?';
const characterText = currentLang === 'zh' ? '字符' : 'character';
const charactersText = currentLang === 'zh' ? '字符' : 'characters';
const wordText = currentLang === 'zh' ? '词' : 'word';
const wordsText = currentLang === 'zh' ? '词' : 'words';
return (
<Modal show={show} wide onClose={onClose}>
<header className={styles.header}>
<h2 className={styles.label}>Your Note</h2>
<h2 className={styles.label}>{labelText}</h2>
<div className={styles.buttons}>
<Button
icon={copying ? <FaCheck /> : <LuCopy />}
tooltip="Copy Note"
tooltip={copyTooltip}
onClick={() => copy(note)}
/>
<Button
icon={<LuDownload />}
tooltip="Download Note"
onClick={() => download('Moodit Note.txt', note)}
tooltip={downloadTooltip}
onClick={() => download(downloadFileName, note)}
/>
<Button
critical={!history}
icon={history ? <FaUndo /> : <BiTrash />}
recommended={!!history}
tooltip={history ? 'Restore Note' : 'Clear Note'}
tooltip={history ? restoreTooltip : clearTooltip}
onClick={() => (history ? restore() : clear())}
/>
</div>
@ -73,7 +116,7 @@ export function Notepad({ onClose, show }: NotepadProps) {
<textarea
className={styles.textarea}
dir="auto"
placeholder="What is on your mind?"
placeholder={placeholderText}
ref={textareaRef}
spellCheck={false}
value={note}
@ -82,8 +125,7 @@ export function Notepad({ onClose, show }: NotepadProps) {
/>
<p className={styles.counter}>
{characters} character{characters !== 1 && 's'} {words} word
{words !== 1 && 's'}
{characters} {characters === 1 ? characterText : charactersText} {words} {words === 1 ? wordText : wordsText}
</p>
</Modal>
);

View file

@ -15,6 +15,18 @@ import { useCloseListener } from '@/hooks/use-close-listener';
import styles from './pomodoro.module.css';
// 安全地获取localStorage
function getLocalStorageItem(key: string, defaultValue: string = 'en'): string {
if (typeof window !== 'undefined' && window.localStorage) {
try {
return localStorage.getItem(key) || defaultValue;
} catch {
return defaultValue;
}
}
return defaultValue;
}
interface PomodoroProps {
onClose: () => void;
open: () => void;
@ -22,8 +34,34 @@ interface PomodoroProps {
}
export function Pomodoro({ onClose, open, show }: PomodoroProps) {
const [currentLang, setCurrentLang] = useState('en');
const [showSetting, setShowSetting] = useState(false);
// 在客户端初始化语言
useEffect(() => {
const lang = getLocalStorageItem('moodist-language');
setCurrentLang(lang);
// 监听语言变化
const handleLanguageChange = (event: CustomEvent) => {
setCurrentLang(event.detail.language);
};
window.addEventListener('languageChanged', handleLanguageChange as EventListener);
return () => {
window.removeEventListener('languageChanged', handleLanguageChange as EventListener);
};
}, []);
// 获取本地化文本
const titleText = currentLang === 'zh' ? '番茄钟定时器' : 'Pomodoro Timer';
const changeTimesTooltip = currentLang === 'zh' ? '更改时间' : 'Change Times';
const completedText = currentLang === 'zh' ? '已完成' : 'completed';
const restartTooltip = currentLang === 'zh' ? '重新开始' : 'Restart';
const pauseTooltip = currentLang === 'zh' ? '暂停' : 'Pause';
const startTooltip = currentLang === 'zh' ? '开始' : 'Start';
const [selectedTab, setSelectedTab] = useState('pomodoro');
const running = usePomodoroStore(state => state.running);
@ -56,11 +94,11 @@ export function Pomodoro({ onClose, open, show }: PomodoroProps) {
const tabs = useMemo(
() => [
{ id: 'pomodoro', label: 'Pomodoro' },
{ id: 'short', label: 'Break' },
{ id: 'long', label: 'Long Break' },
{ id: 'pomodoro', label: currentLang === 'zh' ? '番茄钟' : 'Pomodoro' },
{ id: 'short', label: currentLang === 'zh' ? '短休息' : 'Break' },
{ id: 'long', label: currentLang === 'zh' ? '长休息' : 'Long Break' },
],
[],
[currentLang],
);
useCloseListener(() => setShowSetting(false));
@ -123,12 +161,12 @@ export function Pomodoro({ onClose, open, show }: PomodoroProps) {
<>
<Modal show={show} onClose={onClose}>
<header className={styles.header}>
<h2 className={styles.title}>Pomodoro Timer</h2>
<h2 className={styles.title}>{titleText}</h2>
<div className={styles.button}>
<Button
icon={<IoMdSettings />}
tooltip="Change Times"
tooltip={changeTimesTooltip}
onClick={() => {
onClose();
setShowSetting(true);
@ -142,19 +180,19 @@ export function Pomodoro({ onClose, open, show }: PomodoroProps) {
<div className={styles.control}>
<p className={styles.completed}>
{completions[selectedTab] || 0} completed
{completions[selectedTab] || 0} {completedText}
</p>
<div className={styles.buttons}>
<Button
icon={<FaUndo />}
smallIcon
tooltip="Restart"
tooltip={restartTooltip}
onClick={restart}
/>
<Button
icon={running ? <FaPause /> : <FaPlay />}
smallIcon
tooltip={running ? 'Pause' : 'Start'}
tooltip={running ? pauseTooltip : startTooltip}
onClick={toggleRunning}
/>
</div>

View file

@ -4,6 +4,18 @@ import { Modal } from '@/components/modal';
import styles from './setting.module.css';
// 安全地获取localStorage
function getLocalStorageItem(key: string, defaultValue: string = 'en'): string {
if (typeof window !== 'undefined' && window.localStorage) {
try {
return localStorage.getItem(key) || defaultValue;
} catch {
return defaultValue;
}
}
return defaultValue;
}
interface SettingProps {
onChange: (newTimes: Record<string, number>) => void;
onClose: () => void;
@ -13,6 +25,24 @@ interface SettingProps {
export function Setting({ onChange, onClose, show, times }: SettingProps) {
const [values, setValues] = useState<Record<string, number | string>>(times);
const [currentLang, setCurrentLang] = useState('en');
// 在客户端初始化语言
useEffect(() => {
const lang = getLocalStorageItem('moodist-language');
setCurrentLang(lang);
// 监听语言变化
const handleLanguageChange = (event: CustomEvent) => {
setCurrentLang(event.detail.language);
};
window.addEventListener('languageChanged', handleLanguageChange as EventListener);
return () => {
window.removeEventListener('languageChanged', handleLanguageChange as EventListener);
};
}, []);
useEffect(() => {
if (show) setValues(times);
@ -44,36 +74,44 @@ export function Setting({ onChange, onClose, show, times }: SettingProps) {
onClose();
};
// 获取本地化文本
const titleText = currentLang === 'zh' ? '更改时间' : 'Change Times';
const pomodoroLabel = currentLang === 'zh' ? '番茄钟' : 'Pomodoro';
const shortBreakLabel = currentLang === 'zh' ? '短休息' : 'Short Break';
const longBreakLabel = currentLang === 'zh' ? '长休息' : 'Long Break';
const cancelText = currentLang === 'zh' ? '取消' : 'Cancel';
const saveText = currentLang === 'zh' ? '保存' : 'Save';
return (
<Modal lockBody={false} show={show} onClose={onClose}>
<h2 className={styles.title}>Change Times</h2>
<h2 className={styles.title}>{titleText}</h2>
<form className={styles.form} onSubmit={handleSubmit}>
<Field
id="pomodoro"
label="Pomodoro"
label={pomodoroLabel}
value={values.pomodoro}
onChange={handleChange('pomodoro')}
/>
<Field
id="short"
label="Short Break"
label={shortBreakLabel}
value={values.short}
onChange={handleChange('short')}
/>
<Field
id="long"
label="Long Break"
label={longBreakLabel}
value={values.long}
onChange={handleChange('long')}
/>
<div className={styles.buttons}>
<button type="button" onClick={handleCancel}>
Cancel
{cancelText}
</button>
<button className={styles.primary} type="submit">
Save
{saveText}
</button>
</div>
</form>

View file

@ -1,14 +1,44 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { useTodoStore } from '@/stores/todo';
import styles from './form.module.css';
// 安全地获取localStorage
function getLocalStorageItem(key: string, defaultValue: string = 'en'): string {
if (typeof window !== 'undefined' && window.localStorage) {
try {
return localStorage.getItem(key) || defaultValue;
} catch {
return defaultValue;
}
}
return defaultValue;
}
export function Form() {
const [value, setValue] = useState('');
const [currentLang, setCurrentLang] = useState('en');
const addTodo = useTodoStore(state => state.addTodo);
// 在客户端初始化语言
useEffect(() => {
const lang = getLocalStorageItem('moodist-language');
setCurrentLang(lang);
// 监听语言变化
const handleLanguageChange = (event: CustomEvent) => {
setCurrentLang(event.detail.language);
};
window.addEventListener('languageChanged', handleLanguageChange as EventListener);
return () => {
window.removeEventListener('languageChanged', handleLanguageChange as EventListener);
};
}, []);
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
@ -18,16 +48,20 @@ export function Form() {
setValue('');
};
// 获取本地化文本
const placeholderText = currentLang === 'zh' ? '我必须...' : 'I have to ...';
const addText = currentLang === 'zh' ? '添加' : 'Add';
return (
<form onSubmit={handleSubmit}>
<div className={styles.wrapper}>
<input
placeholder="I have to ..."
placeholder={placeholderText}
type="text"
value={value}
onChange={e => setValue(e.target.value)}
/>
<button type="submit">Add</button>
<button type="submit">{addText}</button>
</div>
</form>
);

View file

@ -1,20 +1,57 @@
import { useState, useEffect } from 'react';
import { Modal } from '@/components/modal';
import { Form } from './form';
import { Todos } from './todos';
import styles from './todo.module.css';
// 安全地获取localStorage
function getLocalStorageItem(key: string, defaultValue: string = 'en'): string {
if (typeof window !== 'undefined' && window.localStorage) {
try {
return localStorage.getItem(key) || defaultValue;
} catch {
return defaultValue;
}
}
return defaultValue;
}
interface TodoProps {
onClose: () => void;
show: boolean;
}
export function Todo({ onClose, show }: TodoProps) {
const [currentLang, setCurrentLang] = useState('en');
// 在客户端初始化语言
useEffect(() => {
const lang = getLocalStorageItem('moodist-language');
setCurrentLang(lang);
// 监听语言变化
const handleLanguageChange = (event: CustomEvent) => {
setCurrentLang(event.detail.language);
};
window.addEventListener('languageChanged', handleLanguageChange as EventListener);
return () => {
window.removeEventListener('languageChanged', handleLanguageChange as EventListener);
};
}, []);
// 获取本地化文本
const titleText = currentLang === 'zh' ? '待办清单' : 'Todo Checklist';
const descText = currentLang === 'zh' ? '超级简单的待办清单。' : 'Super simple todo list.';
return (
<Modal show={show} onClose={onClose}>
<header className={styles.header}>
<h2 className={styles.title}>Todo Checklist</h2>
<p className={styles.desc}>Super simple todo list.</p>
<h2 className={styles.title}>{titleText}</h2>
<p className={styles.desc}>{descText}</p>
</header>
<Form />

View file

@ -1,17 +1,52 @@
import { Todo } from './todo';
import { useState, useEffect } from 'react';
import { useTodoStore } from '@/stores/todo';
import { Todo } from './todo';
import styles from './todos.module.css';
// 安全地获取localStorage
function getLocalStorageItem(key: string, defaultValue: string = 'en'): string {
if (typeof window !== 'undefined' && window.localStorage) {
try {
return localStorage.getItem(key) || defaultValue;
} catch {
return defaultValue;
}
}
return defaultValue;
}
export function Todos() {
const [currentLang, setCurrentLang] = useState('en');
const todos = useTodoStore(state => state.todos);
const doneCount = useTodoStore(state => state.doneCount());
// 在客户端初始化语言
useEffect(() => {
const lang = getLocalStorageItem('moodist-language');
setCurrentLang(lang);
// 监听语言变化
const handleLanguageChange = (event: CustomEvent) => {
setCurrentLang(event.detail.language);
};
window.addEventListener('languageChanged', handleLanguageChange as EventListener);
return () => {
window.removeEventListener('languageChanged', handleLanguageChange as EventListener);
};
}, []);
// 获取本地化文本
const labelText = currentLang === 'zh' ? '您的待办事项' : 'Your Todos';
const emptyText = currentLang === 'zh' ? '您没有任何待办事项。' : 'You don\'t have any todos.';
return (
<div className={styles.todos}>
<header>
<p className={styles.label}>Your Todos</p>
<p className={styles.label}>{labelText}</p>
<div className={styles.divider} />
<p className={styles.counter}>
{doneCount} / {todos.length}
@ -30,7 +65,7 @@ export function Todos() {
))}
</>
) : (
<p className={styles.empty}>You don&apos;t have any todos.</p>
<p className={styles.empty}>{emptyText}</p>
)}
</div>
);

210
src/constants/languages.ts Normal file
View file

@ -0,0 +1,210 @@
export const languages = {
en: {
// Hero section
hero: {
title: 'Ambient Sounds',
subtitle: 'For Focus and Calm',
description: 'Free and Open-Source.',
sounds: 'Sounds'
},
// About section
about: {
paragraphs: [
{
title: 'Free Ambient Sounds',
body: 'Craving a calming escape from the daily grind? Do you need the perfect soundscape to boost your focus or lull you into peaceful sleep? Look no further than Moodist, your free and open-source ambient sound generator! Ditch the subscriptions and registrations with Moodist, you unlock a world of soothing and immersive audio experiences, entirely for free.'
},
{
title: 'Carefully Curated Sounds',
body: 'Dive into an expansive library of carefully curated sounds. Nature lovers will find solace in the gentle murmur of streams, the rhythmic crash of waves, or the crackling warmth of a campfire. Cityscapes come alive with the soft hum of cafes, the rhythmic clatter of trains, or the calming white noise of traffic. And for those seeking deeper focus or relaxation, Moodist offers binaural beats and color noise designed to enhance your state of mind.'
},
{
title: 'Create Your Soundscape',
body: 'The beauty of Moodist lies in its simplicity and customization. No complex menus or confusing options just choose your desired sounds, adjust the volume balance, and hit play. Want to blend the gentle chirping of birds with the soothing sound of rain? No problem! Layer as many sounds as you like to create your personalized soundscape oasis.'
},
{
title: 'Sounds for Every Moment',
body: "Whether you're looking to unwind after a long day, enhance your focus during work, or lull yourself into a peaceful sleep, Moodist has the perfect soundscape waiting for you. The best part? It's completely free and open-source, so you can enjoy its benefits without any strings attached. Start using Moodist today and discover your new haven of tranquility and focus!"
}
],
button: 'Use Moodist'
},
// Donate section
donate: {
text: 'Enjoy Moodist?',
link: 'Support with a donation!'
},
// Source section
source: {
title: 'Open Source',
description: 'Moodist is free and open-source!',
button: 'Source Code'
},
// Footer
footer: {
createdBy: 'Created by',
author: 'Maze ✦'
},
// Main app controls
controls: {
play: 'Play',
pause: 'Pause',
unselectAll: 'Unselect All Sounds',
restoreHistory: 'Restore Unselected Sounds',
tooltipUnselect: 'Unselect all sounds.',
tooltipRestore: 'Restore unselected sounds.',
ariaUnselect: 'Unselect All Sounds',
ariaRestore: 'Restore Unselected Sounds'
},
// Categories
categories: {
favorites: 'Favorites',
nature: 'Nature',
animals: 'Animals',
rain: 'Rain',
places: 'Places',
things: 'Things',
transport: 'Transport',
urban: 'Urban',
noise: 'Noise',
binaural: 'Binaural'
},
// Toolbar menu items
menu: {
shortcuts: 'Shortcuts',
pomodoro: 'Pomodoro',
notepad: 'Notepad',
todo: 'Todo Checklist',
sleepTimer: 'Sleep Timer',
presets: 'Your Presets',
share: 'Share Sounds',
shuffle: 'Shuffle Sounds',
breathingExercise: 'Breathing Exercise',
countdown: 'Countdown Timer',
binaural: 'Binaural Beats',
isochronic: 'Isochronic Tones',
lofi: 'Lofi Music Player',
buyMeCoffee: 'Buy Me a Coffee',
sourceCode: 'Source Code'
},
// Messages and notifications
messages: {
selectSoundFirst: 'Please first select a sound to play.',
showMore: 'Show More'
}
},
zh: {
// Hero section
hero: {
title: '环境音效',
subtitle: '专注与平静',
description: '免费且开源。',
sounds: '种音效'
},
// About section
about: {
paragraphs: [
{
title: '免费环境音效',
body: '渴望从日常忙碌中逃离寻找平静需要完美的音景来提升专注力或帮助您安然入睡Moodist就是您的答案这是一个免费且开源的环境音效生成器告别订阅和注册——使用Moodist您可以免费解锁一个舒缓且沉浸式的音频体验世界。'
},
{
title: '精心策划的音效',
body: '深入探索精心策划的音效库。自然爱好者会在溪流的轻柔低语、海浪的节奏拍打或篝火的温暖噼啪声中找到慰藉。城市景观因咖啡馆的柔和嗡嗡声、火车的节奏咔嗒声或交通的平静白噪音而生动起来。对于那些寻求更深层专注或放松的人来说Moodist提供双耳节拍和彩色噪音旨在增强您的心理状态。'
},
{
title: '创建您的音景',
body: 'Moodist的美在于其简单性和可定制性。没有复杂的菜单或令人困惑的选项——只需选择您想要的音效调整音量平衡然后点击播放。想要将鸟儿的轻柔啁啾声与雨声的舒缓声混合没问题叠加任意数量的音效来创建您个性化的音景绿洲。'
},
{
title: '每个时刻的音效',
body: '无论您是想在漫长的一天后放松在工作时提升专注力还是让自己安然入睡Moodist都有完美的音景等着您。最好的部分它完全免费且开源所以您可以享受其好处而无需任何附加条件。今天就开始使用Moodist发现您新的宁静和专注天堂'
}
],
button: '使用Moodist'
},
// Donate section
donate: {
text: '喜欢Moodist',
link: '支持捐赠!'
},
// Source section
source: {
title: '开源项目',
description: 'Moodist免费且开源',
button: '源代码'
},
// Footer
footer: {
createdBy: '由',
author: 'Maze ✦ 创建'
},
// Main app controls
controls: {
play: '播放',
pause: '暂停',
unselectAll: '取消选择所有音效',
restoreHistory: '恢复未选择的音效',
tooltipUnselect: '取消选择所有音效。',
tooltipRestore: '恢复未选择的音效。',
ariaUnselect: '取消选择所有音效',
ariaRestore: '恢复未选择的音效'
},
// Categories
categories: {
favorites: '收藏',
nature: '自然',
animals: '动物',
rain: '雨声',
places: '场所',
things: '物品',
transport: '交通',
urban: '城市',
noise: '噪音',
binaural: '双耳节拍'
},
// Toolbar menu items
menu: {
shortcuts: '快捷键',
pomodoro: '番茄钟',
notepad: '记事本',
todo: '待办清单',
sleepTimer: '睡眠定时器',
presets: '您的预设',
share: '分享音效',
shuffle: '随机播放',
breathingExercise: '呼吸练习',
countdown: '倒计时',
binaural: '双耳节拍',
isochronic: '等时音调',
lofi: 'Lofi音乐播放器',
buyMeCoffee: '请我喝咖啡',
sourceCode: '源代码'
},
// Messages and notifications
messages: {
selectSoundFirst: '请先选择一个音效来播放。',
showMore: '显示更多'
}
}
};
export type Language = keyof typeof languages;
export type LanguageKey = keyof typeof languages.en;

58
src/hooks/use-language.ts Normal file
View file

@ -0,0 +1,58 @@
import { useState, useEffect } from 'react';
export type Language = 'en' | 'zh';
// 安全地获取localStorage
function getLocalStorageItem(key: string, defaultValue: Language = 'en'): Language {
if (typeof window !== 'undefined' && window.localStorage) {
try {
return (localStorage.getItem(key) as Language) || defaultValue;
} catch {
return defaultValue;
}
}
return defaultValue;
}
// 安全地设置localStorage
function setLocalStorageItem(key: string, value: string): void {
if (typeof window !== 'undefined' && window.localStorage) {
try {
localStorage.setItem(key, value);
} catch {
// 忽略错误
}
}
}
export function useLanguage() {
const [currentLanguage, setCurrentLanguage] = useState<Language>('en');
useEffect(() => {
const savedLanguage = getLocalStorageItem('moodist-language');
setCurrentLanguage(savedLanguage);
}, []);
const changeLanguage = (language: Language) => {
setCurrentLanguage(language);
setLocalStorageItem('moodist-language', language);
// Dispatch custom event to notify other components
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('languageChanged', {
detail: { language }
}));
}
};
const toggleLanguage = () => {
const newLanguage: Language = currentLanguage === 'en' ? 'zh' : 'en';
changeLanguage(newLanguage);
};
return {
currentLanguage,
changeLanguage,
toggleLanguage
};
}

View file

@ -0,0 +1,42 @@
.language-switcher {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
padding: 8px;
font-size: 14px;
font-weight: 500;
color: var(--color-foreground);
cursor: pointer;
background: linear-gradient(
var(--color-neutral-50),
var(--color-neutral-100)
);
border: 1px solid var(--color-neutral-200);
border-radius: 50%;
outline: none;
transition: all 0.2s ease;
backdrop-filter: blur(10px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.language-switcher:hover {
background: linear-gradient(
var(--color-neutral-100),
var(--color-neutral-200)
);
border-color: var(--color-neutral-300);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.language-switcher:focus-visible {
outline: 2px solid var(--color-neutral-400);
outline-offset: 2px;
}
.language-switcher:active {
transform: translateY(0);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

61
src/utils/language.ts Normal file
View file

@ -0,0 +1,61 @@
// 安全地获取localStorage
export function getLocalStorageItem(key: string, defaultValue: string = 'en'): string {
if (typeof window !== 'undefined' && window.localStorage) {
try {
return localStorage.getItem(key) || defaultValue;
} catch {
return defaultValue;
}
}
return defaultValue;
}
// 获取当前语言
export function getCurrentLanguage(): string {
return getLocalStorageItem('moodist-language', 'en');
}
// 语言文本映射
export const languageTexts = {
shortcuts: { en: 'Shortcuts', zh: '快捷键' },
pomodoro: { en: 'Pomodoro', zh: '番茄钟' },
notepad: { en: 'Notepad', zh: '记事本' },
todo: { en: 'Todo Checklist', zh: '待办清单' },
sleepTimer: { en: 'Sleep Timer', zh: '睡眠定时器' },
presets: { en: 'Your Presets', zh: '您的预设' },
share: { en: 'Share Sounds', zh: '分享音效' },
shuffle: { en: 'Shuffle Sounds', zh: '随机播放' },
breathingExercise: { en: 'Breathing Exercise', zh: '呼吸练习' },
countdown: { en: 'Countdown Timer', zh: '倒计时' },
binaural: { en: 'Binaural Beats', zh: '双耳节拍' },
isochronic: { en: 'Isochronic Tones', zh: '等时音调' },
lofi: { en: 'Lofi Music Player', zh: 'Lofi音乐播放器' },
buyMeCoffee: { en: 'Buy Me a Coffee', zh: '请我喝咖啡' },
sourceCode: { en: 'Source Code', zh: '源代码' },
globalVolume: { en: 'Global Volume', zh: '全局音量' }
};
// 音效分类标题映射
export const categoryTitles = {
nature: { en: 'Nature', zh: '自然' },
animals: { en: 'Animals', zh: '动物' },
rain: { en: 'Rain', zh: '雨声' },
places: { en: 'Places', zh: '场所' },
things: { en: 'Things', zh: '物品' },
transport: { en: 'Transport', zh: '交通' },
urban: { en: 'Urban', zh: '城市' },
noise: { en: 'Noise', zh: '噪音' },
binaural: { en: 'Binaural', zh: '双耳节拍' }
};
// 获取指定键的本地化文本
export function getLocalizedText(key: keyof typeof languageTexts): string {
const currentLang = getCurrentLanguage();
return languageTexts[key][currentLang as 'en' | 'zh'] || languageTexts[key].en;
}
// 获取音效分类的本地化标题
export function getLocalizedCategoryTitle(categoryId: keyof typeof categoryTitles): string {
const currentLang = getCurrentLanguage();
return categoryTitles[categoryId]?.[currentLang as 'en' | 'zh'] || categoryTitles[categoryId]?.en || categoryId;
}

117
src/utils/sound-labels.ts Normal file
View file

@ -0,0 +1,117 @@
// 音效名称的多语言映射
export const soundLabels = {
// Nature 分类
river: { en: 'River', zh: '河流' },
waves: { en: 'Waves', zh: '海浪' },
campfire: { en: 'Campfire', zh: '篝火' },
wind: { en: 'Wind', zh: '风' },
'howling-wind': { en: 'Howling Wind', zh: '呼啸的风' },
'wind-in-trees': { en: 'Wind in Trees', zh: '树间风声' },
waterfall: { en: 'Waterfall', zh: '瀑布' },
'walk-in-snow': { en: 'Walk in Snow', zh: '雪中行走' },
'walk-on-leaves': { en: 'Walk on Leaves', zh: '落叶上行走' },
'walk-on-gravel': { en: 'Walk on Gravel', zh: '碎石上行走' },
droplets: { en: 'Droplets', zh: '水滴' },
jungle: { en: 'Jungle', zh: '丛林' },
// Rain 分类
'light-rain': { en: 'Light Rain', zh: '小雨' },
'heavy-rain': { en: 'Heavy Rain', zh: '大雨' },
'thunder': { en: 'Thunder', zh: '雷声' },
'rain-on-window': { en: 'Rain on Window', zh: '窗上雨声' },
'rain-on-car-roof': { en: 'Rain on Car Roof', zh: '车顶雨声' },
'rain-on-umbrella': { en: 'Rain on Umbrella', zh: '雨伞雨声' },
'rain-on-tent': { en: 'Rain on Tent', zh: '帐篷雨声' },
'rain-on-leaves': { en: 'Rain on Leaves', zh: '落叶雨声' },
// Animals 分类
birds: { en: 'Birds', zh: '鸟鸣' },
seagulls: { en: 'Seagulls', zh: '海鸥' },
crickets: { en: 'Crickets', zh: '蟋蟀' },
wolf: { en: 'Wolf', zh: '狼' },
owl: { en: 'Owl', zh: '猫头鹰' },
frog: { en: 'Frog', zh: '青蛙' },
'dog-barking': { en: 'Dog Barking', zh: '狗叫' },
'horse-gallop': { en: 'Horse Gallop', zh: '马奔腾' },
'cat-purring': { en: 'Cat Purring', zh: '猫呼噜' },
crows: { en: 'Crows', zh: '乌鸦' },
whale: { en: 'Whale', zh: '鲸鱼' },
beehive: { en: 'Beehive', zh: '蜂巢' },
woodpecker: { en: 'Woodpecker', zh: '啄木鸟' },
chickens: { en: 'Chickens', zh: '鸡' },
cows: { en: 'Cows', zh: '牛' },
sheep: { en: 'Sheep', zh: '羊' },
// Urban 分类
highway: { en: 'Highway', zh: '高速公路' },
road: { en: 'Road', zh: '道路' },
'ambulance-siren': { en: 'Ambulance Siren', zh: '救护车警笛' },
'busy-street': { en: 'Busy Street', zh: '繁忙街道' },
crowd: { en: 'Crowd', zh: '人群' },
traffic: { en: 'Traffic', zh: '交通' },
fireworks: { en: 'Fireworks', zh: '烟花' },
// Places 分类
cafe: { en: 'Cafe', zh: '咖啡馆' },
airport: { en: 'Airport', zh: '机场' },
church: { en: 'Church', zh: '教堂' },
temple: { en: 'Temple', zh: '寺庙' },
'construction-site': { en: 'Construction Site', zh: '建筑工地' },
underwater: { en: 'Underwater', zh: '水下' },
'crowded-bar': { en: 'Crowded Bar', zh: '拥挤的酒吧' },
'night-village': { en: 'Night Village', zh: '夜晚村庄' },
'subway-station': { en: 'Subway Station', zh: '地铁站' },
office: { en: 'Office', zh: '办公室' },
supermarket: { en: 'Supermarket', zh: '超市' },
carousel: { en: 'Carousel', zh: '旋转木马' },
laboratory: { en: 'Laboratory', zh: '实验室' },
'laundry-room': { en: 'Laundry Room', zh: '洗衣房' },
restaurant: { en: 'Restaurant', zh: '餐厅' },
library: { en: 'Library', zh: '图书馆' },
// Things 分类
keyboard: { en: 'Keyboard', zh: '键盘' },
typewriter: { en: 'Typewriter', zh: '打字机' },
paper: { en: 'Paper', zh: '纸张' },
clock: { en: 'Clock', zh: '时钟' },
'wind-chimes': { en: 'Wind Chimes', zh: '风铃' },
'singing-bowl': { en: 'Singing Bowl', zh: '颂钵' },
'ceiling-fan': { en: 'Ceiling Fan', zh: '吊扇' },
dryer: { en: 'Dryer', zh: '烘干机' },
'slide-projector': { en: 'Slide Projector', zh: '幻灯机' },
'boiling-water': { en: 'Boiling Water', zh: '沸水' },
bubbles: { en: 'Bubbles', zh: '气泡' },
'tuning-radio': { en: 'Tuning Radio', zh: '调频收音机' },
'morse-code': { en: 'Morse Code', zh: '摩尔斯电码' },
'washing-machine': { en: 'Washing Machine', zh: '洗衣机' },
'vinyl-effect': { en: 'Vinyl Effect', zh: '黑胶效果' },
'windshield-wipers': { en: 'Windshield Wipers', zh: '雨刷器' },
// Transport 分类
train: { en: 'Train', zh: '火车' },
'inside-a-train': { en: 'Inside a Train', zh: '火车内部' },
airplane: { en: 'Airplane', zh: '飞机' },
submarine: { en: 'Submarine', zh: '潜艇' },
sailboat: { en: 'Sailboat', zh: '帆船' },
'rowing-boat': { en: 'Rowing Boat', zh: '划艇' },
// Noise 分类
'white-noise': { en: 'White Noise', zh: '白噪音' },
'pink-noise': { en: 'Pink Noise', zh: '粉红噪音' },
'brown-noise': { en: 'Brown Noise', zh: '棕噪音' }
};
// 获取音效的本地化标签
export function getLocalizedSoundLabel(soundId: string): string {
const currentLang = typeof window !== 'undefined' && window.localStorage
? localStorage.getItem('moodist-language') || 'en'
: 'en';
const soundLabel = soundLabels[soundId as keyof typeof soundLabels];
if (soundLabel) {
return soundLabel[currentLang as 'en' | 'zh'] || soundLabel.en;
}
// 如果没有找到映射返回原始ID
return soundId;
}