mirror of
https://github.com/remvze/moodist.git
synced 2025-12-18 17:34:17 +00:00
feat: integrate basic i18n support using i18next and react-i18next
This commit is contained in:
parent
b171793040
commit
09b400d234
13 changed files with 338 additions and 74 deletions
|
|
@ -4,6 +4,13 @@ import react from '@astrojs/react';
|
|||
import AstroPWA from '@vite-pwa/astro';
|
||||
|
||||
export default defineConfig({
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en', 'zh'],
|
||||
routing: {
|
||||
prefixDefaultLocale: false,
|
||||
},
|
||||
},
|
||||
integrations: [
|
||||
react(),
|
||||
AstroPWA({
|
||||
|
|
|
|||
80
package-lock.json
generated
80
package-lock.json
generated
|
|
@ -24,10 +24,12 @@
|
|||
"focus-trap-react": "10.2.3",
|
||||
"framer-motion": "10.16.4",
|
||||
"howler": "2.2.4",
|
||||
"i18next": "25.0.0",
|
||||
"js-confetti": "0.12.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hotkeys-hook": "3.2.1",
|
||||
"react-i18next": "15.4.1",
|
||||
"react-icons": "4.11.0",
|
||||
"react-wrap-balancer": "1.1.0",
|
||||
"uuid": "10.0.0",
|
||||
|
|
@ -2042,9 +2044,10 @@
|
|||
"integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA=="
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.1.tgz",
|
||||
"integrity": "sha512-hC2v6p8ZSI/W0HUzh3V8C5g+NwSKzKPtJwSpTjwl0o297GP9+ZLQSkdvHz46CM3LqyoXxq+5G9komY+eSqSO0g==",
|
||||
"version": "7.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
|
||||
"integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
},
|
||||
|
|
@ -17486,6 +17489,15 @@
|
|||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
|
||||
"integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="
|
||||
},
|
||||
"node_modules/html-parse-stringify": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
|
||||
"integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"void-elements": "3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/html-tags": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz",
|
||||
|
|
@ -17570,6 +17582,37 @@
|
|||
"url": "https://github.com/sponsors/typicode"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "25.0.0",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-25.0.0.tgz",
|
||||
"integrity": "sha512-POPvwjOPR1GQvRnbikTMPEhQD+ekd186MHE6NtVxl3Lby+gPp0iq60eCqGrY6wfRnp1lejjFNu0EKs1afA322w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://locize.com/i18next.html"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.26.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
|
|
@ -22247,6 +22290,28 @@
|
|||
"react-dom": ">=16.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "15.4.1",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.4.1.tgz",
|
||||
"integrity": "sha512-ahGab+IaSgZmNPYXdV1n+OYky95TGpFwnKRflX/16dY04DsYYKHtVLjeny7sBSCREEcoMbAgSkFiGLF5g5Oofw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.25.0",
|
||||
"html-parse-stringify": "^3.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"i18next": ">= 23.2.3",
|
||||
"react": ">= 16.8.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-icons": {
|
||||
"version": "4.11.0",
|
||||
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.11.0.tgz",
|
||||
|
|
@ -26633,6 +26698,15 @@
|
|||
"url": "https://opencollective.com/vitest"
|
||||
}
|
||||
},
|
||||
"node_modules/void-elements": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
|
||||
"integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz",
|
||||
|
|
|
|||
|
|
@ -40,10 +40,12 @@
|
|||
"focus-trap-react": "10.2.3",
|
||||
"framer-motion": "10.16.4",
|
||||
"howler": "2.2.4",
|
||||
"i18next": "25.0.0",
|
||||
"js-confetti": "0.12.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hotkeys-hook": "3.2.1",
|
||||
"react-i18next": "15.4.1",
|
||||
"react-icons": "4.11.0",
|
||||
"react-wrap-balancer": "1.1.0",
|
||||
"uuid": "10.0.0",
|
||||
|
|
|
|||
|
|
@ -2,26 +2,18 @@
|
|||
import { Container } from '@/components/container';
|
||||
|
||||
import { count as soundCount } from '@/lib/sounds';
|
||||
import { getTranslator } from '@/i18n/utils';
|
||||
|
||||
const currentLocale = Astro.currentLocale;
|
||||
const t = await getTranslator(currentLocale);
|
||||
|
||||
const count = soundCount();
|
||||
|
||||
const paragraphs = [
|
||||
{
|
||||
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: 'Free Ambient Sounds',
|
||||
},
|
||||
{
|
||||
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.`,
|
||||
title: 'Carefully Curated Sounds',
|
||||
},
|
||||
{
|
||||
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: 'Create Your Soundscape',
|
||||
},
|
||||
{
|
||||
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!",
|
||||
title: 'Sounds for Every Moment',
|
||||
},
|
||||
const paragraphKeys = [
|
||||
'about.section1',
|
||||
'about.section2',
|
||||
'about.section3',
|
||||
'about.section4',
|
||||
];
|
||||
---
|
||||
|
||||
|
|
@ -30,19 +22,17 @@ const paragraphs = [
|
|||
|
||||
<Container tight>
|
||||
{
|
||||
paragraphs.map((paragraph, index) => (
|
||||
paragraphKeys.map((key, index) => (
|
||||
<div class="paragraph">
|
||||
<div class="counter">
|
||||
<span>0{index + 1}</span> / 0{paragraphs.length}
|
||||
<span>0{index + 1}</span> / 0{paragraphKeys.length}
|
||||
</div>
|
||||
|
||||
<h2 class="title">{paragraph.title}</h2>
|
||||
<p class="body">{paragraph.body}</p>
|
||||
<h2 class="title">{t(`${key}.title`)}</h2>
|
||||
<p class="body">{t(`${key}.body`, { count: count })}</p>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
|
||||
<button class="button" id="use-moodist"> Use Moodist</button>
|
||||
<button class="button" id="use-moodist">{t('buttons.useMoodist')}</button>
|
||||
</Container>
|
||||
</section>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ 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 i18n from '@/i18n';
|
||||
|
||||
import { useSoundStore } from '@/stores/sound';
|
||||
|
||||
|
|
@ -17,11 +19,24 @@ import { MediaControls } from '@/components/media-controls';
|
|||
import { sounds } from '@/data/sounds';
|
||||
import { FADE_OUT } from '@/constants/events';
|
||||
|
||||
import type { Sound } from '@/data/types';
|
||||
import type { Sound, Category as CategoryType } from '@/data/types';
|
||||
import { subscribe } from '@/lib/event';
|
||||
|
||||
export function App() {
|
||||
const categories = useMemo(() => sounds.categories, []);
|
||||
interface AppProps {
|
||||
locale: string; // 接收来自 Astro 的 locale
|
||||
}
|
||||
|
||||
export function App({ locale }: AppProps) {
|
||||
const { t } = useTranslation(); // 获取 t 函数,以便翻译 "Favorites"
|
||||
|
||||
useEffect(() => {
|
||||
if (locale && i18n.language !== locale) {
|
||||
i18n.changeLanguage(locale);
|
||||
}
|
||||
}, [locale]);
|
||||
|
||||
const categoriesData = useMemo(() => sounds.categories, []);
|
||||
const categories = categoriesData; // 暂时不翻译
|
||||
|
||||
const favorites = useSoundStore(useShallow(state => state.getFavorites()));
|
||||
const pause = useSoundStore(state => state.pause);
|
||||
|
|
@ -29,18 +44,20 @@ export function App() {
|
|||
const unlock = useSoundStore(state => state.unlock);
|
||||
|
||||
const favoriteSounds = useMemo(() => {
|
||||
const favoriteSounds = categories
|
||||
const allFlatSounds = categoriesData
|
||||
.map(category => category.sounds)
|
||||
.flat()
|
||||
.filter(sound => favorites.includes(sound.id));
|
||||
|
||||
/**
|
||||
* Reorder based on the order of favorites
|
||||
*/
|
||||
return favorites.map(favorite =>
|
||||
favoriteSounds.find(sound => sound.id === favorite),
|
||||
.flat();
|
||||
const favoriteSoundsData = allFlatSounds.filter(sound =>
|
||||
favorites.includes(sound.id),
|
||||
);
|
||||
}, [favorites, categories]);
|
||||
|
||||
// 暂时不翻译 sound labels
|
||||
return favorites
|
||||
.map(favoriteId =>
|
||||
favoriteSoundsData.find(sound => sound.id === favoriteId),
|
||||
)
|
||||
.filter((s): s is Sound => s !== undefined);
|
||||
}, [favorites, categoriesData]);
|
||||
|
||||
useEffect(() => {
|
||||
const onChange = () => {
|
||||
|
|
@ -72,33 +89,33 @@ export function App() {
|
|||
}, [pause, lock, unlock]);
|
||||
|
||||
const allCategories = useMemo(() => {
|
||||
const favorites = [];
|
||||
|
||||
const favs: CategoryType[] = [];
|
||||
if (favoriteSounds.length) {
|
||||
favorites.push({
|
||||
favs.push({
|
||||
icon: <BiSolidHeart />,
|
||||
id: 'favorites',
|
||||
sounds: favoriteSounds as Array<Sound>,
|
||||
title: 'Favorites',
|
||||
sounds: favoriteSounds,
|
||||
title: t('categories.favorites'),
|
||||
});
|
||||
}
|
||||
|
||||
return [...favorites, ...categories];
|
||||
}, [favoriteSounds, categories]);
|
||||
return [...favs, ...categories];
|
||||
}, [favoriteSounds, categories, t]);
|
||||
|
||||
return (
|
||||
<SnackbarProvider>
|
||||
<StoreConsumer>
|
||||
<MediaControls />
|
||||
<Container>
|
||||
<div id="app" />
|
||||
<Buttons />
|
||||
<Categories categories={allCategories} />
|
||||
</Container>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<SnackbarProvider>
|
||||
<StoreConsumer>
|
||||
<MediaControls />
|
||||
<Container>
|
||||
<div id="app" />
|
||||
<Buttons />
|
||||
<Categories categories={allCategories} />
|
||||
</Container>
|
||||
|
||||
<Toolbar />
|
||||
<SharedModal />
|
||||
</StoreConsumer>
|
||||
</SnackbarProvider>
|
||||
<Toolbar />
|
||||
<SharedModal />
|
||||
</StoreConsumer>
|
||||
</SnackbarProvider>
|
||||
</I18nextProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useCallback, useEffect } from 'react';
|
||||
import { BiPause, BiPlay } from 'react-icons/bi/index';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useSoundStore } from '@/stores/sound';
|
||||
import { useSnackbar } from '@/contexts/snackbar';
|
||||
|
|
@ -9,6 +10,7 @@ import { cn } from '@/helpers/styles';
|
|||
import styles from './play.module.css';
|
||||
|
||||
export function PlayButton() {
|
||||
const { t } = useTranslation();
|
||||
const isPlaying = useSoundStore(state => state.isPlaying);
|
||||
const pause = useSoundStore(state => state.pause);
|
||||
const toggle = useSoundStore(state => state.togglePlay);
|
||||
|
|
@ -20,10 +22,10 @@ export function PlayButton() {
|
|||
const handleToggle = useCallback(() => {
|
||||
if (locked) return;
|
||||
|
||||
if (noSelected) return showSnackbar('Please first select a sound to play.');
|
||||
if (noSelected) return showSnackbar(t('buttons.playError'));
|
||||
|
||||
toggle();
|
||||
}, [showSnackbar, toggle, noSelected, locked]);
|
||||
}, [showSnackbar, toggle, noSelected, locked, t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isPlaying && noSelected) pause();
|
||||
|
|
@ -42,14 +44,14 @@ export function PlayButton() {
|
|||
<span aria-hidden="true">
|
||||
<BiPause />
|
||||
</span>{' '}
|
||||
Pause
|
||||
{t('common.pause')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span aria-hidden="true">
|
||||
<BiPlay />
|
||||
</span>{' '}
|
||||
Play
|
||||
{t('common.play')}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
|
|
|||
30
src/i18n.ts
Normal file
30
src/i18n.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
import enTranslation from './locales/en/translation.json';
|
||||
import zhTranslation from './locales/zh/translation.json';
|
||||
|
||||
const resources = {
|
||||
en: {
|
||||
translation: enTranslation,
|
||||
},
|
||||
zh: {
|
||||
translation: zhTranslation,
|
||||
},
|
||||
};
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
debug: true,
|
||||
fallbackLng: 'en',
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
lng: 'en',
|
||||
react: {
|
||||
useSuspense: false,
|
||||
},
|
||||
|
||||
resources,
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
29
src/i18n/utils.ts
Normal file
29
src/i18n/utils.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import i18n from '@/i18n';
|
||||
import type { TFunction } from 'i18next';
|
||||
|
||||
/**
|
||||
* 获取指定语言的翻译函数 (t function)。
|
||||
* 这个函数是异步的,以防 i18next 需要异步加载资源或切换语言。
|
||||
* @param lng - 语言代码 (例如 'en', 'zh')。如果未提供,则使用当前或回退语言。
|
||||
* @returns Promise<TFunction> - 返回一个解析为翻译函数的 Promise。
|
||||
*/
|
||||
export async function getTranslator(lng?: string): Promise<TFunction> {
|
||||
const targetLng =
|
||||
lng || i18n.language || (i18n.options.fallbackLng as string[])[0];
|
||||
|
||||
// 如果 i18n 实例的当前语言与目标语言不一致,则切换语言
|
||||
// 注意:changeLanguage 是异步的
|
||||
if (i18n.language !== targetLng) {
|
||||
await i18n.changeLanguage(targetLng);
|
||||
}
|
||||
|
||||
return i18n.t;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前支持的语言列表
|
||||
* @returns string[]
|
||||
*/
|
||||
export function getSupportedLangs(): string[] {
|
||||
return Object.keys(i18n.options.resources || {});
|
||||
}
|
||||
|
|
@ -3,7 +3,9 @@ import { pwaInfo } from 'virtual:pwa-info'; // eslint-disable-line
|
|||
|
||||
import { Reload } from '@/components/reload';
|
||||
|
||||
import { count } from '@/lib/sounds';
|
||||
import { count as soundCount } from '@/lib/sounds';
|
||||
|
||||
import { getTranslator } from '@/i18n/utils';
|
||||
|
||||
import '@/styles/global.css';
|
||||
|
||||
|
|
@ -11,15 +13,16 @@ interface Props {
|
|||
description?: string;
|
||||
title?: string;
|
||||
}
|
||||
const currentLocale = Astro.currentLocale;
|
||||
const t = await getTranslator(currentLocale);
|
||||
|
||||
const title = Astro.props.title || 'Moodist: Ambient Sounds for Focus and Calm';
|
||||
const description =
|
||||
Astro.props.description ||
|
||||
`Moodist is a free and open-source ambient sound generator featuring ${count()} carefully curated sounds. Create your ideal atmosphere for relaxation, focus, or creativity with this versatile tool.`;
|
||||
const count = soundCount();
|
||||
const title = Astro.props.title || t('site.title');
|
||||
const description = Astro.props.description || t('site.description', { count });
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang={currentLocale}>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta content="width=device-width" name="viewport" />
|
||||
|
|
@ -33,7 +36,7 @@ const description =
|
|||
|
||||
<meta content={title} property="og:title" />
|
||||
<meta content={description} property="og:description" />
|
||||
<meta content="Moodist" property="og:site_name" />
|
||||
<meta content={t('site.ogSiteName')} property="og:site_name" />
|
||||
<meta content="https://moodist.app" property="og:url" />
|
||||
<meta content="website" property="og:type" />
|
||||
<meta content="https://moodist.app/og.png" property="og:image" />
|
||||
|
|
|
|||
45
src/locales/en/translation.json
Normal file
45
src/locales/en/translation.json
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"site": {
|
||||
"title": "Moodist: Ambient Sounds for Focus and Calm",
|
||||
"description": "Moodist is a free and open-source ambient sound generator featuring {{count}} carefully curated sounds. Create your ideal atmosphere for relaxation, focus, or creativity with this versatile tool.",
|
||||
"ogSiteName": "Moodist"
|
||||
},
|
||||
"common": {
|
||||
"play": "Play",
|
||||
"pause": "Pause",
|
||||
"close": "Close"
|
||||
},
|
||||
"modals": {
|
||||
"reload": {
|
||||
"title": "New Content",
|
||||
"description": "New content available, click on reload button to update.",
|
||||
"reloadButton": "Reload"
|
||||
}
|
||||
},
|
||||
|
||||
"buttons": {
|
||||
"playError": "Please first select a sound to play.",
|
||||
"useMoodist": "Use Moodist"
|
||||
},
|
||||
"categories": {
|
||||
"favorites": "Favorites"
|
||||
},
|
||||
"about": {
|
||||
"section1": {
|
||||
"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."
|
||||
},
|
||||
"section2": {
|
||||
"title": "Carefully Curated Sounds",
|
||||
"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."
|
||||
},
|
||||
"section3": {
|
||||
"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."
|
||||
},
|
||||
"section4": {
|
||||
"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!"
|
||||
}
|
||||
}
|
||||
}
|
||||
44
src/locales/zh/translation.json
Normal file
44
src/locales/zh/translation.json
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"site": {
|
||||
"title": "Moodist:专注与平静的环境声音",
|
||||
"description": "Moodist 是一个免费且开源的环境声音生成器,包含 {{count}} 种精心挑选的声音。使用这款多功能工具,为放松、专注或创造力营造理想的氛围。",
|
||||
"ogSiteName": "Moodist"
|
||||
},
|
||||
"common": {
|
||||
"play": "播放",
|
||||
"pause": "暂停",
|
||||
"close": "关闭"
|
||||
},
|
||||
"modals": {
|
||||
"reload": {
|
||||
"title": "发现新内容",
|
||||
"description": "检测到可用新内容,点击“重新加载”按钮进行更新。",
|
||||
"reloadButton": "重新加载"
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
"playError": "请先选择要播放的声音。",
|
||||
"useMoodist": "使用 Moodist"
|
||||
},
|
||||
"categories": {
|
||||
"favorites": "收藏夹"
|
||||
},
|
||||
"about": {
|
||||
"section1": {
|
||||
"title": "免费环境声音",
|
||||
"body": "渴望从日常喧嚣中获得平静的休憩?需要完美的声音环境来提升专注力或助您安然入睡?Moodist 就是您的答案——免费且开源的环境声音生成器!无需订阅和注册,Moodist 为您免费解锁一个舒缓、沉浸式的音频体验世界。"
|
||||
},
|
||||
"section2": {
|
||||
"title": "精心挑选的声音",
|
||||
"body": "探索包含 {{count}} 种精心策划声音的广阔音库。自然爱好者可以在潺潺溪流、规律的海浪拍岸或噼啪作响的温暖篝火中找到慰藉。城市景观在咖啡馆的轻柔嗡嗡声、火车有节奏的哐当声或交通的平静白噪音中栩栩如生。对于寻求更深层次专注或放松的人,Moodist 提供旨在改善您心境的双耳节拍和彩色噪音。"
|
||||
},
|
||||
"section3": {
|
||||
"title": "创建您的声音景观",
|
||||
"body": "Moodist 的美妙之处在于其简洁和可定制性。没有复杂的菜单或令人困惑的选项——只需选择您想要的声音,调整音量平衡,然后点击播放。想将鸟儿的轻柔啁啾与舒缓的雨声混合?没问题!随心所欲地叠加任意数量的声音,创造您个性化的声音绿洲。"
|
||||
},
|
||||
"section4": {
|
||||
"title": "适合每一刻的声音",
|
||||
"body": "无论您是想在漫长的一天后放松身心,在工作时提高注意力,还是哄自己进入宁静的睡眠,Moodist 都有完美的声音景观等着您。最棒的是?它完全免费且开源,因此您可以无任何附加条件地享受其益处。立即开始使用 Moodist,发现您宁静与专注的新港湾!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
---
|
||||
import Layout from '@/layouts/layout.astro';
|
||||
|
||||
import Donate from '@/components/donate.astro';
|
||||
import Hero from '@/components/hero.astro';
|
||||
import About from '@/components/about.astro';
|
||||
|
|
@ -8,12 +7,14 @@ import Source from '@/components/source.astro';
|
|||
import Footer from '@/components/footer.astro';
|
||||
|
||||
import { App } from '@/components/app';
|
||||
|
||||
const currentLocale = Astro.currentLocale || 'en';
|
||||
---
|
||||
|
||||
<Layout title="Moodist: Ambient Sounds for Focus and Calm">
|
||||
<Layout>
|
||||
<Donate />
|
||||
<Hero />
|
||||
<App client:load />
|
||||
<App client:load locale={currentLocale} />
|
||||
<About />
|
||||
<Source />
|
||||
<Footer />
|
||||
|
|
|
|||
20
src/pages/zh/index.astro
Normal file
20
src/pages/zh/index.astro
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
---
|
||||
import Layout from '@/layouts/layout.astro';
|
||||
import Donate from '@/components/donate.astro';
|
||||
import Hero from '@/components/hero.astro';
|
||||
import About from '@/components/about.astro';
|
||||
import Source from '@/components/source.astro';
|
||||
import Footer from '@/components/footer.astro';
|
||||
import { App } from '@/components/app';
|
||||
|
||||
const currentLocale = Astro.currentLocale || 'en';
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<Donate />
|
||||
<Hero />
|
||||
<App client:load locale={currentLocale} />
|
||||
<About />
|
||||
<Source />
|
||||
<Footer />
|
||||
</Layout>
|
||||
Loading…
Add table
Reference in a new issue