feat: internationalize sound data and buttons

This commit is contained in:
yozuru 2025-04-19 23:52:34 +08:00
parent c8273371e0
commit 10f59a55a6
No known key found for this signature in database
22 changed files with 512 additions and 190 deletions

View file

@ -32,7 +32,9 @@ const paragraphKeys = [
</div>
))
}
<button class="button" id="use-moodist">{t('buttons.useMoodist')}</button>
<button class="button" id="use-moodist"
>{t('buttons.use-moodist.label')}</button
>
</Container>
</section>
@ -60,8 +62,7 @@ const paragraphKeys = [
& .paragraph {
padding: 30px 0;
background: linear-gradient(
transparent,
var(--color-neutral-50) 10%,
transparent var(--color-neutral-50) 10%,
var(--color-neutral-50) 90%,
transparent
);

View file

@ -2,7 +2,7 @@ import { useMemo, useEffect } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { BiSolidHeart } from 'react-icons/bi/index';
import { Howler } from 'howler';
import { I18nextProvider, useTranslation } from 'react-i18next';
import { I18nextProvider } from 'react-i18next';
import i18n from '@/i18n';
import { useSoundStore } from '@/stores/sound';
@ -23,17 +23,14 @@ import type { Sound, Category as CategoryType } from '@/data/types';
import { subscribe } from '@/lib/event';
interface AppProps {
locale: string; // 接收来自 Astro 的 locale
locale: string;
}
export function App({ locale }: AppProps) {
if (locale && i18n.language !== locale) {
i18n.changeLanguage(locale);
}
const { t } = useTranslation();
const categoriesData = useMemo(() => sounds.categories, []);
const categories = categoriesData; // 暂时不翻译
const favorites = useSoundStore(useShallow(state => state.getFavorites()));
const pause = useSoundStore(state => state.pause);
@ -48,7 +45,6 @@ export function App({ locale }: AppProps) {
favorites.includes(sound.id),
);
// 暂时不翻译 sound labels
return favorites
.map(favoriteId =>
favoriteSoundsData.find(sound => sound.id === favoriteId),
@ -92,11 +88,11 @@ export function App({ locale }: AppProps) {
icon: <BiSolidHeart />,
id: 'favorites',
sounds: favoriteSounds,
title: t('categories.favorites'),
titleKey: 'sounds.favorites.title',
});
}
return [...favs, ...categories];
}, [favoriteSounds, categories, t]);
return [...favs, ...categoriesData];
}, [favoriteSounds, categoriesData]);
return (
<I18nextProvider i18n={i18n}>

View file

@ -44,14 +44,14 @@ export function PlayButton() {
<span aria-hidden="true">
<BiPause />
</span>{' '}
{t('common.pause')}
{t('buttons.pause.label')}
</>
) : (
<>
<span aria-hidden="true">
<BiPlay />
</span>{' '}
{t('common.play')}
{t('buttons.play.label')}
</>
)}
</button>

View file

