From 8cb93380d0ff7530ffdfd3f45a38b96fc8a1c703 Mon Sep 17 00:00:00 2001 From: LEIJM <2996636704@qq.com> Date: Sun, 17 Aug 2025 10:25:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=A4=9A=E8=AF=AD?= =?UTF-8?q?=E8=A8=80=E6=94=AF=E6=8C=81=EF=BC=8C=E5=8C=85=E5=90=AB=E4=B8=AD?= =?UTF-8?q?=E8=8B=B1=E6=96=87=E7=95=8C=E9=9D=A2=E5=88=87=E6=8D=A2=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .storybook/main.ts | 8 +- src/components/about.astro | 123 +++++++--- src/components/app/app.tsx | 38 +++- src/components/buttons/play/play.tsx | 46 +++- src/components/buttons/unselect/unselect.tsx | 52 ++++- .../categories/category/category.tsx | 38 +++- src/components/categories/donate/donate.tsx | 42 +++- src/components/donate.astro | 50 ++++- src/components/features/features.tsx | 73 ++++-- src/components/footer.astro | 49 +++- src/components/hero.astro | 105 ++++++++- src/components/language-switcher.tsx | 17 ++ src/components/modals/binaural/binaural.tsx | 132 ++++++++--- src/components/modals/breathing/breathing.tsx | 36 ++- .../modals/breathing/exercise/exercise.tsx | 83 ++++++- .../modals/isochronic/isochronic.tsx | 99 ++++++++- src/components/modals/lofi/lofi.tsx | 53 ++++- src/components/modals/presets/list/list.tsx | 42 +++- src/components/modals/presets/new/new.tsx | 46 +++- src/components/modals/presets/presets.tsx | 36 ++- .../modals/share-link/share-link.tsx | 41 +++- src/components/modals/shortcuts/shortcuts.tsx | 58 ++++- .../modals/sleep-timer/sleep-timer.tsx | 50 ++++- src/components/sounds/sound/sound.tsx | 44 +++- src/components/sounds/sounds.tsx | 44 +++- src/components/source.astro | 54 ++++- .../toolbar/menu/items/binaural.tsx | 5 +- .../toolbar/menu/items/breathing-exercise.tsx | 5 +- .../toolbar/menu/items/countdown.tsx | 5 +- src/components/toolbar/menu/items/donate.tsx | 5 +- .../toolbar/menu/items/isochronic.tsx | 5 +- src/components/toolbar/menu/items/lofi.tsx | 5 +- src/components/toolbar/menu/items/notepad.tsx | 4 +- .../toolbar/menu/items/pomodoro.tsx | 4 +- src/components/toolbar/menu/items/presets.tsx | 5 +- src/components/toolbar/menu/items/share.tsx | 4 +- .../toolbar/menu/items/shortcuts.tsx | 5 +- src/components/toolbar/menu/items/shuffle.tsx | 4 +- .../toolbar/menu/items/sleep-timer.tsx | 4 +- src/components/toolbar/menu/items/source.tsx | 5 +- src/components/toolbar/menu/items/todo.tsx | 5 +- src/components/toolbar/menu/menu.tsx | 3 +- .../toolbox/countdown/countdown.tsx | 47 +++- src/components/toolbox/notepad/notepad.tsx | 60 ++++- src/components/toolbox/pomodoro/pomodoro.tsx | 56 ++++- .../toolbox/pomodoro/setting/setting.tsx | 50 ++++- src/components/toolbox/todo/form/form.tsx | 40 +++- src/components/toolbox/todo/todo.tsx | 41 +++- src/components/toolbox/todo/todos/todos.tsx | 41 +++- src/constants/languages.ts | 210 ++++++++++++++++++ src/hooks/use-language.ts | 58 +++++ src/styles/language-switcher.css | 42 ++++ src/utils/language.ts | 61 +++++ src/utils/sound-labels.ts | 117 ++++++++++ 54 files changed, 2109 insertions(+), 246 deletions(-) create mode 100644 src/components/language-switcher.tsx create mode 100644 src/constants/languages.ts create mode 100644 src/hooks/use-language.ts create mode 100644 src/styles/language-switcher.css create mode 100644 src/utils/language.ts create mode 100644 src/utils/sound-labels.ts diff --git a/.storybook/main.ts b/.storybook/main.ts index 87e6cd5..f02901a 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -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; diff --git a/src/components/about.astro b/src/components/about.astro index 72fd439..92c3eb7 100644 --- a/src/components/about.astro +++ b/src/components/about.astro @@ -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,发现您新的宁静和专注天堂!' + } ]; --- -
-
+
+ +
- - { - paragraphs.map((paragraph, index) => ( -
-
- 0{index + 1} / 0{paragraphs.length} -
- -

{paragraph.title}

-

{paragraph.body}

+ {enParagraphs.map((paragraph, index) => ( +
+
+ {paragraph.counter} + {zhParagraphs[index].counter}
- )) - } +

+ {paragraph.title} + {zhParagraphs[index].title} +

+

+ {paragraph.body} + {zhParagraphs[index].body} +

+
+ ))} - +
+ Use Moodist + 使用Moodist +
-
+ - diff --git a/src/components/app/app.tsx b/src/components/app/app.tsx index c5f890d..e7a7d6d 100644 --- a/src/components/app/app.tsx +++ b/src/components/app/app.tsx @@ -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: , id: 'favorites', sounds: favoriteSounds as Array, - title: 'Favorites', + title: favoritesTitle, }); } return [...favorites, ...categories]; - }, [favoriteSounds, categories]); + }, [favoriteSounds, categories, currentLang]); return ( diff --git a/src/components/buttons/play/play.tsx b/src/components/buttons/play/play.tsx index cd4afa9..b760453 100644 --- a/src/components/buttons/play/play.tsx +++ b/src/components/buttons/play/play.tsx @@ -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 ( diff --git a/src/components/buttons/unselect/unselect.tsx b/src/components/buttons/unselect/unselect.tsx index 978d02b..4cdc1fb 100644 --- a/src/components/buttons/unselect/unselect.tsx +++ b/src/components/buttons/unselect/unselect.tsx @@ -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 ( <> @@ -43,19 +81,11 @@ export function UnselectButton() { >