From bf88469895c4934c9575e170bbde4d0bbd12b037 Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Sat, 10 Aug 2024 22:45:46 +0800 Subject: [PATCH] :sparkles: feat: add i18n support Signed-off-by: SimonShiki --- locales/en.json | 43 ++++++++++ locales/index.ts | 12 +++ locales/zh-cn.json | 42 ++++++++++ package.json | 3 + src/components/album-item.tsx | 5 +- src/components/artist-item.tsx | 5 +- src/components/locale-provider.tsx | 14 ++++ src/components/navigation.tsx | 22 +++--- src/components/now-playing.tsx | 9 ++- src/components/title-bar.tsx | 3 +- src/jotais/settings.ts | 4 + src/main.tsx | 9 ++- src/pages/album.tsx | 11 ++- src/pages/artist.tsx | 11 ++- src/pages/local.tsx | 20 +++-- src/pages/ncm.tsx | 27 +++++-- src/pages/settings.tsx | 112 +++++++++++++++++++------- src/pages/song.tsx | 20 +++-- vite.config.ts | 2 +- yarn.lock | 121 ++++++++++++++++++++++++++--- 20 files changed, 418 insertions(+), 77 deletions(-) create mode 100644 locales/en.json create mode 100644 locales/index.ts create mode 100644 locales/zh-cn.json create mode 100644 src/components/locale-provider.tsx diff --git a/locales/en.json b/locales/en.json new file mode 100644 index 0000000..b86cdc4 --- /dev/null +++ b/locales/en.json @@ -0,0 +1,43 @@ +{ + "+0X5KL": "Scanning...", + "2/2yg+": "Add", + "3HBU/l": "Remote Songlist", + "47FYwb": "Cancel", + "4cTGtq": "Sync song lists", + "5ztrr5": "Cache remote songs in local", + "A+Fjz9": "Use streaming (Experimental)", + "AzGCoc": "Cicadas", + "D3idYv": "Settings", + "D9WUz8": "Albums", + "D9n+q2": "Random", + "DUIDEq": "Storage", + "F62y+K": "Sign Out", + "FpsJ5J": "163 Account", + "J3ca41": "Play", + "MMDobX": "WebDAV", + "OI9isw": "NetEase Cloud Music", + "Pfa+Li": "Remote storages", + "QOZT71": "Scan folders", + "QqmO+m": "{total} Songs", + "Ra3++g": "NCM", + "StcK67": "Library", + "Ub+AGc": "Sign In", + "W4SaxY": "Local", + "ZaKnsn": "Sign in to get the song list...", + "ZqtXuy": "Scan storages", + "cyR7Kh": "Back", + "gjBiyj": "Loading...", + "i+P3rZ": "Auto-scanning behavior", + "j+BR5t": "Songlists", + "m6bUGd": "Sort By:", + "mOFG3K": "Start", + "pY1mMS": "Songs", + "sVX1a7": "Folders to scan", + "sm72ff": "Playlist ({total})", + "xMnH8F": "Load More", + "xmcVZ0": "Search", + "y1Z3or": "Language", + "yOcWit": "Add Folders", + "yoNyUi": "You've not logged in...", + "zxUgzJ": "Artists" +} diff --git a/locales/index.ts b/locales/index.ts new file mode 100644 index 0000000..9191328 --- /dev/null +++ b/locales/index.ts @@ -0,0 +1,12 @@ +import en from './en.json'; +import zhCN from './zh-cn.json'; + +export default { + en, + 'zh-CN': zhCN +} as const; + +export const langMap = [ + {value: 'en', label: 'English'}, + {value: 'zh-CN', label: '简体中文'} +]; diff --git a/locales/zh-cn.json b/locales/zh-cn.json new file mode 100644 index 0000000..0b9f099 --- /dev/null +++ b/locales/zh-cn.json @@ -0,0 +1,42 @@ +{ + "+0X5KL": "扫描中...", + "2/2yg+": "添加", + "3HBU/l": "远程歌单", + "47FYwb": "取消", + "5ztrr5": "在本地缓存远程歌曲", + "A+Fjz9": "流式播放 (实验性)", + "AzGCoc": "知音", + "D3idYv": "设置", + "D9WUz8": "专辑", + "D9n+q2": "随机播放", + "DUIDEq": "存储", + "F62y+K": "登出", + "FpsJ5J": "网易账户", + "J3ca41": "播放", + "MMDobX": "WebDAV", + "OI9isw": "网易云音乐", + "Ra3++g": "网易云音乐", + "Pfa+Li": "远程存储", + "QOZT71": "扫描", + "QqmO+m": "{total} 首歌", + "StcK67": "音乐库", + "Ub+AGc": "登录", + "W4SaxY": "本地", + "ZaKnsn": "登录以获取个人歌单...", + "ZqtXuy": "扫描", + "cyR7Kh": "返回", + "gjBiyj": "加载中...", + "i+P3rZ": "自动扫描行为", + "m6bUGd": "排序方式", + "mOFG3K": "开始", + "sVX1a7": "要扫描的文件夹", + "sm72ff": "播放列表 ({total})", + "xMnH8F": "加载更多", + "y1Z3or": "语言", + "xmcVZ0": "搜索", + "yOcWit": "添加文件夹", + "yoNyUi": "你还没有登录...", + "j+BR5t": "歌单", + "pY1mMS": "所有音乐", + "zxUgzJ": "艺术家" +} diff --git a/package.json b/package.json index ab89c7d..fa9f208 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "build": "tsc && vite build", "preview": "vite preview", "lint": "eslint ./src/ --ext .js,.jsx,.ts,.tsx", + "i18n:extract": "formatjs extract --ignore=\"**/*.d.ts\" --out-file locales/en.json --format simple \"src/**/*.{ts,tsx}\"", "tauri": "tauri" }, "dependencies": { @@ -24,6 +25,7 @@ "optics-ts": "^2.4.1", "react": "19.0.0-rc-8269d55d-20240802", "react-dom": "19.0.0-rc-8269d55d-20240802", + "react-intl": "^6.6.8", "react-router-dom": "^6.26.0", "react-virtuoso": "^4.9.0" }, @@ -40,6 +42,7 @@ "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "@vitejs/plugin-react": "^4.2.1", + "babel-plugin-formatjs": "^10.5.16", "babel-plugin-react-compiler": "^0.0.0-experimental-334f00b-20240725", "commitlint-config-gitmoji": "^2.3.1", "eslint": "8", diff --git a/src/components/album-item.tsx b/src/components/album-item.tsx index d7c992d..45b02be 100644 --- a/src/components/album-item.tsx +++ b/src/components/album-item.tsx @@ -1,3 +1,4 @@ +import { FormattedMessage } from 'react-intl'; import { Album } from '../jotais/library'; interface AlbumItemProps { @@ -13,7 +14,9 @@ export default function AlbumItem ({album, onClick}: AlbumItemProps) { {album.name}
{album.name} - {album.songs.length} Songs + + +
); diff --git a/src/components/artist-item.tsx b/src/components/artist-item.tsx index 5174aaf..89bb6b7 100644 --- a/src/components/artist-item.tsx +++ b/src/components/artist-item.tsx @@ -1,3 +1,4 @@ +import { FormattedMessage } from 'react-intl'; import { Artist } from '../jotais/library'; interface ArtistItemProps { @@ -13,7 +14,9 @@ export default function ArtistItem({ artist, onClick }: ArtistItemProps) { {artist.name}
{artist.name} - {artist.songs.length} Songs + + +
); diff --git a/src/components/locale-provider.tsx b/src/components/locale-provider.tsx new file mode 100644 index 0000000..14e0e7c --- /dev/null +++ b/src/components/locale-provider.tsx @@ -0,0 +1,14 @@ +import { useAtomValue } from 'jotai'; +import { IntlProvider } from 'react-intl'; +import { localeJotai } from '../jotais/settings'; +import messages from '../../locales'; +import { PropsWithChildren } from 'react'; + +export default function LocaleProvider ({children}: PropsWithChildren) { + const locale = useAtomValue(localeJotai); + return ( + + {children} + + ); +} diff --git a/src/components/navigation.tsx b/src/components/navigation.tsx index 6ada4bd..1eb895f 100644 --- a/src/components/navigation.tsx +++ b/src/components/navigation.tsx @@ -1,3 +1,4 @@ +import { useIntl } from 'react-intl'; import { Link, useLocation } from 'react-router-dom'; interface NavItemProps { @@ -18,20 +19,21 @@ function NavItem ({ path, icon, label }: NavItemProps) { } export default function Navigation () { + const intl = useIntl(); const navItems = [ { - section: 'Library', items: [ - { path: '/', icon: 'i-fluent:music-note-1-20-regular', label: 'Songs' }, - { path: '/album', icon: 'i-fluent:album-20-regular', label: 'Albums' }, - { path: '/artist', icon: 'i-fluent:people-20-regular', label: 'Artists' }, - { path: '/songlist', icon: 'i-fluent:list-20-regular', label: 'Songlists' }, + section: intl.formatMessage({ defaultMessage: 'Library'}), items: [ + { path: '/', icon: 'i-fluent:music-note-1-20-regular', label: intl.formatMessage({ defaultMessage: 'Songs'}) }, + { path: '/album', icon: 'i-fluent:album-20-regular', label: intl.formatMessage({ defaultMessage: 'Albums' }) }, + { path: '/artist', icon: 'i-fluent:people-20-regular', label: intl.formatMessage({ defaultMessage: 'Artists' }) }, + { path: '/songlist', icon: 'i-fluent:list-20-regular', label: intl.formatMessage({ defaultMessage: 'Songlists' }) }, ] }, { - section: 'Storage', items: [ - { path: '/local', icon: 'i-fluent:folder-20-regular', label: 'Local' }, - { path: '/webdav', icon: 'i-fluent:folder-globe-20-regular', label: 'WebDAV' }, - { path: '/ncm', icon: 'i-simple-icons:neteasecloudmusic', label: 'Netease' }, + section: intl.formatMessage({ defaultMessage: 'Storage' }), items: [ + { path: '/local', icon: 'i-fluent:folder-20-regular', label: intl.formatMessage({ defaultMessage: 'Local' }) }, + { path: '/webdav', icon: 'i-fluent:folder-globe-20-regular', label: intl.formatMessage({ defaultMessage: 'WebDAV' }) }, + { path: '/ncm', icon: 'i-simple-icons:neteasecloudmusic', label: intl.formatMessage({ defaultMessage: 'NCM' }) }, ] }, ]; @@ -48,7 +50,7 @@ export default function Navigation () { ))}
- +
); } diff --git a/src/components/now-playing.tsx b/src/components/now-playing.tsx index 83110f5..bfb8f47 100644 --- a/src/components/now-playing.tsx +++ b/src/components/now-playing.tsx @@ -12,6 +12,7 @@ import { Virtuoso } from 'react-virtuoso'; import Lyrics from './lyrics'; import { focusAtom } from 'jotai-optics'; import { settingsJotai } from '../jotais/settings'; +import { FormattedMessage } from 'react-intl'; const playModeIconMap: Record = { list: 'i-fluent:arrow-repeat-all-off-20-regular', @@ -105,7 +106,9 @@ export default function NowPlaying () { - Playlist ({playlist.length}) + + + { @@ -183,7 +186,9 @@ export default function NowPlaying () { - Playlist ({playlist.length}) + + + { diff --git a/src/components/title-bar.tsx b/src/components/title-bar.tsx index a8e3a01..66da8f7 100644 --- a/src/components/title-bar.tsx +++ b/src/components/title-bar.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from 'react'; import { nowPlayingPageJotai } from '../jotais/play'; import { listen, UnlistenFn } from '@tauri-apps/api/event'; import { getCurrentWindow } from '@tauri-apps/api/window'; +import { FormattedMessage } from 'react-intl'; const appWindow = getCurrentWindow(); @@ -47,7 +48,7 @@ export default function TitleBar () { - Cicadas +
{ appWindow.minimize(); diff --git a/src/jotais/settings.ts b/src/jotais/settings.ts index b07aa50..668b881 100644 --- a/src/jotais/settings.ts +++ b/src/jotais/settings.ts @@ -1,5 +1,6 @@ import { focusAtom } from 'jotai-optics'; import { atomWithStorage } from 'jotai/utils'; +import sharedStore from './shared-store'; export interface StorageConfig { identifer: Idenfiter; @@ -7,14 +8,17 @@ export interface StorageConfig { export interface Setting { streaming: boolean; + locale: string; storages: Record | undefined>; } export const settingsJotai = atomWithStorage('settings', { streaming: false, + locale: navigator.language, storages: {} }, undefined, { getOnInit: true }); +export const localeJotai = focusAtom(settingsJotai, (optic) => optic.prop('locale')); export const storagesConfigJotai = focusAtom(settingsJotai, (optic) => optic.prop('storages')); diff --git a/src/main.tsx b/src/main.tsx index b62f645..6853841 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -7,12 +7,15 @@ import router from './router'; import { Provider } from 'jotai'; import TitleBar from './components/title-bar'; import sharedStore from './jotais/shared-store'; +import LocaleProvider from './components/locale-provider'; ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( - - + + + + - , + ); diff --git a/src/pages/album.tsx b/src/pages/album.tsx index 2154c6c..c287146 100644 --- a/src/pages/album.tsx +++ b/src/pages/album.tsx @@ -8,6 +8,7 @@ import { useCallback, useState } from 'react'; import SongItem from '../components/song-item'; import * as player from '../utils/player'; import Button from '../components/base/button'; +import { FormattedMessage } from 'react-intl'; export default function AlbumPage() { const scanned = useAtomValue(scannedJotai); @@ -23,12 +24,16 @@ export default function AlbumPage() { return (
- Albums + + +
{!scanned && (
- Scanning... + + +
)}
@@ -56,7 +61,7 @@ export default function AlbumPage() { onClick={() => setCurrentAlbum(null)} > - Back +

{currentAlbum.name}

diff --git a/src/pages/artist.tsx b/src/pages/artist.tsx index 7712a61..32ce13c 100644 --- a/src/pages/artist.tsx +++ b/src/pages/artist.tsx @@ -8,6 +8,7 @@ import SongItem from '../components/song-item'; import * as player from '../utils/player'; import Button from '../components/base/button'; import ArtistItem from '../components/artist-item'; +import { FormattedMessage } from 'react-intl'; export default function ArtistPage () { const scanned = useAtomValue(scannedJotai); @@ -23,12 +24,16 @@ export default function ArtistPage () { return (
- Artists + + +
{!scanned && (
- Scanning... + + +
)}
@@ -56,7 +61,7 @@ export default function ArtistPage () { onClick={() => setCurrentArtist(null)} > - Back +

{currentArtist.name}

diff --git a/src/pages/local.tsx b/src/pages/local.tsx index 6b73be4..6446fce 100644 --- a/src/pages/local.tsx +++ b/src/pages/local.tsx @@ -12,6 +12,7 @@ import * as player from '../utils/player'; import { useCallback, useEffect, useState } from 'react'; import { filterSongList, SortOptions, sortSongList } from '../utils/sort'; import { nowPlayingBarJotai } from '../jotais/play'; +import { FormattedMessage, useIntl } from 'react-intl'; const localStorageJotai = focusAtom(storagesJotai, (optic) => optic.prop('local')); const songlistJotai = focusAtom(localStorageJotai, (optic) => optic.prop('songList')); @@ -32,6 +33,7 @@ export default function Local () { const scanned = useAtomValue(scannedJotai); const [keyword, setKeyword] = useState(''); const [sortBy, setSortBy] = useState('a-z'); + const intl = useIntl(); const handleClickSong = useCallback((song: Song<'local'>) => { player.clearPlaylist(); player.addToPlaylist(...list); @@ -55,19 +57,27 @@ export default function Local () { return (
- Local + + +
- + {!scanned && (
- Scanning... + + +
)} - { + { setKeyword(e.target.value); }} after={} className='m-l-auto' /> - Sort By: + + + setSearchText(e.target.value)} onEnter={() => setIsSearching(true)} @@ -159,7 +166,9 @@ export default function NCM () { return (
- Remote Songlist + + +
{songlist.length > 0 ? ( ) : (
- {ncmConfig.loggedIn ? : Sign in to get the song list...} + {ncmConfig.loggedIn ? : + + }
)}
@@ -189,7 +200,9 @@ export default function NCM () { {songlistName}
); diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index 98e31a9..c51c623 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -4,7 +4,7 @@ import Card from '../components/base/card'; import Input from '../components/base/input'; import Select from '../components/base/select'; import Switch from '../components/base/switch'; -import { settingsJotai, storagesConfigJotai } from '../jotais/settings'; +import { localeJotai, settingsJotai, storagesConfigJotai } from '../jotais/settings'; import { SetStateAction, useAtom, useAtomValue, useSetAtom, WritableAtom } from 'jotai'; import type { LocalConfig } from '../storages/local'; import { useCallback, useEffect, useState } from 'react'; @@ -16,6 +16,8 @@ import md5 from 'md5'; import { NCMConfig } from '../storages/ncm'; import Spinner from '../components/base/spinner'; import { nowPlayingBarJotai } from '../jotais/play'; +import { FormattedMessage } from 'react-intl'; +import { langMap } from '../../locales'; const autoScanOptions = [ {value: 'startup', label: 'Each startup'} as const, @@ -36,6 +38,7 @@ const localStorageJotai = focusAtom(storagesJotai, (optic) => optic.prop('local' const localScannedJotai = focusAtom(localStorageJotai, (optic) => optic.prop('scanned')); export default function Settings () { + const [locale, setLocale] = useAtom(localeJotai); const [localFolders, setLocalFolders] = useAtom(localFoldersJotai); const [localAutoScan, setLocalAutoScan] = useAtom(localAutoScanJotai); const [phone, setPhone] = useState(''); @@ -126,24 +129,47 @@ export default function Settings () { return (
- Settings - Play + + + + + + + +
+ + + + + { setLocalAutoScan(value); }} /> @@ -176,45 +204,69 @@ export default function Settings () {
- Scan folders - {!localScanned && Scanning...} - + + + + {!localScanned && + + } +
- WebDAV + + +
- Remote storages - + + + +
- Cache remote songs in local + + +
- Auto-scanning behavior + + + { + { setKeyword(e.target.value); }} after={} className='m-l-auto' /> - Sort By: + + +