@ -11,13 +11,15 @@ import { fade, mix, slideX } from '@/lib/motion';
import styles from './unselect.module.css';
import { useTranslation } from 'react-i18next';
export function UnselectButton() {
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);
const { t } = useTranslation();
const variants = {
...mix(fade(), slideX(15)),
exit: { opacity: 0 },
@ -45,16 +47,16 @@ export function UnselectButton() {
showDelay={0}
content={
hasHistory
? 'Restore unselected sounds.'
: 'Unselect all sounds.'
? t('buttons.unselect.restore.tooltip')
: t('buttons.unselect.tooltip')
}
>
<button
disabled={noSelected && !hasHistory}
aria-label={
hasHistory
? 'Restore Unselected Sounds'
: 'Unselect All Sounds'
? t('buttons.unselect.restore.aria-label')
: t('buttons.unselect.aria-label')
}
className={cn(
styles.unselectButton,

View file

@ -1,11 +1,12 @@
import { useTranslation } from 'react-i18next';
import { Sounds } from '@/components/sounds';
import styles from './category.module.css';
import type { Category as CategoryType } from '@/data/types';
import type { Category } from '@/data/types';
interface CategoryProps extends Category {
interface CategoryProps extends Omit<CategoryType, 'title'> {
functional?: boolean;
titleKey: string;
}
export function Category({
@ -13,8 +14,10 @@ export function Category({
icon,
id,
sounds,
title,
titleKey,
}: CategoryProps) {
const { t } = useTranslation();
return (
<div className={styles.category} id={`category-${id}`}>
<div className={styles.iconContainer}>
@ -24,7 +27,7 @@ export function Category({
</div>
</div>
<div className={styles.title}>{title}</div>
<div className={styles.title}>{t(titleKey)}</div>
<Sounds functional={functional} id={id} sounds={sounds} />
</div>

View file

@ -1,5 +1,6 @@
import { BiHeart, BiSolidHeart } from 'react-icons/bi/index';
import { AnimatePresence, motion } from 'framer-motion';
import { useTranslation } from 'react-i18next';
import { useSoundStore } from '@/stores/sound';
import { cn } from '@/helpers/styles';
@ -16,7 +17,8 @@ interface FavoriteProps {
}
export function Favorite({ id, label }: FavoriteProps) {
const isFavorite = useSoundStore(state => state.sounds[id].isFavorite);
const { t } = useTranslation();
const isFavorite = useSoundStore(state => state.sounds[id]?.isFavorite);
const toggleFavorite = useSoundStore(state => state.toggleFavorite);
const handleToggle = async () => {
@ -36,18 +38,21 @@ export function Favorite({ id, label }: FavoriteProps) {
};
const variants = fade();
const handleKeyDown = useKeyboardButton(handleToggle);
const ariaLabel = isFavorite
? t('buttons.favorite.remove.aria-label', { label: label })
: t('buttons.favorite.add.aria-label', { label: label });
if (useSoundStore.getState().sounds[id] === undefined) {
return null;
}
return (
<AnimatePresence initial={false} mode="wait">
<button
aria-label={ariaLabel}
className={cn(styles.favoriteButton, isFavorite && styles.isFavorite)}
aria-label={
isFavorite
? `Remove ${label} Sound from Favorites`
: `Add ${label} Sound to Favorites`
}
onKeyDown={handleKeyDown}
onClick={e => {
e.stopPropagation();

View file

@ -1,3 +1,4 @@
import { useTranslation } from 'react-i18next';
import { useSoundStore } from '@/stores/sound';
import styles from './range.module.css';
@ -8,14 +9,20 @@ interface RangeProps {
}
export function Range({ id, label }: RangeProps) {
const { t } = useTranslation();
const setVolume = useSoundStore(state => state.setVolume);
const volume = useSoundStore(state => state.sounds[id].volume);
const isSelected = useSoundStore(state => state.sounds[id].isSelected);
const soundState = useSoundStore(state => state.sounds[id]);
const locked = useSoundStore(state => state.locked);
if (!soundState) {
return null;
}
const { isSelected, volume } = soundState;
return (
<input
aria-label={`${label} sound volume`}
aria-label={t('volume.aria-label', { label: label })}
autoComplete="off"
className={styles.range}
disabled={!isSelected}

View file

@ -1,5 +1,6 @@
import { useCallback, useEffect, forwardRef, useMemo } from 'react';
import { ImSpinner9 } from 'react-icons/im/index';
import { useTranslation } from 'react-i18next'; // 导入 useTranslation
import { Range } from './range';
import { Favorite } from './favorite';
@ -15,17 +16,21 @@ import type { Sound as SoundType } from '@/data/types';
import { useKeyboardButton } from '@/hooks/use-keyboard-button';
interface SoundProps extends SoundType {
interface SoundProps extends Omit<SoundType, 'label'> {
functional: boolean;
hidden: boolean;
labelKey: string;
selectHidden: (key: string) => void;
unselectHidden: (key: string) => void;
}
export const Sound = forwardRef<HTMLDivElement, SoundProps>(function Sound(
{ functional, hidden, icon, id, label, selectHidden, src, unselectHidden },
{ functional, hidden, icon, id, labelKey, selectHidden, src, unselectHidden },
ref,
) {
const { t } = useTranslation(); // 获取 t 函数
const translatedLabel = useMemo(() => t(labelKey), [t, labelKey]);
const isPlaying = useSoundStore(state => state.isPlaying);
const play = useSoundStore(state => state.play);
const selectSound = useSoundStore(state => state.select);
@ -43,22 +48,22 @@ export const Sound = forwardRef<HTMLDivElement, SoundProps>(function Sound(
const isLoading = useLoadingStore(state => state.loaders[src]);
const sound = useSound(src, { loop: true, volume: adjustedVolume });
const soundControls = useSound(src, { loop: true, volume: adjustedVolume });
useEffect(() => {
if (locked) return;
if (isSelected && isPlaying && functional) {
sound?.play();
soundControls?.play();
} else {
sound?.pause();
soundControls?.pause();
}
}, [isSelected, sound, isPlaying, functional, locked]);
}, [isSelected, soundControls, isPlaying, functional, locked]);
useEffect(() => {
if (hidden && isSelected) selectHidden(label);
else if (hidden && !isSelected) unselectHidden(label);
}, [label, isSelected, hidden, selectHidden, unselectHidden]);
if (hidden && isSelected) selectHidden(labelKey);
else if (hidden && !isSelected) unselectHidden(labelKey);
}, [labelKey, isSelected, hidden, selectHidden, unselectHidden]);
const select = useCallback(() => {
if (locked) return;
@ -82,13 +87,11 @@ export const Sound = forwardRef<HTMLDivElement, SoundProps>(function Sound(
toggle();
}, [toggle]);
const handleKeyDown = useKeyboardButton(() => {
toggle();
});
const handleKeyDown = useKeyboardButton(toggle);
return (
<div
aria-label={`${label} sound`}
aria-label={t('sounds.aria-label', { name: translatedLabel })}
ref={ref}
role="button"
tabIndex={0}
@ -100,7 +103,7 @@ export const Sound = forwardRef<HTMLDivElement, SoundProps>(function Sound(
onClick={handleClick}
onKeyDown={handleKeyDown}
>
<Favorite id={id} label={label} />
<Favorite id={id} label={translatedLabel} />
<div className={styles.icon}>
{isLoading ? (
<span aria-hidden="true" className={styles.spinner}>
@ -111,9 +114,9 @@ export const Sound = forwardRef<HTMLDivElement, SoundProps>(function Sound(
)}
</div>
<div className={styles.label} id={id}>
{label}
{translatedLabel}
</div>
<Range id={id} label={label} />
<Range id={id} label={translatedLabel} />
</div>
);
});

View file

@ -1,22 +1,22 @@
import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import { useTranslation } from 'react-i18next';
import { Sound } from './sound';
import { useLocalStorage } from '@/hooks/use-local-storage';
import { cn } from '@/helpers/styles';
import { fade, scale, mix } from '@/lib/motion';
import styles from './sounds.module.css';
import type { Sounds } from '@/data/types';
import type { Sounds as SoundsType } from '@/data/types';
interface SoundsProps {
functional: boolean;
id: string;
sounds: Sounds;
sounds: SoundsType;
}
export function Sounds({ functional, id, sounds }: SoundsProps) {
const { t } = useTranslation();
const [showAll, setShowAll] = useLocalStorage(`${id}-show-more`, false);
const [clickedMore, setClickedMore] = useState(false);
@ -71,7 +71,7 @@ export function Sounds({ functional, id, sounds }: SoundsProps) {
<div className={styles.sounds}>
{sounds.map((sound, index) => (
<Sound
key={sound.label}
key={sound.id}
{...sound}
functional={functional}
hidden={!showAll && index > 5}
@ -84,7 +84,7 @@ export function Sounds({ functional, id, sounds }: SoundsProps) {
{sounds.length < 2 &&
new Array(2 - sounds.length)
.fill(null)
.map((_, index) => <div key={index} />)}
.map((_, index) => <div key={`placeholder-${index}`} />)}
</div>
{sounds.length > 6 && (
@ -106,7 +106,7 @@ export function Sounds({ functional, id, sounds }: SoundsProps) {
onAnimationComplete={() => setIsAnimating(false)}
onAnimationStart={() => setIsAnimating(true)}
>
{showAll ? 'Show Less' : 'Show More'}
{showAll ? t('sounds.show-less') : t('sounds.show-more')}
</motion.span>
</AnimatePresence>
</button>

View file

@ -24,103 +24,104 @@ import type { Category } from '../types';
export const animals: Category = {
icon: <FaDog />,
id: 'animals',
// 修改
sounds: [
{
icon: <PiBirdFill />,
id: 'birds',
label: 'Birds',
labelKey: 'sounds.animals.birds',
src: '/sounds/animals/birds.mp3',
},
{
icon: <GiSeagull />,
id: 'seagulls',
label: 'Seagulls',
labelKey: 'sounds.animals.seagulls',
src: '/sounds/animals/seagulls.mp3',
},
{
icon: <GiCricket />,
id: 'crickets',
label: 'Crickets',
labelKey: 'sounds.animals.crickets',
src: '/sounds/animals/crickets.mp3',
},
{
icon: <GiWolfHead />,
id: 'wolf',
label: 'Wolf',
labelKey: 'sounds.animals.wolf',
src: '/sounds/animals/wolf.mp3',
},
{
icon: <GiOwl />,
id: 'owl',
label: 'Owl',
labelKey: 'sounds.animals.owl',
src: '/sounds/animals/owl.mp3',
},
{
icon: <FaFrog />,
id: 'frog',
label: 'Frog',
labelKey: 'sounds.animals.frog',
src: '/sounds/animals/frog.mp3',
},
{
icon: <PiDogBold />,
id: 'dog-barking',
label: 'Dog Barking',
labelKey: 'sounds.animals.dog-barking',
src: '/sounds/animals/dog-barking.mp3',
},
{
icon: <FaHorseHead />,
id: 'horse-galopp',
label: 'Horse Galopp',
labelKey: 'sounds.animals.horse-galopp',
src: '/sounds/animals/horse-galopp.mp3',
},
{
icon: <FaCat />,
id: 'cat-purring',
label: 'Cat Purring',
labelKey: 'sounds.animals.cat-purring',
src: '/sounds/animals/cat-purring.mp3',
},
{
icon: <FaCrow />,
id: 'crows',
label: 'Crows',
labelKey: 'sounds.animals.crows',
src: '/sounds/animals/crows.mp3',
},
{
icon: <GiWhaleTail />,
id: 'whale',
label: 'Whale',
labelKey: 'sounds.animals.whale',
src: '/sounds/animals/whale.mp3',
},
{
icon: <GiTreeBeehive />,
id: 'beehive',
label: 'Beehive',
labelKey: 'sounds.animals.beehive',
src: '/sounds/animals/beehive.mp3',
},
{
icon: <GiEgyptianBird />,
id: 'woodpecker',
label: 'Woodpecker',
labelKey: 'sounds.animals.woodpecker',
src: '/sounds/animals/woodpecker.mp3',
},
{
icon: <GiChicken />,
id: 'chickens',
label: 'Chickens',
labelKey: 'sounds.animals.chickens',
src: '/sounds/animals/chickens.mp3',
},
{
icon: <GiCow />,
id: 'cows',
label: 'Cows',
labelKey: 'sounds.animals.cows',
src: '/sounds/animals/cows.mp3',
},
{
icon: <GiSheep />,
id: 'sheep',
label: 'Sheep',
labelKey: 'sounds.animals.sheep',
src: '/sounds/animals/sheep.mp3',
},
],
title: 'Animals',
titleKey: 'sounds.animals.title',
};

View file

@ -6,37 +6,38 @@ import type { Category } from '../types';
export const binaural: Category = {
icon: <TbWaveSine />,
id: 'binaural',
// 修改
sounds: [
{
icon: <BsSoundwave />,
id: 'binaural-delta',
label: 'Delta',
labelKey: 'sounds.binaural.binaural-delta',
src: '/sounds/binaural/binaural-delta.wav',
},
{
icon: <BsSoundwave />,
id: 'binaural-theta',
label: 'Theta',
labelKey: 'sounds.binaural.binaural-theta',
src: '/sounds/binaural/binaural-theta.wav',
},
{
icon: <BsSoundwave />,
id: 'binaural-alpha',
label: 'Alpha',
labelKey: 'sounds.binaural.binaural-alpha',
src: '/sounds/binaural/binaural-alpha.wav',
},
{
icon: <BsSoundwave />,
id: 'binaural-beta',
label: 'Beta',
labelKey: 'sounds.binaural.binaural-beta',
src: '/sounds/binaural/binaural-beta.wav',
},
{
icon: <BsSoundwave />,
id: 'binaural-gamma',
label: 'Gamma',
labelKey: 'sounds.binaural.binaural-gamma',
src: '/sounds/binaural/binaural-gamma.wav',
},
],
title: 'Binaural Beats',
titleKey: 'sounds.binaural.title',
};

View file

@ -14,79 +14,80 @@ import type { Category } from '../types';
export const nature: Category = {
icon: <BiSolidTree />,
id: 'nature',
// 修改
sounds: [
{
icon: <BiWater />,
id: 'river',
label: 'River',
labelKey: 'sounds.nature.river',
src: '/sounds/nature/river.mp3',
},
{
icon: <FaWater />,
id: 'waves',
label: 'Waves',
labelKey: 'sounds.nature.waves',
src: '/sounds/nature/waves.mp3',
},
{
icon: <BsFire />,
id: 'campfire',
label: 'Campfire',
labelKey: 'sounds.nature.campfire',
src: '/sounds/nature/campfire.mp3',
},
{
icon: <FaWind />,
id: 'wind',
label: 'Wind',
labelKey: 'sounds.nature.wind',
src: '/sounds/nature/wind.mp3',
},
{
icon: <FaWind />,
id: 'howling-wind',
label: 'Howling Wind',
labelKey: 'sounds.nature.howling-wind',
src: '/sounds/nature/howling-wind.mp3',
},
{
icon: <BiSolidTree />,
id: 'wind-in-trees',
label: 'Wind in Trees',
labelKey: 'sounds.nature.wind-in-trees',
src: '/sounds/nature/wind-in-trees.mp3',
},
{
icon: <GiWaterfall />,
id: 'waterfall',
label: 'Waterfall',
labelKey: 'sounds.nature.waterfall',
src: '/sounds/nature/waterfall.mp3',
},
{
icon: <FaRegSnowflake />,
id: 'walk-in-snow',
label: 'Walk in Snow',
labelKey: 'sounds.nature.walk-in-snow',
src: '/sounds/nature/walk-in-snow.mp3',
},
{
icon: <FaLeaf />,
id: 'walk-on-leaves',
label: 'Walk on Leaves',
labelKey: 'sounds.nature.walk-on-leaves',
src: '/sounds/nature/walk-on-leaves.mp3',
},
{
icon: <GiStonePile />,
id: 'walk-on-gravel',
label: 'Walk on Gravel',
labelKey: 'sounds.nature.walk-on-gravel',
src: '/sounds/nature/walk-on-gravel.mp3',
},
{
icon: <BsFillDropletFill />,
id: 'droplets',
label: 'Droplets',
labelKey: 'sounds.nature.droplets',
src: '/sounds/nature/droplets.mp3',
},
{
icon: <FaTree />,
id: 'jungle',
label: 'Jungle',
labelKey: 'sounds.nature.jungle',
src: '/sounds/nature/jungle.mp3',
},
],
title: 'Nature',
titleKey: 'sounds.nature.title',
};

View file

@ -6,25 +6,26 @@ import type { Category } from '../types';
export const noise: Category = {
icon: <BsSoundwave />,
id: 'noise',
// 修改
sounds: [
{
icon: <GiSoundWaves />,
id: 'white-noise',
label: 'White Noise',
labelKey: 'sounds.noise.white-noise',
src: '/sounds/noise/white-noise.wav',
},
{
icon: <GiSoundWaves />,
id: 'pink-noise',
label: 'Pink Noise',
labelKey: 'sounds.noise.pink-noise',
src: '/sounds/noise/pink-noise.wav',
},
{
icon: <GiSoundWaves />,
id: 'brown-noise',
label: 'Brown Noise',
labelKey: 'sounds.noise.brown-noise',
src: '/sounds/noise/brown-noise.wav',
},
],
title: 'Noise',
titleKey: 'sounds.noise.title',
};

View file

@ -21,103 +21,104 @@ import type { Category } from '../types';
export const places: Category = {
icon: <MdLocationPin />,
id: 'places',
// 修改
sounds: [
{
icon: <BiSolidCoffeeAlt />,
id: 'cafe',
label: 'Cafe',
labelKey: 'sounds.places.cafe',
src: '/sounds/places/cafe.mp3',
},
{
icon: <BiSolidPlaneAlt />,
id: 'airport',
label: 'Airport',
labelKey: 'sounds.places.airport',
src: '/sounds/places/airport.mp3',
},
{
icon: <FaChurch />,
id: 'church',
label: 'Church',
labelKey: 'sounds.places.church',
src: '/sounds/places/church.mp3',
},
{
icon: <MdTempleBuddhist />,
id: 'temple',
label: 'Temple',
labelKey: 'sounds.places.temple',
src: '/sounds/places/temple.mp3',
},
{
icon: <MdConstruction />,
id: 'construction-site',
label: 'Construction Site',
labelKey: 'sounds.places.construction-site',
src: '/sounds/places/construction-site.mp3',
},
{
icon: <TbScubaMask />,
id: 'underwater',
label: 'Underwater',
labelKey: 'sounds.places.underwater',
src: '/sounds/places/underwater.mp3',
},
{
icon: <TbBeerFilled />,
id: 'crowded-bar',
label: 'Crowded Bar',
labelKey: 'sounds.places.crowded-bar',
src: '/sounds/places/crowded-bar.mp3',
},
{
icon: <GiVillage />,
id: 'night-village',
label: 'Night Village',
labelKey: 'sounds.places.night-village',
src: '/sounds/places/night-village.mp3',
},
{
icon: <FaSubway />,
id: 'subway-station',
label: 'Subway Station',
labelKey: 'sounds.places.subway-station',
src: '/sounds/places/subway-station.mp3',
},
{
icon: <HiOfficeBuilding />,
id: 'office',
label: 'Office',
labelKey: 'sounds.places.office',
src: '/sounds/places/office.mp3',
},
{
icon: <FaShoppingBasket />,
id: 'supermarket',
label: 'Supermarket',
labelKey: 'sounds.places.supermarket',
src: '/sounds/places/supermarket.mp3',
},
{
icon: <GiCarousel />,
id: 'carousel',
label: 'Carousel',
labelKey: 'sounds.places.carousel',
src: '/sounds/places/carousel.mp3',
},
{
icon: <AiFillExperiment />,
id: 'laboratory',
label: 'Laboratory',
labelKey: 'sounds.places.laboratory',
src: '/sounds/places/laboratory.mp3',
},
{
icon: <BiSolidDryer />,
id: 'laundry-room',
label: 'Laundry Room',
labelKey: 'sounds.places.laundry-room',
src: '/sounds/places/laundry-room.mp3',
},
{
icon: <IoRestaurant />,
id: 'restaurant',
label: 'Restaurant',
labelKey: 'sounds.places.restaurant',
src: '/sounds/places/restaurant.mp3',
},
{
icon: <FaBookOpen />,
id: 'library',
label: 'Library',
labelKey: 'sounds.places.library',
src: '/sounds/places/library.mp3',
},
],
title: 'Places',
titleKey: 'sounds.places.title',
};

View file

@ -13,55 +13,56 @@ import type { Category } from '../types';
export const rain: Category = {
icon: <BsFillCloudRainFill />,
id: 'rain',
// 修改
sounds: [
{
icon: <BsFillCloudRainFill />,
id: 'light-rain',
label: 'Light Rain',
labelKey: 'sounds.rain.light-rain',
src: '/sounds/rain/light-rain.mp3',
},
{
icon: <BsFillCloudRainHeavyFill />,
id: 'heavy-rain',
label: 'Heavy Rain',
labelKey: 'sounds.rain.heavy-rain',
src: '/sounds/rain/heavy-rain.mp3',
},
{
icon: <MdOutlineThunderstorm />,
id: 'thunder',
label: 'Thunder',
labelKey: 'sounds.rain.thunder',
src: '/sounds/rain/thunder.mp3',
},
{
icon: <GiWindow />,
id: 'rain-on-window',
label: 'Rain on Window',
labelKey: 'sounds.rain.rain-on-window',
src: '/sounds/rain/rain-on-window.mp3',
},
{
icon: <FaCarSide />,
id: 'rain-on-car-roof',
label: 'Rain on Car Roof',
labelKey: 'sounds.rain.rain-on-car-roof',
src: '/sounds/rain/rain-on-car-roof.mp3',
},
{
icon: <BsUmbrellaFill />,
id: 'rain-on-umbrella',
label: 'Rain on Umbrella',
labelKey: 'sounds.rain.rain-on-umbrella',
src: '/sounds/rain/rain-on-umbrella.mp3',
},
{
icon: <PiTentFill />,
id: 'rain-on-tent',
label: 'Rain on Tent',
labelKey: 'sounds.rain.rain-on-tent',
src: '/sounds/rain/rain-on-tent.mp3',
},
{
icon: <FaLeaf />,
id: 'rain-on-leaves',
label: 'Rain on Leaves',
labelKey: 'sounds.rain.rain-on-leaves',
src: '/sounds/rain/rain-on-leaves.mp3',
},
],
title: 'Rain',
titleKey: 'sounds.rain.title',
};

View file

@ -17,103 +17,104 @@ import type { Category } from '../types';
export const things: Category = {
icon: <MdSmartToy />,
id: 'things',
// 修改
sounds: [
{
icon: <BsFillKeyboardFill />,
id: 'keyboard',
label: 'Keyboard',
labelKey: 'sounds.things.keyboard',
src: '/sounds/things/keyboard.mp3',
},
{
icon: <FaKeyboard />,
id: 'typewriter',
label: 'Typewriter',
labelKey: 'sounds.things.typewriter',
src: '/sounds/things/typewriter.mp3',
},
{
icon: <RiFilePaper2Fill />,
id: 'paper',
label: 'Paper',
labelKey: 'sounds.things.paper',
src: '/sounds/things/paper.mp3',
},
{
icon: <FaClock />,
id: 'clock',
label: 'Clock',
labelKey: 'sounds.things.clock',
src: '/sounds/things/clock.mp3',
},
{
icon: <GiWindchimes />,
id: 'wind-chimes',
label: 'Wind Chimes',
labelKey: 'sounds.things.wind-chimes',
src: '/sounds/things/wind-chimes.mp3',
},
{
icon: <TbBowlFilled />,
id: 'singing-bowl',
label: 'Singing Bowl',
labelKey: 'sounds.things.singing-bowl',
src: '/sounds/things/singing-bowl.mp3',
},
{
icon: <FaFan />,
id: 'ceiling-fan',
label: 'Ceiling Fan',
labelKey: 'sounds.things.ceiling-fan',
src: '/sounds/things/ceiling-fan.mp3',
},
{
icon: <BiSolidDryer />,
id: 'dryer',
label: 'Dryer',
labelKey: 'sounds.things.dryer',
src: '/sounds/things/dryer.mp3',
},
{
icon: <GiFilmProjector />,
id: 'slide-projector',
label: 'Slide Projector',
labelKey: 'sounds.things.slide-projector',
src: '/sounds/things/slide-projector.mp3',
},
{
icon: <MdWaterDrop />,
id: 'boiling-water',
label: 'Boiling Water',
labelKey: 'sounds.things.boiling-water',
src: '/sounds/things/boiling-water.mp3',
},
{
icon: <RiBubbleChartFill />,
id: 'bubbles',
label: 'Bubbles',
labelKey: 'sounds.things.bubbles',
src: '/sounds/things/bubbles.mp3',
},
{
icon: <MdRadio />,
id: 'tuning-radio',
label: 'Tuning Radio',
labelKey: 'sounds.things.tuning-radio',
src: '/sounds/things/tuning-radio.mp3',
},
{
icon: <IoIosRadio />,
id: 'morse-code',
label: 'Morse Code',
labelKey: 'sounds.things.morse-code',
src: '/sounds/things/morse-code.mp3',
},
{
icon: <GiWashingMachine />,
id: 'washing-machine',
label: 'Washing Machine',
labelKey: 'sounds.things.washing-machine',
src: '/sounds/things/washing-machine.mp3',
},
{
icon: <PiVinylRecord />,
id: 'vinyl-effect',
label: 'Vinyl Effect',
labelKey: 'sounds.things.vinyl-effect',
src: '/sounds/things/vinyl-effect.mp3',
},
{
icon: <TbWiper />,
id: 'windshield-wipers',
label: 'Windshield Wipers',
labelKey: 'sounds.things.windshield-wipers',
src: '/sounds/things/windshield-wipers.mp3',
},
],
title: 'Things',
titleKey: 'sounds.things.title',
};

View file

@ -8,43 +8,44 @@ import type { Category } from '../types';
export const transport: Category = {
icon: <FaCarSide />,
id: 'transport',
// 修改
sounds: [
{
icon: <BiSolidTrain />,
id: 'train',
label: 'Train',
labelKey: 'sounds.transport.train',
src: '/sounds/transport/train.mp3',
},
{
icon: <BiSolidTrain />,
id: 'inside-a-train',
label: 'Inside a Train',
labelKey: 'sounds.transport.inside-a-train',
src: '/sounds/transport/inside-a-train.mp3',
},
{
icon: <BiSolidPlaneAlt />,
id: 'airplane',
label: 'Airplane',
labelKey: 'sounds.transport.airplane',
src: '/sounds/transport/airplane.mp3',
},
{
icon: <GiSubmarine />,
id: 'submarine',
label: 'Submarine',
labelKey: 'sounds.transport.submarine',
src: '/sounds/transport/submarine.mp3',
},
{
icon: <GiSailboat />,
id: 'sailboat',
label: 'Sailboat',
labelKey: 'sounds.transport.sailboat',
src: '/sounds/transport/sailboat.mp3',
},
{
icon: <TbSailboat />,
id: 'rowing-boat',
label: 'Rowing Boat',
labelKey: 'sounds.transport.rowing-boat',
src: '/sounds/transport/rowing-boat.mp3',
},
],
title: 'Transport',
titleKey: 'sounds.transport.title',
};

View file

@ -9,49 +9,50 @@ import type { Category } from '../types';
export const urban: Category = {
icon: <FaCity />,
id: 'urban',
// 修改
sounds: [
{
icon: <PiRoadHorizonFill />,
id: 'highway',
label: 'Highway',
labelKey: 'sounds.urban.highway',
src: '/sounds/urban/highway.mp3',
},
{
icon: <FaRoad />,
id: 'road',
label: 'Road',
labelKey: 'sounds.urban.road',
src: '/sounds/urban/road.mp3',
},
{
icon: <PiSirenBold />,
id: 'ambulance-siren',
label: 'Ambulance Siren',
labelKey: 'sounds.urban.ambulance-siren',
src: '/sounds/urban/ambulance-siren.mp3',
},
{
icon: <BsSoundwave />,
id: 'busy-street',
label: 'Busy Street',
labelKey: 'sounds.urban.busy-street',
src: '/sounds/urban/busy-street.mp3',
},
{
icon: <BsPeopleFill />,
id: 'crowd',
label: 'Crowd',
labelKey: 'sounds.urban.crowd',
src: '/sounds/urban/crowd.mp3',
},
{
icon: <BiSolidTraffic />,
id: 'traffic',
label: 'Traffic',
labelKey: 'sounds.urban.traffic',
src: '/sounds/urban/traffic.mp3',
},
{
icon: <RiSparkling2Fill />,
id: 'fireworks',
label: 'Fireworks',
labelKey: 'sounds.urban.fireworks',
src: '/sounds/urban/fireworks.mp3',
},
],
title: 'Urban',
titleKey: 'sounds.urban.title',
};

4
src/data/types.d.ts vendored
View file

@ -1,7 +1,7 @@
export interface Sound {
icon: React.ReactNode;
id: string;
label: string;
labelKey: string;
src: string;
}
@ -11,7 +11,7 @@ export interface Category {
icon: React.ReactNode;
id: string;
sounds: Sounds;
title: string;
titleKey: string;
}
export type Categories = Array<Category>;

View file

@ -16,15 +16,32 @@ const resources = {
i18n.use(initReactI18next).init({
debug: true,
fallbackLng: 'en',
interpolation: {
escapeValue: false,
},
interpolation: { escapeValue: false },
lng: 'en',
react: {
useSuspense: false,
},
missingKeyHandler: (lngs, ns, key, fallbackValue, updateMissing, options) => {
const resolvedLng = lngs && lngs.length > 0 ? lngs[0] : i18n.language;
const value = i18n.getResource(resolvedLng, ns || 'translation', key);
if (typeof value === 'object' && value !== null) {
console.warn(
`i18next: Key '${key}' in namespace '${
ns || 'translation'
}' resolved to an object, but expected a string. Check your t() call.`,
options,
);
} else {
console.warn(
`i18next: Missing key '${key}' in namespace '${
ns || 'translation'
}' for language(s) '${lngs.join(', ')}'.`,
);
}
return fallbackValue || key;
},
react: { useSuspense: false },
resources,
returnObjects: true,
});
export default i18n;

View file

@ -8,14 +8,6 @@
"en": "English",
"zh": "简体中文"
},
"languageSwitcher": {
"label": "Language selection"
},
"common": {
"play": "Play",
"pause": "Pause",
"close": "Close"
},
"modals": {
"reload": {
"title": "New Content",
@ -23,13 +15,33 @@
"reloadButton": "Reload"
}
},
"buttons": {
"playError": "Please first select a sound to play.",
"useMoodist": "Use Moodist"
},
"categories": {
"favorites": "Favorites"
"play": {
"label": "Play",
"error": "Please select a sound to play."
},
"favorite": {
"add": {
"aria-label": "Add {{label}} Sound to Favorites"
},
"remove": {
"aria-label": "Remove {{label}} Sound from Favorites"
}
},
"unselect": {
"tooltip": "Unselect all sounds.",
"aria-label": "Unselect All Sounds",
"restore": {
"tooltip": "Restore unselected sounds.",
"aria-label": "Restore Unselected Sounds"
}
},
"pause": {
"label": "Pause"
},
"use-moodist": {
"label": "Use Moodist"
}
},
"about": {
"section1": {
@ -48,5 +60,132 @@
"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!"
}
},
"volume": {
"aria-label": "{{label}} sound volume"
},
"sounds": {
"show-less": "Show Less",
"show-more": "Show More",
"aria-label": "{{name}} sound",
"favorites": {
"title": "Favorites"
},
"animals": {
"title": "Animals",
"birds": "Birds",
"seagulls": "Seagulls",
"crickets": "Crickets",
"wolf": "Wolf",
"owl": "Owl",
"frog": "Frog",
"dog-barking": "Dog Barking",
"horse-galopp": "Horse Galopp",
"cat-purring": "Cat Purring",
"crows": "Crows",
"whale": "Whale",
"beehive": "Beehive",
"woodpecker": "Woodpecker",
"chickens": "Chickens",
"cows": "Cows",
"sheep": "Sheep"
},
"binaural": {
"title": "Binaural Beats",
"binaural-delta": "Delta",
"binaural-theta": "Theta",
"binaural-alpha": "Alpha",
"binaural-beta": "Beta",
"binaural-gamma": "Gamma"
},
"nature": {
"title": "Nature",
"river": "River",
"waves": "Waves",
"campfire": "Campfire",
"wind": "Wind",
"howling-wind": "Howling Wind",
"wind-in-trees": "Wind in Trees",
"waterfall": "Waterfall",
"walk-in-snow": "Walk in Snow",
"walk-on-leaves": "Walk on Leaves",
"walk-on-gravel": "Walk on Gravel",
"droplets": "Droplets",
"jungle": "Jungle"
},
"noise": {
"title": "Noise",
"white-noise": "White Noise",
"pink-noise": "Pink Noise",
"brown-noise": "Brown Noise"
},
"places": {
"title": "Places",
"cafe": "Cafe",
"airport": "Airport",
"church": "Church",
"temple": "Temple",
"construction-site": "Construction Site",
"underwater": "Underwater",
"crowded-bar": "Crowded Bar",
"night-village": "Night Village",
"subway-station": "Subway Station",
"office": "Office",
"supermarket": "Supermarket",
"carousel": "Carousel",
"laboratory": "Laboratory",
"laundry-room": "Laundry Room",
"restaurant": "Restaurant",
"library": "Library"
},
"rain": {
"title": "Rain",
"light-rain": "Light Rain",
"heavy-rain": "Heavy Rain",
"thunder": "Thunder",
"rain-on-window": "Rain on Window",
"rain-on-car-roof": "Rain on Car Roof",
"rain-on-umbrella": "Rain on Umbrella",
"rain-on-tent": "Rain on Tent",
"rain-on-leaves": "Rain on Leaves"
},
"things": {
"title": "Things",
"keyboard": "Keyboard",
"typewriter": "Typewriter",
"paper": "Paper",
"clock": "Clock",
"wind-chimes": "Wind Chimes",
"singing-bowl": "Singing Bowl",
"ceiling-fan": "Ceiling Fan",
"dryer": "Dryer",
"slide-projector": "Slide Projector",
"boiling-water": "Boiling Water",
"bubbles": "Bubbles",
"tuning-radio": "Tuning Radio",
"morse-code": "Morse Code",
"washing-machine": "Washing Machine",
"vinyl-effect": "Vinyl Effect",
"windshield-wipers": "Windshield Wipers"
},
"transport": {
"title": "Transport",
"train": "Train",
"inside-a-train": "Inside a Train",
"airplane": "Airplane",
"submarine": "Submarine",
"sailboat": "Sailboat",
"rowing-boat": "Rowing Boat"
},
"urban": {
"title": "Urban",
"highway": "Highway",
"road": "Road",
"ambulance-siren": "Ambulance Siren",
"busy-street": "Busy Street",
"crowd": "Crowd",
"traffic": "Traffic",
"fireworks": "Fireworks"
}
}
}

View file

@ -8,14 +8,6 @@
"en": "English",
"zh": "简体中文"
},
"languageSwitcher": {
"label": "语言选择"
},
"common": {
"play": "播放",
"pause": "暂停",
"close": "关闭"
},
"modals": {
"reload": {
"title": "发现新内容",
@ -24,11 +16,32 @@
}
},
"buttons": {
"playError": "请先选择要播放的声音。",
"useMoodist": "使用 Moodist"
},
"categories": {
"favorites": "收藏夹"
"play": {
"label": "播放",
"error": "请先选择要播放的声音。"
},
"favorite": {
"add": {
"aria-label": "将 {{label}} 声音添加到收藏夹"
},
"remove": {
"aria-label": "从收藏夹移除 {{label}} 声音"
}
},
"unselect": {
"tooltip": "取消选择所有声音。",
"aria-label": "取消选择所有声音",
"restore": {
"tooltip": "恢复上次选择的声音。",
"aria-label": "恢复上次选择的声音"
}
},
"pause": {
"label": "暂停"
},
"use-moodist": {
"label": "使用 Moodist"
}
},
"about": {
"section1": {
@ -47,5 +60,132 @@
"title": "适合每一刻的声音",
"body": "无论您是想在漫长的一天后放松身心在工作时提高注意力还是哄自己进入宁静的睡眠Moodist 都有完美的声音景观等着您。最棒的是?它完全免费且开源,因此您可以无任何附加条件地享受其益处。立即开始使用 Moodist发现您宁静与专注的新港湾"
}
},
"volume": {
"aria-label": "{{label}} 声音音量"
},
"sounds": {
"show-less": "收起",
"show-more": "显示更多",
"aria-label": "{{name}} 声音",
"favorites": {
"title": "收藏夹"
},
"animals": {
"title": "动物",
"birds": "鸟鸣",
"seagulls": "海鸥",
"crickets": "蟋蟀",
"wolf": "狼嚎",
"owl": "猫头鹰",
"frog": "蛙鸣",
"dog-barking": "狗吠",
"horse-galopp": "马蹄",
"cat-purring": "猫打呼",
"crows": "乌鸦",
"whale": "鲸鱼",
"beehive": "蜂巢",
"woodpecker": "啄木鸟",
"chickens": "鸡",
"cows": "牛",
"sheep": "羊"
},
"binaural": {
"title": "双耳节拍",
"binaural-delta": "Delta 波",
"binaural-theta": "Theta 波",
"binaural-alpha": "Alpha 波",
"binaural-beta": "Beta 波",
"binaural-gamma": "Gamma 波"
},
"nature": {
"title": "自然",
"river": "河流",
"waves": "海浪",
"campfire": "篝火",
"wind": "风声",
"howling-wind": "呼啸的风",
"wind-in-trees": "林间风声",
"waterfall": "瀑布",
"walk-in-snow": "雪地行走",
"walk-on-leaves": "踩树叶",
"walk-on-gravel": "踩砾石",
"droplets": "水滴",
"jungle": "丛林"
},
"noise": {
"title": "噪音",
"white-noise": "白噪音",
"pink-noise": "粉红噪音",
"brown-noise": "布朗噪音"
},
"places": {
"title": "场所",
"cafe": "咖啡馆",
"airport": "机场",
"church": "教堂",
"temple": "寺庙",
"construction-site": "建筑工地",
"underwater": "水下",
"crowded-bar": "拥挤的酒吧",
"night-village": "夜晚村庄",
"subway-station": "地铁站",
"office": "办公室",
"supermarket": "超市",
"carousel": "旋转木马",
"laboratory": "实验室",
"laundry-room": "洗衣房",
"restaurant": "餐厅",
"library": "图书馆"
},
"rain": {
"title": "雨声",
"light-rain": "小雨",
"heavy-rain": "大雨",
"thunder": "雷声",
"rain-on-window": "雨打窗户",
"rain-on-car-roof": "雨打车顶",
"rain-on-umbrella": "雨打雨伞",
"rain-on-tent": "雨打帐篷",
"rain-on-leaves": "雨打树叶"
},
"things": {
"title": "物品",
"keyboard": "键盘",
"typewriter": "打字机",
"paper": "纸张",
"clock": "时钟",
"wind-chimes": "风铃",
"singing-bowl": "颂钵",
"ceiling-fan": "吊扇",
"dryer": "烘干机",
"slide-projector": "幻灯机",
"boiling-water": "沸水",
"bubbles": "气泡",
"tuning-radio": "调谐收音机",
"morse-code": "摩尔斯电码",
"washing-machine": "洗衣机",
"vinyl-effect": "黑胶唱片",
"windshield-wipers": "雨刮器"
},
"transport": {
"title": "交通",
"train": "火车",
"inside-a-train": "火车内部",
"airplane": "飞机",
"submarine": "潜水艇",
"sailboat": "帆船",
"rowing-boat": "划艇"
},
"urban": {
"title": "城市",
"highway": "高速公路",
"road": "马路",
"ambulance-siren": "救护车警报",
"busy-street": "繁忙街道",
"crowd": "人群",
"traffic": "交通",
"fireworks": "烟花"
}
}
}