Compare commits
2 Commits
b74e985770
...
b69dcbb52c
| Author | SHA1 | Date | |
|---|---|---|---|
| b69dcbb52c | |||
| f5bb1ca507 |
13
messages/en-US/alphabet.json
Normal file
13
messages/en-US/alphabet.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"chooseCharacters": "Please select the characters you want to learn",
|
||||||
|
"japanese": "Japanese Kana",
|
||||||
|
"english": "English Alphabet",
|
||||||
|
"uyghur": "Uyghur Alphabet",
|
||||||
|
"esperanto": "Esperanto Alphabet",
|
||||||
|
"loading": "Loading...",
|
||||||
|
"loadFailed": "Loading failed, please try again",
|
||||||
|
"hideLetter": "Hide Letter",
|
||||||
|
"showLetter": "Show Letter",
|
||||||
|
"hideIPA": "Hide IPA",
|
||||||
|
"showIPA": "Show IPA"
|
||||||
|
}
|
||||||
33
messages/en-US/home.json
Normal file
33
messages/en-US/home.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"title": "Learn Languages",
|
||||||
|
"description": "Here is a very useful website to help you learn almost every language in the world, including constructed ones.",
|
||||||
|
"explore": "Explore",
|
||||||
|
"fortune": {
|
||||||
|
"quote": "Stay hungry, stay foolish.",
|
||||||
|
"author": "— Steve Jobs"
|
||||||
|
},
|
||||||
|
"translator": {
|
||||||
|
"name": "Translator",
|
||||||
|
"description": "Translate to any language and annotate with International Phonetic Alphabet (IPA)"
|
||||||
|
},
|
||||||
|
"textSpeaker": {
|
||||||
|
"name": "Text Speaker",
|
||||||
|
"description": "Recognize and read text aloud, supports loop playback and speed adjustment"
|
||||||
|
},
|
||||||
|
"srtPlayer": {
|
||||||
|
"name": "SRT Video Player",
|
||||||
|
"description": "Play videos sentence by sentence based on SRT subtitle files to mimic native speaker pronunciation"
|
||||||
|
},
|
||||||
|
"alphabet": {
|
||||||
|
"name": "Memorize Alphabet",
|
||||||
|
"description": "Start learning a new language from the alphabet"
|
||||||
|
},
|
||||||
|
"memorize": {
|
||||||
|
"name": "Memorize Words",
|
||||||
|
"description": "Language A to Language B, Language B to Language A, supports dictation"
|
||||||
|
},
|
||||||
|
"moreFeatures": {
|
||||||
|
"name": "More Features",
|
||||||
|
"description": "Under development, stay tuned"
|
||||||
|
}
|
||||||
|
}
|
||||||
4
messages/en-US/memorize/choose.json
Normal file
4
messages/en-US/memorize/choose.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"back": "Back",
|
||||||
|
"choose": "Choose"
|
||||||
|
}
|
||||||
6
messages/en-US/memorize/edit.json
Normal file
6
messages/en-US/memorize/edit.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"back": "Back",
|
||||||
|
"save": "Save Word Pairs",
|
||||||
|
"locale1": "Locale 1",
|
||||||
|
"locale2": "Locale 2"
|
||||||
|
}
|
||||||
10
messages/en-US/memorize/main.json
Normal file
10
messages/en-US/memorize/main.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"title": "Memorize",
|
||||||
|
"locale1": "Your selected locale 1 is {locale}",
|
||||||
|
"locale2": "Your selected locale 2 is {locale}",
|
||||||
|
"total": "There are {total} word pairs in total",
|
||||||
|
"start": "Start",
|
||||||
|
"import": "Import",
|
||||||
|
"save": "Save",
|
||||||
|
"edit": "Edit"
|
||||||
|
}
|
||||||
7
messages/en-US/memorize/start.json
Normal file
7
messages/en-US/memorize/start.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"show": "Show",
|
||||||
|
"reverse": "Reverse",
|
||||||
|
"dictation": "Dictation",
|
||||||
|
"back": "Back",
|
||||||
|
"next": "Next"
|
||||||
|
}
|
||||||
5
messages/en-US/navbar.json
Normal file
5
messages/en-US/navbar.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"title": "LL",
|
||||||
|
"about": "About",
|
||||||
|
"sourceCode": "GitHub"
|
||||||
|
}
|
||||||
10
messages/en-US/srt-player.json
Normal file
10
messages/en-US/srt-player.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"uploadVideo": "Upload Video",
|
||||||
|
"uploadSubtitle": "Upload Subtitle",
|
||||||
|
"pause": "Pause",
|
||||||
|
"play": "Play",
|
||||||
|
"previous": "Previous",
|
||||||
|
"next": "Next",
|
||||||
|
"restart": "Restart",
|
||||||
|
"autoPause": "Auto Pause ({enabled})"
|
||||||
|
}
|
||||||
5
messages/en-US/text-speaker.json
Normal file
5
messages/en-US/text-speaker.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"generateIPA": "Generate IPA",
|
||||||
|
"viewSavedItems": "View Saved Items",
|
||||||
|
"confirmDeleteAll": "Are you sure you want to delete everything? (Y/N)"
|
||||||
|
}
|
||||||
12
messages/en-US/translator.json
Normal file
12
messages/en-US/translator.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"detectLanguage": "detect language",
|
||||||
|
"generateIPA": "generate ipa",
|
||||||
|
"translateInto": "translate into",
|
||||||
|
"chinese": "Chinese",
|
||||||
|
"english": "English",
|
||||||
|
"italian": "Italian",
|
||||||
|
"other": "Other",
|
||||||
|
"translating": "translating...",
|
||||||
|
"translate": "translate",
|
||||||
|
"inputLanguage": "Input a language."
|
||||||
|
}
|
||||||
13
messages/zh-CN/alphabet.json
Normal file
13
messages/zh-CN/alphabet.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"chooseCharacters": "请选择您想学习的字符",
|
||||||
|
"japanese": "日语假名",
|
||||||
|
"english": "英文字母",
|
||||||
|
"uyghur": "维吾尔字母",
|
||||||
|
"esperanto": "世界语字母",
|
||||||
|
"loading": "加载中...",
|
||||||
|
"loadFailed": "加载失败,请重试",
|
||||||
|
"hideLetter": "隐藏字母",
|
||||||
|
"showLetter": "显示字母",
|
||||||
|
"hideIPA": "隐藏IPA",
|
||||||
|
"showIPA": "显示IPA"
|
||||||
|
}
|
||||||
33
messages/zh-CN/home.json
Normal file
33
messages/zh-CN/home.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"title": "学语言",
|
||||||
|
"description": "这里是一个非常有用的网站,可以帮助你学习世界上几乎每一种语言,包括人造语言。",
|
||||||
|
"explore": "探索网站",
|
||||||
|
"fortune": {
|
||||||
|
"quote": "求知若饥,虚心若愚。",
|
||||||
|
"author": "—— 史蒂夫·乔布斯"
|
||||||
|
},
|
||||||
|
"translator": {
|
||||||
|
"name": "翻译器",
|
||||||
|
"description": "翻译到任何语言,并标注国际音标(IPA)"
|
||||||
|
},
|
||||||
|
"textSpeaker": {
|
||||||
|
"name": "朗读器",
|
||||||
|
"description": "识别并朗读文本,支持循环朗读、朗读速度调节"
|
||||||
|
},
|
||||||
|
"srtPlayer": {
|
||||||
|
"name": "逐句视频播放器",
|
||||||
|
"description": "基于SRT字幕文件,逐句播放视频以模仿母语者的发音"
|
||||||
|
},
|
||||||
|
"alphabet": {
|
||||||
|
"name": "背字母",
|
||||||
|
"description": "从字母表开始新语言的学习"
|
||||||
|
},
|
||||||
|
"memorize": {
|
||||||
|
"name": "背单词",
|
||||||
|
"description": "语言A到语言B,语言B到语言A,支持听写"
|
||||||
|
},
|
||||||
|
"moreFeatures": {
|
||||||
|
"name": "更多功能",
|
||||||
|
"description": "开发中,敬请期待"
|
||||||
|
}
|
||||||
|
}
|
||||||
4
messages/zh-CN/memorize/choose.json
Normal file
4
messages/zh-CN/memorize/choose.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"back": "返回",
|
||||||
|
"choose": "选择"
|
||||||
|
}
|
||||||
6
messages/zh-CN/memorize/edit.json
Normal file
6
messages/zh-CN/memorize/edit.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"back": "返回",
|
||||||
|
"save": "保存单词对",
|
||||||
|
"locale1": "区域1",
|
||||||
|
"locale2": "区域2"
|
||||||
|
}
|
||||||
10
messages/zh-CN/memorize/main.json
Normal file
10
messages/zh-CN/memorize/main.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"title": "记忆",
|
||||||
|
"locale1": "您选择的区域一是{locale}",
|
||||||
|
"locale2": "您选择的区域二是{locale}",
|
||||||
|
"total": "总计有{total}个单词对",
|
||||||
|
"start": "开始",
|
||||||
|
"import": "导入",
|
||||||
|
"save": "保存",
|
||||||
|
"edit": "编辑"
|
||||||
|
}
|
||||||
7
messages/zh-CN/memorize/start.json
Normal file
7
messages/zh-CN/memorize/start.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"show": "显示",
|
||||||
|
"reverse": "反向",
|
||||||
|
"dictation": "听写",
|
||||||
|
"back": "返回",
|
||||||
|
"next": "下个"
|
||||||
|
}
|
||||||
5
messages/zh-CN/navbar.json
Normal file
5
messages/zh-CN/navbar.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"title": "学语言",
|
||||||
|
"about": "关于",
|
||||||
|
"sourceCode": "源码"
|
||||||
|
}
|
||||||
10
messages/zh-CN/srt-player.json
Normal file
10
messages/zh-CN/srt-player.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"uploadVideo": "上传视频",
|
||||||
|
"uploadSubtitle": "上传字幕",
|
||||||
|
"pause": "暂停",
|
||||||
|
"play": "播放",
|
||||||
|
"previous": "上句",
|
||||||
|
"next": "下句",
|
||||||
|
"restart": "句首",
|
||||||
|
"autoPause": "自动暂停({enabled})"
|
||||||
|
}
|
||||||
5
messages/zh-CN/text-speaker.json
Normal file
5
messages/zh-CN/text-speaker.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"generateIPA": "生成IPA",
|
||||||
|
"viewSavedItems": "查看保存项",
|
||||||
|
"confirmDeleteAll": "确定删光吗?(Y/N)"
|
||||||
|
}
|
||||||
12
messages/zh-CN/translator.json
Normal file
12
messages/zh-CN/translator.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"detectLanguage": "检测语言",
|
||||||
|
"generateIPA": "生成国际音标",
|
||||||
|
"translateInto": "翻译为",
|
||||||
|
"chinese": "中文",
|
||||||
|
"english": "英文",
|
||||||
|
"italian": "意大利语",
|
||||||
|
"other": "其他",
|
||||||
|
"translating": "翻译中...",
|
||||||
|
"translate": "翻译",
|
||||||
|
"inputLanguage": "请输入语言。"
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
import createNextIntlPlugin from "next-intl/plugin";
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
/* config options here */
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
allowedDevOrigins: ["192.168.3.65", "192.168.3.66"],
|
allowedDevOrigins: ["192.168.3.65", "192.168.3.66"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
const withNextIntl = createNextIntlPlugin();
|
||||||
|
export default withNextIntl(nextConfig);
|
||||||
|
|||||||
157
package-lock.json
generated
157
package-lock.json
generated
@@ -9,9 +9,12 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@formatjs/intl-localematcher": "^0.6.2",
|
||||||
"edge-tts-universal": "^1.3.2",
|
"edge-tts-universal": "^1.3.2",
|
||||||
"motion": "^12.23.24",
|
"motion": "^12.23.24",
|
||||||
|
"negotiator": "^1.0.0",
|
||||||
"next": "15.5.3",
|
"next": "15.5.3",
|
||||||
|
"next-intl": "^4.4.0",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
@@ -2452,6 +2455,57 @@
|
|||||||
"excpretty": "build/cli.js"
|
"excpretty": "build/cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@formatjs/ecma402-abstract": {
|
||||||
|
"version": "2.3.6",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz",
|
||||||
|
"integrity": "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@formatjs/fast-memoize": "2.2.7",
|
||||||
|
"@formatjs/intl-localematcher": "0.6.2",
|
||||||
|
"decimal.js": "^10.4.3",
|
||||||
|
"tslib": "^2.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@formatjs/fast-memoize": {
|
||||||
|
"version": "2.2.7",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz",
|
||||||
|
"integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@formatjs/icu-messageformat-parser": {
|
||||||
|
"version": "2.11.4",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.4.tgz",
|
||||||
|
"integrity": "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@formatjs/ecma402-abstract": "2.3.6",
|
||||||
|
"@formatjs/icu-skeleton-parser": "1.8.16",
|
||||||
|
"tslib": "^2.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@formatjs/icu-skeleton-parser": {
|
||||||
|
"version": "1.8.16",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.16.tgz",
|
||||||
|
"integrity": "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@formatjs/ecma402-abstract": "2.3.6",
|
||||||
|
"tslib": "^2.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@formatjs/intl-localematcher": {
|
||||||
|
"version": "0.6.2",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz",
|
||||||
|
"integrity": "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanfs/core": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.1",
|
"version": "0.19.1",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@humanfs/core/-/core-0.19.1.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/@humanfs/core/-/core-0.19.1.tgz",
|
||||||
@@ -3876,6 +3930,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@schummar/icu-type-parser": {
|
||||||
|
"version": "1.21.5",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz",
|
||||||
|
"integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@sinclair/typebox": {
|
"node_modules/@sinclair/typebox": {
|
||||||
"version": "0.27.8",
|
"version": "0.27.8",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@sinclair/typebox/-/typebox-0.27.8.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/@sinclair/typebox/-/typebox-0.27.8.tgz",
|
||||||
@@ -5019,6 +5079,17 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/accepts/node_modules/negotiator": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/negotiator/-/negotiator-0.6.3.tgz",
|
||||||
|
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.15.0",
|
"version": "8.15.0",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/acorn/-/acorn-8.15.0.tgz",
|
||||||
@@ -6528,6 +6599,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decimal.js": {
|
||||||
|
"version": "10.6.0",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/decimal.js/-/decimal.js-10.6.0.tgz",
|
||||||
|
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/deep-extend": {
|
"node_modules/deep-extend": {
|
||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/deep-extend/-/deep-extend-0.6.0.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||||
@@ -8462,6 +8539,18 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/intl-messageformat": {
|
||||||
|
"version": "10.7.18",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/intl-messageformat/-/intl-messageformat-10.7.18.tgz",
|
||||||
|
"integrity": "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"@formatjs/ecma402-abstract": "2.3.6",
|
||||||
|
"@formatjs/fast-memoize": "2.2.7",
|
||||||
|
"@formatjs/icu-messageformat-parser": "2.11.4",
|
||||||
|
"tslib": "^2.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/invariant": {
|
"node_modules/invariant": {
|
||||||
"version": "2.2.4",
|
"version": "2.2.4",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/invariant/-/invariant-2.2.4.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/invariant/-/invariant-2.2.4.tgz",
|
||||||
@@ -11483,12 +11572,10 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/negotiator": {
|
"node_modules/negotiator": {
|
||||||
"version": "0.6.3",
|
"version": "1.0.0",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/negotiator/-/negotiator-0.6.3.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/negotiator/-/negotiator-1.0.0.tgz",
|
||||||
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
@@ -11553,6 +11640,42 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/next-intl": {
|
||||||
|
"version": "4.4.0",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/next-intl/-/next-intl-4.4.0.tgz",
|
||||||
|
"integrity": "sha512-QHqnP9V9Pe7Tn0PdVQ7u1Z8k9yCkW5SJKeRy2g5gxzhSt/C01y3B9qNxuj3Fsmup/yreIHe6osxU6sFa+9WIkQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/amannn"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@formatjs/intl-localematcher": "^0.5.4",
|
||||||
|
"negotiator": "^1.0.0",
|
||||||
|
"use-intl": "^4.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0",
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/next-intl/node_modules/@formatjs/intl-localematcher": {
|
||||||
|
"version": "0.5.10",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/@formatjs/intl-localematcher/-/intl-localematcher-0.5.10.tgz",
|
||||||
|
"integrity": "sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/next/node_modules/postcss": {
|
"node_modules/next/node_modules/postcss": {
|
||||||
"version": "8.4.31",
|
"version": "8.4.31",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/postcss/-/postcss-8.4.31.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/postcss/-/postcss-8.4.31.tgz",
|
||||||
@@ -14028,10 +14151,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tar": {
|
"node_modules/tar": {
|
||||||
"version": "7.5.1",
|
"version": "7.5.2",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/tar/-/tar-7.5.1.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/tar/-/tar-7.5.2.tgz",
|
||||||
"integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==",
|
"integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==",
|
||||||
"license": "ISC",
|
"license": "BlueOak-1.0.0",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"peer": true,
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -14427,7 +14550,7 @@
|
|||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/typescript/-/typescript-5.9.3.tgz",
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
@@ -14634,6 +14757,20 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-intl": {
|
||||||
|
"version": "4.4.0",
|
||||||
|
"resolved": "https://mirrors.cloud.tencent.com/npm/use-intl/-/use-intl-4.4.0.tgz",
|
||||||
|
"integrity": "sha512-smFekJWtokDRBLC5/ZumlBREzdXOkw06+56Ifj2uRe9266Mk+yWQm2PcJO+EwlOE5sHIXHixOTzN6V8E0RGUbw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@formatjs/fast-memoize": "^2.2.0",
|
||||||
|
"@schummar/icu-type-parser": "1.21.5",
|
||||||
|
"intl-messageformat": "^10.5.14"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/utils-merge": {
|
"node_modules/utils-merge": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://mirrors.cloud.tencent.com/npm/utils-merge/-/utils-merge-1.0.1.tgz",
|
"resolved": "https://mirrors.cloud.tencent.com/npm/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||||
|
|||||||
@@ -10,9 +10,12 @@
|
|||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@formatjs/intl-localematcher": "^0.6.2",
|
||||||
"edge-tts-universal": "^1.3.2",
|
"edge-tts-universal": "^1.3.2",
|
||||||
"motion": "^12.23.24",
|
"motion": "^12.23.24",
|
||||||
|
"negotiator": "^1.0.0",
|
||||||
"next": "15.5.3",
|
"next": "15.5.3",
|
||||||
|
"next-intl": "^4.4.0",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
2025.10.31 添加国际化支持
|
||||||
2025.10.30 添加背单词功能
|
2025.10.30 添加背单词功能
|
||||||
2025.10.12 添加朗读器本地保存功能
|
2025.10.12 添加朗读器本地保存功能
|
||||||
2025.10.09 新增记忆字母表功能
|
2025.10.09 新增记忆字母表功能
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M480-80q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-155.5t86-127Q252-817 325-848.5T480-880q83 0 155.5 31.5t127 86q54.5 54.5 86 127T880-480q0 82-31.5 155t-86 127.5q-54.5 54.5-127 86T480-80Zm0-82q26-36 45-75t31-83H404q12 44 31 83t45 75Zm-104-16q-18-33-31.5-68.5T322-320H204q29 50 72.5 87t99.5 55Zm208 0q56-18 99.5-55t72.5-87H638q-9 38-22.5 73.5T584-178ZM170-400h136q-3-20-4.5-39.5T300-480q0-21 1.5-40.5T306-560H170q-5 20-7.5 39.5T160-480q0 21 2.5 40.5T170-400Zm216 0h188q3-20 4.5-39.5T580-480q0-21-1.5-40.5T574-560H386q-3 20-4.5 39.5T380-480q0 21 1.5 40.5T386-400Zm268 0h136q5-20 7.5-39.5T800-480q0-21-2.5-40.5T790-560H654q3 20 4.5 39.5T660-480q0 21-1.5 40.5T654-400Zm-16-240h118q-29-50-72.5-87T584-782q18 33 31.5 68.5T638-640Zm-234 0h152q-12-44-31-83t-45-75q-26 36-45 75t-31 83Zm-200 0h118q9-38 22.5-73.5T376-782q-56 18-99.5 55T204-640Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 976 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#FFFFFF"><path d="M480-80q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-155.5t86-127Q252-817 325-848.5T480-880q83 0 155.5 31.5t127 86q54.5 54.5 86 127T880-480q0 82-31.5 155t-86 127.5q-54.5 54.5-127 86T480-80Zm0-82q26-36 45-75t31-83H404q12 44 31 83t45 75Zm-104-16q-18-33-31.5-68.5T322-320H204q29 50 72.5 87t99.5 55Zm208 0q56-18 99.5-55t72.5-87H638q-9 38-22.5 73.5T584-178ZM170-400h136q-3-20-4.5-39.5T300-480q0-21 1.5-40.5T306-560H170q-5 20-7.5 39.5T160-480q0 21 2.5 40.5T170-400Zm216 0h188q3-20 4.5-39.5T580-480q0-21-1.5-40.5T574-560H386q-3 20-4.5 39.5T380-480q0 21 1.5 40.5T386-400Zm268 0h136q5-20 7.5-39.5T800-480q0-21-2.5-40.5T790-560H654q3 20 4.5 39.5T660-480q0 21-1.5 40.5T654-400Zm-16-240h118q-29-50-72.5-87T584-782q18 33 31.5 68.5T638-640Zm-234 0h152q-12-44-31-83t-45-75q-26 36-45 75t-31 83Zm-200 0h118q9-38 22.5-73.5T376-782q-56 18-99.5 55T204-640Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 976 B |
@@ -9,6 +9,7 @@ import {
|
|||||||
useEffect,
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
export default function MemoryCard({
|
export default function MemoryCard({
|
||||||
alphabet,
|
alphabet,
|
||||||
@@ -17,6 +18,7 @@ export default function MemoryCard({
|
|||||||
alphabet: Letter[];
|
alphabet: Letter[];
|
||||||
setChosenAlphabet: Dispatch<SetStateAction<SupportedAlphabets | null>>;
|
setChosenAlphabet: Dispatch<SetStateAction<SupportedAlphabets | null>>;
|
||||||
}) {
|
}) {
|
||||||
|
const t = useTranslations("alphabet");
|
||||||
const [index, setIndex] = useState(
|
const [index, setIndex] = useState(
|
||||||
Math.floor(Math.random() * alphabet.length),
|
Math.floor(Math.random() * alphabet.length),
|
||||||
);
|
);
|
||||||
@@ -79,7 +81,7 @@ export default function MemoryCard({
|
|||||||
setLetterDisplay(!letterDisplay);
|
setLetterDisplay(!letterDisplay);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{letterDisplay ? "隐藏字母" : "显示字母"}
|
{letterDisplay ? t("hideLetter") : t("showLetter")}
|
||||||
</LightButton>
|
</LightButton>
|
||||||
<LightButton
|
<LightButton
|
||||||
className="w-20"
|
className="w-20"
|
||||||
@@ -87,7 +89,7 @@ export default function MemoryCard({
|
|||||||
setIPADisplay(!ipaDisplay);
|
setIPADisplay(!ipaDisplay);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{ipaDisplay ? "隐藏IPA" : "显示IPA"}
|
{ipaDisplay ? t("hideIPA") : t("showIPA")}
|
||||||
</LightButton>
|
</LightButton>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ import { Letter, SupportedAlphabets } from "@/interfaces";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import MemoryCard from "./MemoryCard";
|
import MemoryCard from "./MemoryCard";
|
||||||
import { Navbar } from "@/components/Navbar";
|
import { Navbar } from "@/components/Navbar";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
export default function Alphabet() {
|
export default function Alphabet() {
|
||||||
|
const t = useTranslations("alphabet");
|
||||||
const [chosenAlphabet, setChosenAlphabet] =
|
const [chosenAlphabet, setChosenAlphabet] =
|
||||||
useState<SupportedAlphabets | null>(null);
|
useState<SupportedAlphabets | null>(null);
|
||||||
const [alphabetData, setAlphabetData] = useState<
|
const [alphabetData, setAlphabetData] = useState<
|
||||||
@@ -58,29 +60,29 @@ export default function Alphabet() {
|
|||||||
<>
|
<>
|
||||||
<Navbar></Navbar>
|
<Navbar></Navbar>
|
||||||
<div className="border border-gray-200 m-4 mt-4 flex flex-col justify-center items-center p-4 rounded-2xl gap-2">
|
<div className="border border-gray-200 m-4 mt-4 flex flex-col justify-center items-center p-4 rounded-2xl gap-2">
|
||||||
<span className="text-2xl md:text-3xl">请选择您想学习的字符</span>
|
<span className="text-2xl md:text-3xl">{t("chooseCharacters")}</span>
|
||||||
<div className="flex gap-1 flex-wrap">
|
<div className="flex gap-1 flex-wrap">
|
||||||
<LightButton onClick={() => setChosenAlphabet("japanese")}>
|
<LightButton onClick={() => setChosenAlphabet("japanese")}>
|
||||||
日语假名
|
{t("japanese")}
|
||||||
</LightButton>
|
</LightButton>
|
||||||
<LightButton onClick={() => setChosenAlphabet("english")}>
|
<LightButton onClick={() => setChosenAlphabet("english")}>
|
||||||
英文字母
|
{t("english")}
|
||||||
</LightButton>
|
</LightButton>
|
||||||
<LightButton onClick={() => setChosenAlphabet("uyghur")}>
|
<LightButton onClick={() => setChosenAlphabet("uyghur")}>
|
||||||
维吾尔字母
|
{t("uyghur")}
|
||||||
</LightButton>
|
</LightButton>
|
||||||
<LightButton onClick={() => setChosenAlphabet("esperanto")}>
|
<LightButton onClick={() => setChosenAlphabet("esperanto")}>
|
||||||
世界语字母
|
{t("esperanto")}
|
||||||
</LightButton>
|
</LightButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
if (loadingState === "loading") {
|
if (loadingState === "loading") {
|
||||||
return "加载中...";
|
return t("loading");
|
||||||
}
|
}
|
||||||
if (loadingState === "error") {
|
if (loadingState === "error") {
|
||||||
return "加载失败,请重试";
|
return t("loadFailed");
|
||||||
}
|
}
|
||||||
if (loadingState === "success" && alphabetData[chosenAlphabet]) {
|
if (loadingState === "success" && alphabetData[chosenAlphabet]) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
|
|||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import type { Viewport } from "next";
|
import type { Viewport } from "next";
|
||||||
|
import { NextIntlClientProvider } from "next-intl";
|
||||||
|
|
||||||
export const viewport: Viewport = {
|
export const viewport: Viewport = {
|
||||||
width: "device-width",
|
width: "device-width",
|
||||||
@@ -23,7 +24,7 @@ export const metadata: Metadata = {
|
|||||||
description: "A Website to Learn Languages",
|
description: "A Website to Learn Languages",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default async function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -33,7 +34,7 @@ export default function RootLayout({
|
|||||||
<body
|
<body
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
>
|
>
|
||||||
{children}
|
<NextIntlClientProvider>{children}</NextIntlClientProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { LOCALES } from "@/config/locales";
|
|||||||
import { Dispatch, SetStateAction, useState } from "react";
|
import { Dispatch, SetStateAction, useState } from "react";
|
||||||
import { WordData } from "@/interfaces";
|
import { WordData } from "@/interfaces";
|
||||||
import NavbarCenterWrapper from "@/components/NavbarCenterWrapper";
|
import NavbarCenterWrapper from "@/components/NavbarCenterWrapper";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
setEditPage: Dispatch<SetStateAction<"choose" | "edit">>;
|
setEditPage: Dispatch<SetStateAction<"choose" | "edit">>;
|
||||||
@@ -19,6 +20,7 @@ export default function Choose({
|
|||||||
setWordData,
|
setWordData,
|
||||||
localeKey,
|
localeKey,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const t = useTranslations("memorize.choose");
|
||||||
const [chosenLocale, setChosenLocale] = useState<
|
const [chosenLocale, setChosenLocale] = useState<
|
||||||
(typeof LOCALES)[number] | null
|
(typeof LOCALES)[number] | null
|
||||||
>(null);
|
>(null);
|
||||||
@@ -53,8 +55,10 @@ export default function Choose({
|
|||||||
</div>
|
</div>
|
||||||
<div className="w-full flex items-center justify-center">
|
<div className="w-full flex items-center justify-center">
|
||||||
<BCard className="flex gap-2 justify-center items-center w-fit">
|
<BCard className="flex gap-2 justify-center items-center w-fit">
|
||||||
<LightButton onClick={handleChooseClick}>Choose</LightButton>
|
<LightButton onClick={handleChooseClick}>{t("choose")}</LightButton>
|
||||||
<LightButton onClick={() => setEditPage("edit")}>Back</LightButton>
|
<LightButton onClick={() => setEditPage("edit")}>
|
||||||
|
{t("back")}
|
||||||
|
</LightButton>
|
||||||
</BCard>
|
</BCard>
|
||||||
</div>
|
</div>
|
||||||
</ACard>
|
</ACard>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import DarkButton from "@/components/buttons/DarkButton";
|
|||||||
import { WordData } from "@/interfaces";
|
import { WordData } from "@/interfaces";
|
||||||
import Choose from "./Choose";
|
import Choose from "./Choose";
|
||||||
import NavbarCenterWrapper from "@/components/NavbarCenterWrapper";
|
import NavbarCenterWrapper from "@/components/NavbarCenterWrapper";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
setPage: Dispatch<SetStateAction<"start" | "main" | "edit">>;
|
setPage: Dispatch<SetStateAction<"start" | "main" | "edit">>;
|
||||||
@@ -14,6 +15,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Edit({ setPage, wordData, setWordData }: Props) {
|
export default function Edit({ setPage, wordData, setWordData }: Props) {
|
||||||
|
const t = useTranslations("memorize.edit");
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const [localeKey, setLocaleKey] = useState<0 | 1>(0);
|
const [localeKey, setLocaleKey] = useState<0 | 1>(0);
|
||||||
const [editPage, setEditPage] = useState<"choose" | "edit">("edit");
|
const [editPage, setEditPage] = useState<"choose" | "edit">("edit");
|
||||||
@@ -65,15 +67,17 @@ export default function Edit({ setPage, wordData, setWordData }: Props) {
|
|||||||
></textarea>
|
></textarea>
|
||||||
<div className="w-full flex items-center justify-center">
|
<div className="w-full flex items-center justify-center">
|
||||||
<BCard className="flex gap-2 justify-center items-center w-fit">
|
<BCard className="flex gap-2 justify-center items-center w-fit">
|
||||||
<LightButton onClick={() => setPage("main")}>Back</LightButton>
|
<LightButton onClick={() => setPage("main")}>
|
||||||
<LightButton onClick={handleSave}>Save Pairs</LightButton>
|
{t("back")}
|
||||||
|
</LightButton>
|
||||||
|
<LightButton onClick={handleSave}>{t("save")}</LightButton>
|
||||||
<DarkButton
|
<DarkButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setLocaleKey(0);
|
setLocaleKey(0);
|
||||||
setEditPage("choose");
|
setEditPage("choose");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Locale 1
|
{t("locale1")}
|
||||||
</DarkButton>
|
</DarkButton>
|
||||||
<DarkButton
|
<DarkButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -81,7 +85,7 @@ export default function Edit({ setPage, wordData, setWordData }: Props) {
|
|||||||
setEditPage("choose");
|
setEditPage("choose");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Locale 2
|
{t("locale2")}
|
||||||
</DarkButton>
|
</DarkButton>
|
||||||
</BCard>
|
</BCard>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { WordData, WordDataSchema } from "@/interfaces";
|
|||||||
import { Dispatch, SetStateAction } from "react";
|
import { Dispatch, SetStateAction } from "react";
|
||||||
import useFileUpload from "@/hooks/useFileUpload";
|
import useFileUpload from "@/hooks/useFileUpload";
|
||||||
import NavbarCenterWrapper from "@/components/NavbarCenterWrapper";
|
import NavbarCenterWrapper from "@/components/NavbarCenterWrapper";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
wordData: WordData;
|
wordData: WordData;
|
||||||
@@ -17,6 +18,7 @@ export default function Main({
|
|||||||
setWordData,
|
setWordData,
|
||||||
setPage: setPage,
|
setPage: setPage,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const t = useTranslations("memorize.main");
|
||||||
const { upload, inputRef } = useFileUpload(async (file) => {
|
const { upload, inputRef } = useFileUpload(async (file) => {
|
||||||
try {
|
try {
|
||||||
const obj = JSON.parse(await file.text());
|
const obj = JSON.parse(await file.text());
|
||||||
@@ -44,21 +46,25 @@ export default function Main({
|
|||||||
<NavbarCenterWrapper className="bg-gray-100">
|
<NavbarCenterWrapper className="bg-gray-100">
|
||||||
<ACard className="flex-col flex">
|
<ACard className="flex-col flex">
|
||||||
<h1 className="text-center font-extrabold text-4xl text-gray-800 m-2 mb-4">
|
<h1 className="text-center font-extrabold text-4xl text-gray-800 m-2 mb-4">
|
||||||
Memorize
|
{t("title")}
|
||||||
</h1>
|
</h1>
|
||||||
<div className="flex-1 font-serif text-2xl w-full h-full text-gray-800">
|
<div className="flex-1 font-serif text-2xl w-full h-full text-gray-800">
|
||||||
<BCard>
|
<BCard>
|
||||||
<p>locale 1 {wordData.locales[0]}</p>
|
<p>{t("locale1", { locale: wordData.locales[0] })}</p>
|
||||||
<p>locale 2 {wordData.locales[1]}</p>
|
<p>{t("locale2", { locale: wordData.locales[1] })}</p>
|
||||||
<p>total {wordData.wordPairs.length} word pairs</p>
|
<p>{t("total", { total: wordData.wordPairs.length })}</p>
|
||||||
</BCard>
|
</BCard>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full flex items-center justify-center">
|
<div className="w-full flex items-center justify-center">
|
||||||
<BCard className="flex gap-2 justify-center items-center w-fit">
|
<BCard className="flex gap-2 justify-center items-center w-fit">
|
||||||
<LightButton onClick={() => setPage("start")}>开始</LightButton>
|
<LightButton onClick={() => setPage("start")}>
|
||||||
<LightButton onClick={handleLoad}>导入</LightButton>
|
{t("start")}
|
||||||
<LightButton onClick={handleSave}>保存</LightButton>
|
</LightButton>
|
||||||
<LightButton onClick={() => setPage("edit")}>编辑</LightButton>
|
<LightButton onClick={handleLoad}>{t("import")}</LightButton>
|
||||||
|
<LightButton onClick={handleSave}>{t("save")}</LightButton>
|
||||||
|
<LightButton onClick={() => setPage("edit")}>
|
||||||
|
{t("edit")}
|
||||||
|
</LightButton>
|
||||||
</BCard>
|
</BCard>
|
||||||
</div>
|
</div>
|
||||||
</ACard>
|
</ACard>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useAudioPlayer } from "@/hooks/useAudioPlayer";
|
|||||||
import { getTTSAudioUrl } from "@/utils";
|
import { getTTSAudioUrl } from "@/utils";
|
||||||
import { VOICES } from "@/config/locales";
|
import { VOICES } from "@/config/locales";
|
||||||
import NavbarCenterWrapper from "@/components/NavbarCenterWrapper";
|
import NavbarCenterWrapper from "@/components/NavbarCenterWrapper";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
interface WordBoardProps {
|
interface WordBoardProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -22,6 +23,7 @@ interface Props {
|
|||||||
setPage: Dispatch<SetStateAction<"start" | "main" | "edit">>;
|
setPage: Dispatch<SetStateAction<"start" | "main" | "edit">>;
|
||||||
}
|
}
|
||||||
export default function Start({ wordData, setPage }: Props) {
|
export default function Start({ wordData, setPage }: Props) {
|
||||||
|
const t = useTranslations("memorize.start");
|
||||||
const [display, setDisplay] = useState<"ask" | "show">("ask");
|
const [display, setDisplay] = useState<"ask" | "show">("ask");
|
||||||
const [wordPair, setWordPair] = useState(
|
const [wordPair, setWordPair] = useState(
|
||||||
wordData.wordPairs[Math.floor(Math.random() * wordData.wordPairs.length)],
|
wordData.wordPairs[Math.floor(Math.random() * wordData.wordPairs.length)],
|
||||||
@@ -71,23 +73,25 @@ export default function Start({ wordData, setPage }: Props) {
|
|||||||
<div className="w-full flex items-center justify-center">
|
<div className="w-full flex items-center justify-center">
|
||||||
<div className="flex gap-2 justify-center items-center w-fit font-mono flex-wrap">
|
<div className="flex gap-2 justify-center items-center w-fit font-mono flex-wrap">
|
||||||
{display === "ask" ? (
|
{display === "ask" ? (
|
||||||
<LightButton onClick={show}>Show</LightButton>
|
<LightButton onClick={show}>{t("show")}</LightButton>
|
||||||
) : (
|
) : (
|
||||||
<LightButton onClick={next}>Next</LightButton>
|
<LightButton onClick={next}>{t("next")}</LightButton>
|
||||||
)}
|
)}
|
||||||
<LightButton
|
<LightButton
|
||||||
onClick={() => setReverse(!reverse)}
|
onClick={() => setReverse(!reverse)}
|
||||||
selected={reverse}
|
selected={reverse}
|
||||||
>
|
>
|
||||||
Reverse
|
{t("reverse")}
|
||||||
</LightButton>
|
</LightButton>
|
||||||
<LightButton
|
<LightButton
|
||||||
onClick={() => setDictation(!dictation)}
|
onClick={() => setDictation(!dictation)}
|
||||||
selected={dictation}
|
selected={dictation}
|
||||||
>
|
>
|
||||||
Dictation
|
{t("dictation")}
|
||||||
|
</LightButton>
|
||||||
|
<LightButton onClick={() => setPage("main")}>
|
||||||
|
{t("back")}
|
||||||
</LightButton>
|
</LightButton>
|
||||||
<LightButton onClick={() => setPage("main")}>Exit</LightButton>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
198
src/app/page.tsx
198
src/app/page.tsx
@@ -1,108 +1,104 @@
|
|||||||
import { Navbar } from "@/components/Navbar";
|
import { Navbar } from "@/components/Navbar";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
function TopArea() {
|
|
||||||
return (
|
|
||||||
<div className="bg-[#35786f] text-white w-full min-h-[75dvh] flex justify-center items-center">
|
|
||||||
<div className="mb-16 mx-16 md:mx-0 md:max-w-[60dvw]">
|
|
||||||
<h1 className="text-6xl md:text-9xl mb-8">Learn Languages</h1>
|
|
||||||
<p className="text-2xl md:text-5xl">
|
|
||||||
Here is a very useful website to help you learn almost every language
|
|
||||||
in the world, including constructed ones.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LinkAreaProps {
|
|
||||||
href: string;
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
color: string;
|
|
||||||
}
|
|
||||||
function LinkArea({ href, name, description, color }: LinkAreaProps) {
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
href={href}
|
|
||||||
style={{ backgroundColor: color }}
|
|
||||||
className={`h-32 md:h-64 flex justify-center items-center`}
|
|
||||||
>
|
|
||||||
<div className="text-white m-8">
|
|
||||||
<h1 className="text-4xl">{name}</h1>
|
|
||||||
<p className="text-xl">{description}</p>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function LinkGrid() {
|
|
||||||
return (
|
|
||||||
<div className="w-full grid grid-cols-1 grid-rows-6 md:grid-cols-3">
|
|
||||||
<LinkArea
|
|
||||||
href="/translator"
|
|
||||||
name="翻译器"
|
|
||||||
description="翻译到任何语言,并标注国际音标(IPA)"
|
|
||||||
color="#a56068"
|
|
||||||
></LinkArea>
|
|
||||||
<LinkArea
|
|
||||||
href="/text-speaker"
|
|
||||||
name="朗读器"
|
|
||||||
description="识别并朗读文本,支持循环朗读、朗读速度调节"
|
|
||||||
color="#578aad"
|
|
||||||
></LinkArea>
|
|
||||||
{/* <LinkArea
|
|
||||||
href="/word-board"
|
|
||||||
name="词墙"
|
|
||||||
description="将单词固定到一片区域,高效便捷地记忆单词"
|
|
||||||
color="#e9b353"></LinkArea> */}
|
|
||||||
<LinkArea
|
|
||||||
href="/srt-player"
|
|
||||||
name="逐句视频播放器"
|
|
||||||
description="基于SRT字幕文件,逐句播放视频以模仿母语者的发音"
|
|
||||||
color="#3c988d"
|
|
||||||
></LinkArea>
|
|
||||||
<LinkArea
|
|
||||||
href="/alphabet"
|
|
||||||
name="背字母"
|
|
||||||
description="从字母表开始新语言的学习"
|
|
||||||
color="#dd7486"
|
|
||||||
></LinkArea>
|
|
||||||
<LinkArea
|
|
||||||
href="/memorize"
|
|
||||||
name="背单词"
|
|
||||||
description="语言A到语言B,语言B到语言A,支持听写"
|
|
||||||
color="#cc9988"
|
|
||||||
></LinkArea>
|
|
||||||
<LinkArea
|
|
||||||
href="#"
|
|
||||||
name="更多功能"
|
|
||||||
description="开发中,敬请期待"
|
|
||||||
color="#cab48a"
|
|
||||||
></LinkArea>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Fortune() {
|
|
||||||
return (
|
|
||||||
<div className="w-full flex justify-center font-serif items-center flex-col min-h-64 h-[25vdh]">
|
|
||||||
<p className="text-3xl">Stay hungry, stay foolish.</p>
|
|
||||||
<cite className="text-[#e9b353] text-xl">—— Steve Jobs</cite>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Explore() {
|
|
||||||
return (
|
|
||||||
<div className="bg-[#bbbbbb] w-full flex justify-center items-center flex-col h-52">
|
|
||||||
<span className="text-[100px] text-white">探索网站</span>
|
|
||||||
<div className="w-0 h-0 border-l-[40px] border-r-[40px] border-t-[30px] border-l-transparent border-r-transparent border-t-white"></div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
const t = useTranslations("home");
|
||||||
|
function TopArea() {
|
||||||
|
return (
|
||||||
|
<div className="bg-[#35786f] text-white w-full min-h-[75dvh] flex justify-center items-center">
|
||||||
|
<div className="mb-16 mx-16 md:mx-0 md:max-w-[60dvw]">
|
||||||
|
<h1 className="text-6xl md:text-9xl mb-8 font-extrabold">
|
||||||
|
{t("title")}
|
||||||
|
</h1>
|
||||||
|
<p className="text-2xl md:text-5xl font-medium">{t("description")}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
interface LinkAreaProps {
|
||||||
|
href: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
function LinkArea({ href, name, description, color }: LinkAreaProps) {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={href}
|
||||||
|
style={{ backgroundColor: color }}
|
||||||
|
className={`h-32 md:h-64 flex justify-center items-center`}
|
||||||
|
>
|
||||||
|
<div className="text-white m-8">
|
||||||
|
<h1 className="text-4xl">{name}</h1>
|
||||||
|
<p className="text-xl">{description}</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function LinkGrid() {
|
||||||
|
return (
|
||||||
|
<div className="w-full grid grid-cols-1 grid-rows-6 md:grid-cols-3">
|
||||||
|
<LinkArea
|
||||||
|
href="/translator"
|
||||||
|
name={t("translator.name")}
|
||||||
|
description={t("translator.description")}
|
||||||
|
color="#a56068"
|
||||||
|
></LinkArea>
|
||||||
|
<LinkArea
|
||||||
|
href="/text-speaker"
|
||||||
|
name={t("textSpeaker.name")}
|
||||||
|
description={t("textSpeaker.description")}
|
||||||
|
color="#578aad"
|
||||||
|
></LinkArea>
|
||||||
|
{/* <LinkArea
|
||||||
|
href="/word-board"
|
||||||
|
name="词墙"
|
||||||
|
description="将单词固定到一片区域,高效便捷地记忆单词"
|
||||||
|
color="#e9b353"></LinkArea> */}
|
||||||
|
<LinkArea
|
||||||
|
href="/srt-player"
|
||||||
|
name={t("srtPlayer.name")}
|
||||||
|
description={t("srtPlayer.description")}
|
||||||
|
color="#3c988d"
|
||||||
|
></LinkArea>
|
||||||
|
<LinkArea
|
||||||
|
href="/alphabet"
|
||||||
|
name={t("alphabet.name")}
|
||||||
|
description={t("alphabet.description")}
|
||||||
|
color="#dd7486"
|
||||||
|
></LinkArea>
|
||||||
|
<LinkArea
|
||||||
|
href="/memorize"
|
||||||
|
name={t("memorize.name")}
|
||||||
|
description={t("memorize.description")}
|
||||||
|
color="#cc9988"
|
||||||
|
></LinkArea>
|
||||||
|
<LinkArea
|
||||||
|
href="#"
|
||||||
|
name={t("moreFeatures.name")}
|
||||||
|
description={t("moreFeatures.description")}
|
||||||
|
color="#cab48a"
|
||||||
|
></LinkArea>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function Fortune() {
|
||||||
|
return (
|
||||||
|
<div className="w-full flex justify-center font-serif items-center flex-col min-h-64 h-[25vdh]">
|
||||||
|
<p className="text-3xl">{t("fortune.quote")}</p>
|
||||||
|
<cite className="text-[#e9b353] text-xl">{t("fortune.author")}</cite>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function Explore() {
|
||||||
|
return (
|
||||||
|
<div className="bg-[#bbbbbb] w-full flex justify-center items-center flex-col h-52">
|
||||||
|
<span className="text-[100px] text-white">{t("explore")}</span>
|
||||||
|
<div className="w-0 h-0 border-l-40 border-r-40 border-t-30 border-l-transparent border-r-transparent border-t-white"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Navbar></Navbar>
|
<Navbar></Navbar>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import LightButton from "@/components/buttons/LightButton";
|
import LightButton from "@/components/buttons/LightButton";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
export default function UploadArea({
|
export default function UploadArea({
|
||||||
setVideoUrl,
|
setVideoUrl,
|
||||||
@@ -8,6 +9,7 @@ export default function UploadArea({
|
|||||||
setVideoUrl: (url: string | null) => void;
|
setVideoUrl: (url: string | null) => void;
|
||||||
setSrtUrl: (url: string | null) => void;
|
setSrtUrl: (url: string | null) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const t = useTranslations("srt-player");
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const uploadVideo = () => {
|
const uploadVideo = () => {
|
||||||
@@ -38,8 +40,8 @@ export default function UploadArea({
|
|||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex flex-col gap-2 m-2">
|
<div className="w-full flex flex-col gap-2 m-2">
|
||||||
<LightButton onClick={uploadVideo}>上传视频</LightButton>
|
<LightButton onClick={uploadVideo}>{t("uploadVideo")}</LightButton>
|
||||||
<LightButton onClick={uploadSRT}>上传字幕</LightButton>
|
<LightButton onClick={uploadSRT}>{t("uploadSubtitle")}</LightButton>
|
||||||
<input type="file" className="hidden" ref={inputRef} />
|
<input type="file" className="hidden" ref={inputRef} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useState, useRef, forwardRef, useEffect, useCallback } from "react";
|
|||||||
import SubtitleDisplay from "./SubtitleDisplay";
|
import SubtitleDisplay from "./SubtitleDisplay";
|
||||||
import LightButton from "@/components/buttons/LightButton";
|
import LightButton from "@/components/buttons/LightButton";
|
||||||
import { getIndex, parseSrt, getNearistIndex } from "../subtitle";
|
import { getIndex, parseSrt, getNearistIndex } from "../subtitle";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
type VideoPanelProps = {
|
type VideoPanelProps = {
|
||||||
videoUrl: string | null;
|
videoUrl: string | null;
|
||||||
@@ -10,6 +11,7 @@ type VideoPanelProps = {
|
|||||||
|
|
||||||
const VideoPanel = forwardRef<HTMLVideoElement, VideoPanelProps>(
|
const VideoPanel = forwardRef<HTMLVideoElement, VideoPanelProps>(
|
||||||
({ videoUrl, srtUrl }, videoRef) => {
|
({ videoUrl, srtUrl }, videoRef) => {
|
||||||
|
const t = useTranslations("srt-player");
|
||||||
videoRef = videoRef as React.RefObject<HTMLVideoElement>;
|
videoRef = videoRef as React.RefObject<HTMLVideoElement>;
|
||||||
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
||||||
const [srtLength, setSrtLength] = useState<number>(0);
|
const [srtLength, setSrtLength] = useState<number>(0);
|
||||||
@@ -185,14 +187,14 @@ const VideoPanel = forwardRef<HTMLVideoElement, VideoPanelProps>(
|
|||||||
<SubtitleDisplay subtitle={subtitle}></SubtitleDisplay>
|
<SubtitleDisplay subtitle={subtitle}></SubtitleDisplay>
|
||||||
<div className="buttons flex mt-2 gap-2 flex-wrap">
|
<div className="buttons flex mt-2 gap-2 flex-wrap">
|
||||||
<LightButton onClick={togglePlayPause}>
|
<LightButton onClick={togglePlayPause}>
|
||||||
{isPlaying ? "暂停" : "播放"}
|
{isPlaying ? t("pause") : t("play")}
|
||||||
|
</LightButton>
|
||||||
|
<LightButton onClick={previous}>{t("previous")}</LightButton>
|
||||||
|
<LightButton onClick={next}>{t("next")}</LightButton>
|
||||||
|
<LightButton onClick={restart}>{t("restart")}</LightButton>
|
||||||
|
<LightButton onClick={handleAutoPauseToggle}>
|
||||||
|
{t("autoPause", { enabled: autoPause ? "Yes" : "No" })}
|
||||||
</LightButton>
|
</LightButton>
|
||||||
<LightButton onClick={previous}>上句</LightButton>
|
|
||||||
<LightButton onClick={next}>下句</LightButton>
|
|
||||||
<LightButton onClick={restart}>句首</LightButton>
|
|
||||||
<LightButton
|
|
||||||
onClick={handleAutoPauseToggle}
|
|
||||||
>{`自动暂停(${autoPause ? "是" : "否"})`}</LightButton>
|
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
className="seekbar"
|
className="seekbar"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import z from "zod";
|
|||||||
import { TextSpeakerItemSchema } from "@/interfaces";
|
import { TextSpeakerItemSchema } from "@/interfaces";
|
||||||
import IconClick from "@/components/IconClick";
|
import IconClick from "@/components/IconClick";
|
||||||
import IMAGES from "@/config/images";
|
import IMAGES from "@/config/images";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
interface TextCardProps {
|
interface TextCardProps {
|
||||||
item: z.infer<typeof TextSpeakerItemSchema>;
|
item: z.infer<typeof TextSpeakerItemSchema>;
|
||||||
@@ -47,6 +48,7 @@ interface SaveListProps {
|
|||||||
handleUse: (item: z.infer<typeof TextSpeakerItemSchema>) => void;
|
handleUse: (item: z.infer<typeof TextSpeakerItemSchema>) => void;
|
||||||
}
|
}
|
||||||
export default function SaveList({ show = false, handleUse }: SaveListProps) {
|
export default function SaveList({ show = false, handleUse }: SaveListProps) {
|
||||||
|
const t = useTranslations("text-speaker");
|
||||||
const [data, setData] = useState(getTextSpeakerData());
|
const [data, setData] = useState(getTextSpeakerData());
|
||||||
const handleDel = (item: z.infer<typeof TextSpeakerItemSchema>) => {
|
const handleDel = (item: z.infer<typeof TextSpeakerItemSchema>) => {
|
||||||
const current_data = getTextSpeakerData();
|
const current_data = getTextSpeakerData();
|
||||||
@@ -61,7 +63,7 @@ export default function SaveList({ show = false, handleUse }: SaveListProps) {
|
|||||||
setData(getTextSpeakerData());
|
setData(getTextSpeakerData());
|
||||||
};
|
};
|
||||||
const handleDeleteAll = () => {
|
const handleDeleteAll = () => {
|
||||||
const yesorno = prompt("确定删光吗?(Y/N)")?.trim();
|
const yesorno = prompt(t("confirmDeleteAll"))?.trim();
|
||||||
if (yesorno && (yesorno === "Y" || yesorno === "y")) {
|
if (yesorno && (yesorno === "Y" || yesorno === "y")) {
|
||||||
setTextSpeakerData([]);
|
setTextSpeakerData([]);
|
||||||
refresh();
|
refresh();
|
||||||
|
|||||||
@@ -15,8 +15,10 @@ import { TextSpeakerItemSchema } from "@/interfaces";
|
|||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { Navbar } from "@/components/Navbar";
|
import { Navbar } from "@/components/Navbar";
|
||||||
import { VOICES } from "@/config/locales";
|
import { VOICES } from "@/config/locales";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
export default function TextSpeaker() {
|
export default function TextSpeaker() {
|
||||||
|
const t = useTranslations("text-speaker");
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const [showSpeedAdjust, setShowSpeedAdjust] = useState(false);
|
const [showSpeedAdjust, setShowSpeedAdjust] = useState(false);
|
||||||
const [showSaveList, setShowSaveList] = useState(false);
|
const [showSaveList, setShowSaveList] = useState(false);
|
||||||
@@ -320,7 +322,7 @@ export default function TextSpeaker() {
|
|||||||
selected={ipaEnabled}
|
selected={ipaEnabled}
|
||||||
onClick={() => setIPAEnabled(!ipaEnabled)}
|
onClick={() => setIPAEnabled(!ipaEnabled)}
|
||||||
>
|
>
|
||||||
生成IPA
|
{t("generateIPA")}
|
||||||
</LightButton>
|
</LightButton>
|
||||||
<LightButton
|
<LightButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -328,7 +330,7 @@ export default function TextSpeaker() {
|
|||||||
}}
|
}}
|
||||||
selected={showSaveList}
|
selected={showSaveList}
|
||||||
>
|
>
|
||||||
查看保存项
|
{t("viewSavedItems")}
|
||||||
</LightButton>
|
</LightButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ import IMAGES from "@/config/images";
|
|||||||
import { getTTSAudioUrl } from "@/utils";
|
import { getTTSAudioUrl } from "@/utils";
|
||||||
import { Navbar } from "@/components/Navbar";
|
import { Navbar } from "@/components/Navbar";
|
||||||
import { VOICES } from "@/config/locales";
|
import { VOICES } from "@/config/locales";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
export default function Translator() {
|
export default function Translator() {
|
||||||
|
const t = useTranslations("translator");
|
||||||
const [ipaEnabled, setIPAEnabled] = useState(true);
|
const [ipaEnabled, setIPAEnabled] = useState(true);
|
||||||
const [targetLang, setTargetLang] = useState("Chinese");
|
const [targetLang, setTargetLang] = useState("Chinese");
|
||||||
|
|
||||||
@@ -25,7 +27,7 @@ export default function Translator() {
|
|||||||
const tl = ["Chinese", "English", "Italian"];
|
const tl = ["Chinese", "English", "Italian"];
|
||||||
|
|
||||||
const inputLanguage = () => {
|
const inputLanguage = () => {
|
||||||
const lang = prompt("Input a language.")?.trim();
|
const lang = prompt(t("inputLanguage"))?.trim();
|
||||||
if (lang) {
|
if (lang) {
|
||||||
setTargetLang(lang);
|
setTargetLang(lang);
|
||||||
}
|
}
|
||||||
@@ -210,12 +212,12 @@ export default function Translator() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="option1 w-full flex flex-row justify-between items-center">
|
<div className="option1 w-full flex flex-row justify-between items-center">
|
||||||
<span>detect language</span>
|
<span>{t("detectLanguage")}</span>
|
||||||
<LightButton
|
<LightButton
|
||||||
selected={ipaEnabled}
|
selected={ipaEnabled}
|
||||||
onClick={() => setIPAEnabled(!ipaEnabled)}
|
onClick={() => setIPAEnabled(!ipaEnabled)}
|
||||||
>
|
>
|
||||||
generate ipa
|
{t("generateIPA")}
|
||||||
</LightButton>
|
</LightButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -242,14 +244,14 @@ export default function Translator() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="option2 w-full flex gap-1 items-center flex-wrap">
|
<div className="option2 w-full flex gap-1 items-center flex-wrap">
|
||||||
<span>translate into</span>
|
<span>{t("translateInto")}</span>
|
||||||
<LightButton
|
<LightButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setTargetLang("Chinese");
|
setTargetLang("Chinese");
|
||||||
}}
|
}}
|
||||||
selected={targetLang === "Chinese"}
|
selected={targetLang === "Chinese"}
|
||||||
>
|
>
|
||||||
Chinese
|
{t("chinese")}
|
||||||
</LightButton>
|
</LightButton>
|
||||||
<LightButton
|
<LightButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -257,7 +259,7 @@ export default function Translator() {
|
|||||||
}}
|
}}
|
||||||
selected={targetLang === "English"}
|
selected={targetLang === "English"}
|
||||||
>
|
>
|
||||||
English
|
{t("english")}
|
||||||
</LightButton>
|
</LightButton>
|
||||||
<LightButton
|
<LightButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -265,13 +267,13 @@ export default function Translator() {
|
|||||||
}}
|
}}
|
||||||
selected={targetLang === "Italian"}
|
selected={targetLang === "Italian"}
|
||||||
>
|
>
|
||||||
Italian
|
{t("italian")}
|
||||||
</LightButton>
|
</LightButton>
|
||||||
<LightButton
|
<LightButton
|
||||||
onClick={inputLanguage}
|
onClick={inputLanguage}
|
||||||
selected={!tl.includes(targetLang)}
|
selected={!tl.includes(targetLang)}
|
||||||
>
|
>
|
||||||
{"Other" + (tl.includes(targetLang) ? "" : ": " + targetLang)}
|
{t("other") + (tl.includes(targetLang) ? "" : ": " + targetLang)}
|
||||||
</LightButton>
|
</LightButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -282,7 +284,7 @@ export default function Translator() {
|
|||||||
onClick={translate}
|
onClick={translate}
|
||||||
className={`duration-150 ease-in text-xl font-extrabold border rounded-4xl p-3 border-gray-200 h-16 ${translating ? "bg-gray-200" : "bg-white hover:bg-gray-200 hover:cursor-pointer"}`}
|
className={`duration-150 ease-in text-xl font-extrabold border rounded-4xl p-3 border-gray-200 h-16 ${translating ? "bg-gray-200" : "bg-white hover:bg-gray-200 hover:cursor-pointer"}`}
|
||||||
>
|
>
|
||||||
{translating ? "translating..." : "translate"}
|
{translating ? t("translating") : t("translate")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ interface IconClickProps {
|
|||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
size?: number;
|
size?: number;
|
||||||
|
disableOnHoverBgChange?: boolean;
|
||||||
}
|
}
|
||||||
export default function IconClick({
|
export default function IconClick({
|
||||||
src,
|
src,
|
||||||
@@ -13,12 +14,13 @@ export default function IconClick({
|
|||||||
onClick = () => {},
|
onClick = () => {},
|
||||||
className = "",
|
className = "",
|
||||||
size = 32,
|
size = 32,
|
||||||
|
disableOnHoverBgChange = false,
|
||||||
}: IconClickProps) {
|
}: IconClickProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={`hover:cursor-pointer hover:bg-gray-200 rounded-3xl w-[${size}px] h-[${size}px] flex justify-center items-center ${className}`}
|
className={`${disableOnHoverBgChange ? "" : "hover:bg-gray-200"}hover:cursor-pointer rounded-3xl w-[${size}px] h-[${size}px] flex justify-center items-center ${className}`}
|
||||||
>
|
>
|
||||||
<Image src={src} width={size - 5} height={size - 5} alt={alt}></Image>
|
<Image src={src} width={size - 5} height={size - 5} alt={alt}></Image>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import IconClick from "./IconClick";
|
||||||
|
import IMAGES from "@/config/images";
|
||||||
|
import { useState } from "react";
|
||||||
|
import LightButton from "./buttons/LightButton";
|
||||||
|
|
||||||
function MyLink({ href, label }: { href: string; label: string }) {
|
function MyLink({ href, label }: { href: string; label: string }) {
|
||||||
return (
|
return (
|
||||||
@@ -9,6 +16,15 @@ function MyLink({ href, label }: { href: string; label: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
export function Navbar() {
|
export function Navbar() {
|
||||||
|
const t = useTranslations("navbar");
|
||||||
|
const [showLanguageMenu, setShowLanguageMenu] = useState(false);
|
||||||
|
const handleLanguageClick = () => {
|
||||||
|
setShowLanguageMenu((prev) => !prev);
|
||||||
|
};
|
||||||
|
const setLocale = async (locale: string) => {
|
||||||
|
document.cookie = `locale=${locale}`;
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-between items-center w-full h-16 px-8 bg-[#35786f] text-white">
|
<div className="flex justify-between items-center w-full h-16 px-8 bg-[#35786f] text-white">
|
||||||
<Link href={"/"} className="text-xl flex">
|
<Link href={"/"} className="text-xl flex">
|
||||||
@@ -19,13 +35,39 @@ export function Navbar() {
|
|||||||
height="32"
|
height="32"
|
||||||
className="rounded-4xl"
|
className="rounded-4xl"
|
||||||
></Image>
|
></Image>
|
||||||
<span className="font-bold">学语言</span>
|
<span className="font-bold">{t("title")}</span>
|
||||||
</Link>
|
</Link>
|
||||||
<div className="flex gap-4 text-xl">
|
<div className="flex gap-4 text-xl">
|
||||||
<MyLink href="/changelog.txt" label="关于"></MyLink>
|
<div className="relative">
|
||||||
|
{showLanguageMenu && (
|
||||||
|
<div>
|
||||||
|
<div className="absolute top-10 right-0 rounded-md shadow-md flex flex-col gap-2">
|
||||||
|
<LightButton
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => setLocale("en-US")}
|
||||||
|
>
|
||||||
|
English
|
||||||
|
</LightButton>
|
||||||
|
<LightButton
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => setLocale("zh-CN")}
|
||||||
|
>
|
||||||
|
中文
|
||||||
|
</LightButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<IconClick
|
||||||
|
src={IMAGES.language_white}
|
||||||
|
alt="language"
|
||||||
|
disableOnHoverBgChange={true}
|
||||||
|
onClick={handleLanguageClick}
|
||||||
|
></IconClick>
|
||||||
|
</div>
|
||||||
|
<MyLink href="/changelog.txt" label={t("about")}></MyLink>
|
||||||
<MyLink
|
<MyLink
|
||||||
href="https://github.com/GoddoNebianU/learn-languages"
|
href="https://github.com/GoddoNebianU/learn-languages"
|
||||||
label="源码"
|
label={t("sourceCode")}
|
||||||
></MyLink>
|
></MyLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
2
src/config/i18n.ts
Normal file
2
src/config/i18n.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export const SUPPORTED_LOCALES = ["en-US", "zh-CN"];
|
||||||
|
export const DEFAULT_LOCALE = "en-US";
|
||||||
@@ -15,6 +15,8 @@ const IMAGES = {
|
|||||||
save: "/images/save_24dp_1F1F1F_FILL0_wght400_GRAD0_opsz24.svg",
|
save: "/images/save_24dp_1F1F1F_FILL0_wght400_GRAD0_opsz24.svg",
|
||||||
delete: "/images/delete_24dp_1F1F1F_FILL0_wght400_GRAD0_opsz24.svg",
|
delete: "/images/delete_24dp_1F1F1F_FILL0_wght400_GRAD0_opsz24.svg",
|
||||||
speed: "/images/speed_24dp_1F1F1F_FILL0_wght400_GRAD0_opsz24.svg",
|
speed: "/images/speed_24dp_1F1F1F_FILL0_wght400_GRAD0_opsz24.svg",
|
||||||
|
language_black: "/images/language_24dp_1F1F1F_FILL0_wght400_GRAD0_opsz24.svg",
|
||||||
|
language_white: "/images/language_24dp_FFFFFF_FILL0_wght400_GRAD0_opsz24.svg",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default IMAGES;
|
export default IMAGES;
|
||||||
|
|||||||
53
src/i18n/request.ts
Normal file
53
src/i18n/request.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { DEFAULT_LOCALE, SUPPORTED_LOCALES } from "@/config/i18n";
|
||||||
|
import { getRequestConfig } from "next-intl/server";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
import { readFileSync, readdirSync, statSync } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
function loadMessagesFromDir(dirPath: string): Record<string, any> {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const messages: Record<string, any> = {};
|
||||||
|
try {
|
||||||
|
const items = readdirSync(dirPath);
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const fullPath = join(dirPath, item);
|
||||||
|
const stat = statSync(fullPath);
|
||||||
|
|
||||||
|
if (stat.isDirectory()) {
|
||||||
|
const dirMessages = loadMessagesFromDir(fullPath);
|
||||||
|
Object.assign(messages, { [item]: dirMessages });
|
||||||
|
} else if (item.endsWith(".json")) {
|
||||||
|
try {
|
||||||
|
const content = readFileSync(fullPath, "utf-8");
|
||||||
|
const jsonContent = JSON.parse(content);
|
||||||
|
Object.assign(messages, { [item.replace(".json", "")]: jsonContent });
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to load JSON file ${fullPath}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to read directory ${dirPath}:`, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getRequestConfig(async () => {
|
||||||
|
const store = await cookies();
|
||||||
|
const locale = (() => {
|
||||||
|
const locale = store.get("locale")?.value ?? DEFAULT_LOCALE;
|
||||||
|
if (!SUPPORTED_LOCALES.includes(locale)) return DEFAULT_LOCALE;
|
||||||
|
return locale;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const messagesPath = join(process.cwd(), "messages", locale);
|
||||||
|
const messages = loadMessagesFromDir(messagesPath);
|
||||||
|
|
||||||
|
return {
|
||||||
|
locale,
|
||||||
|
messages,
|
||||||
|
};
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user