Compare commits
13 Commits
6ba5ae993a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b9fba254d | |||
| 0cb240791b | |||
| d9fd09c13d | |||
| 5406543cbe | |||
| d2a3d32376 | |||
| 436d58be52 | |||
| 11a265d52e | |||
| fb4346377a | |||
| c83aefabfa | |||
| 020744b353 | |||
| 719aef5a7f | |||
| 6c811a77db | |||
| 3652e350e6 |
@@ -2,6 +2,8 @@
|
|||||||
kind: pipeline
|
kind: pipeline
|
||||||
type: docker
|
type: docker
|
||||||
name: learn-languages
|
name: learn-languages
|
||||||
|
concurrency:
|
||||||
|
limit: 1
|
||||||
|
|
||||||
platform:
|
platform:
|
||||||
os: linux
|
os: linux
|
||||||
|
|||||||
@@ -13,3 +13,11 @@ DATABASE_URL=
|
|||||||
|
|
||||||
// DashScore
|
// DashScore
|
||||||
DASHSCORE_API_KEY=
|
DASHSCORE_API_KEY=
|
||||||
|
|
||||||
|
// SMTP Email - Resend (https://resend.com)
|
||||||
|
SMTP_HOST=smtp.resend.com
|
||||||
|
SMTP_PORT=587
|
||||||
|
SMTP_SECURE=false
|
||||||
|
SMTP_USER=resend
|
||||||
|
SMTP_PASS=re_your_resend_api_key
|
||||||
|
SMTP_FROM=onboarding@resend.dev
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# LEARN-LANGUAGES 知识库
|
# LEARN-LANGUAGES 知识库
|
||||||
|
|
||||||
**生成时间:** 2026-03-08
|
**生成时间:** 2026-03-08
|
||||||
**提交:** 91c59c3
|
**提交:** 6ba5ae9
|
||||||
**分支:** dev
|
**分支:** dev
|
||||||
|
|
||||||
## 概述
|
## 概述
|
||||||
@@ -112,6 +112,7 @@ log.error("Failed to fetch folders", { error });
|
|||||||
- ❌ Server Component 可行时用 Client Component
|
- ❌ Server Component 可行时用 Client Component
|
||||||
- ❌ npm 或 yarn (使用 pnpm)
|
- ❌ npm 或 yarn (使用 pnpm)
|
||||||
- ❌ 生产代码中使用 `console.log` (使用 winston logger)
|
- ❌ 生产代码中使用 `console.log` (使用 winston logger)
|
||||||
|
- ❌ 擅自运行 `pnpm dev` (不需要,用 `pnpm build` 验证即可)
|
||||||
|
|
||||||
## 独特风格
|
## 独特风格
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
|
|
||||||
### 前置要求
|
### 前置要求
|
||||||
|
|
||||||
- Node.js 23+
|
- Node.js 24+
|
||||||
- PostgreSQL 14+
|
- PostgreSQL 14+
|
||||||
- pnpm 8+ (推荐) 或 npm/yarn
|
- pnpm 8+ (推荐) 或 npm/yarn
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +1,57 @@
|
|||||||
{
|
{
|
||||||
"alphabet": {
|
"alphabet": {
|
||||||
"chooseCharacters": "Bitte wählen Sie die Zeichen aus, die Sie lernen möchten",
|
"chooseCharacters": "Bitte wählen Sie die Zeichen aus, die Sie lernen möchten",
|
||||||
|
"chooseAlphabetHint": "Wählen Sie ein Alphabet, um mit dem Lernen zu beginnen",
|
||||||
"japanese": "Japanische Kana",
|
"japanese": "Japanische Kana",
|
||||||
"english": "Englisches Alphabet",
|
"english": "Englisches Alphabet",
|
||||||
"uyghur": "Uigurisches Alphabet",
|
"uyghur": "Uigurisches Alphabet",
|
||||||
"esperanto": "Esperanto-Alphabet",
|
"esperanto": "Esperanto-Alphabet",
|
||||||
"loading": "Laden...",
|
"loading": "Wird geladen...",
|
||||||
"loadFailed": "Laden fehlgeschlagen, bitte versuchen Sie es erneut",
|
"loadFailed": "Laden fehlgeschlagen, bitte versuchen Sie es erneut",
|
||||||
"hideLetter": "Zeichen ausblenden",
|
"hideLetter": "Buchstabe ausblenden",
|
||||||
"showLetter": "Zeichen anzeigen",
|
"showLetter": "Buchstabe anzeigen",
|
||||||
"hideIPA": "IPA ausblenden",
|
"hideIPA": "IPA ausblenden",
|
||||||
"showIPA": "IPA anzeigen",
|
"showIPA": "IPA anzeigen",
|
||||||
"roman": "Romanisierung",
|
"roman": "Romanisierung",
|
||||||
"letter": "Zeichen",
|
"letter": "Buchstabe",
|
||||||
"random": "Zufälliger Modus",
|
"random": "Zufallsmodus",
|
||||||
"randomNext": "Zufällig weiter"
|
"randomNext": "Zufällig weiter",
|
||||||
|
"previousLetter": "Vorheriger Buchstabe",
|
||||||
|
"nextLetter": "Nächster Buchstabe",
|
||||||
|
"keyboardHint": "Verwenden Sie die Pfeiltasten links/rechts oder Leertaste für Zufall, ESC zum Zurückgehen",
|
||||||
|
"swipeHint": "Verwenden Sie die Pfeiltasten links/rechts oder wischen Sie zum Navigieren, ESC zum Zurückgehen"
|
||||||
},
|
},
|
||||||
"folders": {
|
"folders": {
|
||||||
"title": "Ordner",
|
"title": "Ordner",
|
||||||
"subtitle": "Verwalten Sie Ihre Sammlungen",
|
"subtitle": "Verwalten Sie Ihre Sammlungen",
|
||||||
"newFolder": "Neuer Ordner",
|
"newFolder": "Neuer Ordner",
|
||||||
"creating": "Erstellen...",
|
"creating": "Wird erstellt...",
|
||||||
"noFoldersYet": "Noch keine Ordner",
|
"noFoldersYet": "Noch keine Ordner vorhanden",
|
||||||
"folderInfo": "ID: {id} • {totalPairs} Paare",
|
"folderInfo": "ID: {id} • {totalPairs} Paare",
|
||||||
"enterFolderName": "Ordnernamen eingeben:",
|
"enterFolderName": "Ordnernamen eingeben:",
|
||||||
"confirmDelete": "Geben Sie \"{name}\" ein, um zu löschen:"
|
"confirmDelete": "Geben Sie \"{name}\" zum Löschen ein:",
|
||||||
|
"myFolders": "Meine Ordner",
|
||||||
|
"publicFolders": "Öffentliche Ordner",
|
||||||
|
"public": "Öffentlich",
|
||||||
|
"private": "Privat",
|
||||||
|
"setPublic": "Öffentlich machen",
|
||||||
|
"setPrivate": "Privat machen",
|
||||||
|
"publicFolderInfo": "{userName} • {totalPairs} Paare",
|
||||||
|
"searchPlaceholder": "Öffentliche Ordner durchsuchen...",
|
||||||
|
"loading": "Wird geladen...",
|
||||||
|
"noPublicFolders": "Keine öffentlichen Ordner gefunden",
|
||||||
|
"unknownUser": "Unbekannter Benutzer",
|
||||||
|
"enterNewName": "Neuen Namen eingeben:",
|
||||||
|
"favorite": "Favorisieren",
|
||||||
|
"unfavorite": "Aus Favoriten entfernen",
|
||||||
|
"pleaseLogin": "Bitte melden Sie sich zuerst an"
|
||||||
},
|
},
|
||||||
"folder_id": {
|
"folder_id": {
|
||||||
"unauthorized": "Sie sind nicht der Eigentümer dieses Ordners",
|
"unauthorized": "Sie sind nicht der Besitzer dieses Ordners",
|
||||||
"back": "Zurück",
|
"back": "Zurück",
|
||||||
"textPairs": "Textpaare",
|
"textPairs": "Textpaare",
|
||||||
"itemsCount": "{count} Elemente",
|
"itemsCount": "{count} Einträge",
|
||||||
"memorize": "Einprägen",
|
"memorize": "Auswendig lernen",
|
||||||
"loadingTextPairs": "Textpaare werden geladen...",
|
"loadingTextPairs": "Textpaare werden geladen...",
|
||||||
"noTextPairs": "Keine Textpaare in diesem Ordner",
|
"noTextPairs": "Keine Textpaare in diesem Ordner",
|
||||||
"addNewTextPair": "Neues Textpaar hinzufügen",
|
"addNewTextPair": "Neues Textpaar hinzufügen",
|
||||||
@@ -42,14 +62,14 @@
|
|||||||
"text2": "Text 2",
|
"text2": "Text 2",
|
||||||
"language1": "Sprache 1",
|
"language1": "Sprache 1",
|
||||||
"language2": "Sprache 2",
|
"language2": "Sprache 2",
|
||||||
"enterLanguageName": "Bitte geben Sie den Sprachennamen ein",
|
"enterLanguageName": "Bitte Sprachnamen eingeben",
|
||||||
"edit": "Bearbeiten",
|
"edit": "Bearbeiten",
|
||||||
"delete": "Löschen",
|
"delete": "Löschen",
|
||||||
"permissionDenied": "Sie haben keine Berechtigung, diese Aktion auszuführen",
|
"permissionDenied": "Sie haben keine Berechtigung für diese Aktion",
|
||||||
"error": {
|
"error": {
|
||||||
"update": "Sie haben keine Berechtigung, dieses Element zu aktualisieren.",
|
"update": "Sie haben keine Berechtigung, diesen Eintrag zu aktualisieren.",
|
||||||
"delete": "Sie haben keine Berechtigung, dieses Element zu löschen.",
|
"delete": "Sie haben keine Berechtigung, diesen Eintrag zu löschen.",
|
||||||
"add": "Sie haben keine Berechtigung, Elemente zu diesem Ordner hinzuzufügen.",
|
"add": "Sie haben keine Berechtigung, Einträge zu diesem Ordner hinzuzufügen.",
|
||||||
"rename": "Sie haben keine Berechtigung, diesen Ordner umzubenennen.",
|
"rename": "Sie haben keine Berechtigung, diesen Ordner umzubenennen.",
|
||||||
"deleteFolder": "Sie haben keine Berechtigung, diesen Ordner zu löschen."
|
"deleteFolder": "Sie haben keine Berechtigung, diesen Ordner zu löschen."
|
||||||
}
|
}
|
||||||
@@ -57,42 +77,43 @@
|
|||||||
"home": {
|
"home": {
|
||||||
"title": "Sprachen lernen",
|
"title": "Sprachen lernen",
|
||||||
"description": "Hier ist eine sehr nützliche Website, die Ihnen hilft, fast jede Sprache der Welt zu lernen, einschließlich konstruierter Sprachen.",
|
"description": "Hier ist eine sehr nützliche Website, die Ihnen hilft, fast jede Sprache der Welt zu lernen, einschließlich konstruierter Sprachen.",
|
||||||
"explore": "Erkunden",
|
"explore": "Entdecken",
|
||||||
"fortune": {
|
"fortune": {
|
||||||
"quote": "Bleib hungrig, bleib dumm.",
|
"quote": "Stay hungry, stay foolish.",
|
||||||
"author": "— Steve Jobs"
|
"author": "— Steve Jobs"
|
||||||
},
|
},
|
||||||
"translator": {
|
"translator": {
|
||||||
"name": "Übersetzer",
|
"name": "Übersetzer",
|
||||||
"description": "In jede Sprache übersetzen und mit Internationalem Phonetischem Alphabet (IPA) annotieren"
|
"description": "In jede Sprache übersetzen und mit dem Internationalen Phonetischen Alphabet (IPA) annotieren"
|
||||||
},
|
},
|
||||||
"textSpeaker": {
|
"textSpeaker": {
|
||||||
"name": "Text-Sprecher",
|
"name": "Textvorleser",
|
||||||
"description": "Text erkennen und vorlesen, unterstützt Schleifenwiedergabe und Geschwindigkeitsanpassung"
|
"description": "Text erkennen und vorlesen, unterstützt Schleifenwiedergabe und Geschwindigkeitsanpassung"
|
||||||
},
|
},
|
||||||
"srtPlayer": {
|
"srtPlayer": {
|
||||||
"name": "SRT-Videoplayer",
|
"name": "SRT-Videoplayer",
|
||||||
"description": "Videos basierend auf SRT-Untertiteldateien satzweise abspielen, um die Aussprache von Muttersprachlern zu imitieren"
|
"description": "Videos Satz für Satz basierend auf SRT-Untertiteldateien abspielen, um die Aussprache von Muttersprachlern nachzuahmen"
|
||||||
},
|
},
|
||||||
"alphabet": {
|
"alphabet": {
|
||||||
"name": "Alphabet",
|
"name": "Alphabet",
|
||||||
"description": "Beginnen Sie mit dem Erlernen einer neuen Sprache mit dem Alphabet"
|
"description": "Beginnen Sie mit dem Lernen einer neuen Sprache vom Alphabet aus"
|
||||||
},
|
},
|
||||||
"memorize": {
|
"memorize": {
|
||||||
"name": "Einprägen",
|
"name": "Auswendig lernen",
|
||||||
"description": "Sprache A zu Sprache B, Sprache B zu Sprache A, unterstützt Diktat"
|
"description": "Sprache A zu Sprache B, Sprache B zu Sprache A, unterstützt Diktat"
|
||||||
},
|
},
|
||||||
"dictionary": {
|
"dictionary": {
|
||||||
"name": "Wörterbuch",
|
"name": "Wörterbuch",
|
||||||
"description": "Wörter und Redewendungen nachschlagen mit detaillierten Definitionen und Beispielen"
|
"description": "Wörter und Ausdrücke mit detaillierten Definitionen und Beispielen nachschlagen"
|
||||||
},
|
},
|
||||||
"moreFeatures": {
|
"moreFeatures": {
|
||||||
"name": "Weitere Funktionen",
|
"name": "Weitere Funktionen",
|
||||||
"description": "In Entwicklung, bleiben Sie dran"
|
"description": "In Entwicklung, bleiben Sie gespannt"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"title": "Authentifizierung",
|
"title": "Anmelden",
|
||||||
|
"signUpTitle": "Registrieren",
|
||||||
"signIn": "Anmelden",
|
"signIn": "Anmelden",
|
||||||
"signUp": "Registrieren",
|
"signUp": "Registrieren",
|
||||||
"email": "E-Mail",
|
"email": "E-Mail",
|
||||||
@@ -109,20 +130,47 @@
|
|||||||
"signUpWithGitHub": "Mit GitHub registrieren",
|
"signUpWithGitHub": "Mit GitHub registrieren",
|
||||||
"invalidEmail": "Bitte geben Sie eine gültige E-Mail-Adresse ein",
|
"invalidEmail": "Bitte geben Sie eine gültige E-Mail-Adresse ein",
|
||||||
"passwordTooShort": "Das Passwort muss mindestens 8 Zeichen lang sein",
|
"passwordTooShort": "Das Passwort muss mindestens 8 Zeichen lang sein",
|
||||||
"passwordsNotMatch": "Passwörter stimmen nicht überein",
|
"passwordsNotMatch": "Die Passwörter stimmen nicht überein",
|
||||||
"nameRequired": "Bitte geben Sie Ihren Namen ein",
|
"nameRequired": "Bitte geben Sie Ihren Namen ein",
|
||||||
"usernameRequired": "Bitte geben Sie Ihren Benutzernamen ein",
|
"usernameRequired": "Bitte geben Sie einen Benutzernamen ein",
|
||||||
"usernameTooShort": "Der Benutzername muss mindestens 3 Zeichen lang sein",
|
"usernameTooShort": "Der Benutzername muss mindestens 3 Zeichen lang sein",
|
||||||
"usernameInvalid": "Der Benutzername darf nur Buchstaben, Zahlen und Unterstriche enthalten",
|
"usernameInvalid": "Der Benutzername darf nur Buchstaben, Zahlen und Unterstriche enthalten",
|
||||||
"emailRequired": "Bitte geben Sie Ihre E-Mail ein",
|
"emailRequired": "Bitte geben Sie Ihre E-Mail ein",
|
||||||
"identifierRequired": "Bitte geben Sie Ihre E-Mail oder Ihren Benutzernamen ein",
|
"identifierRequired": "Bitte geben Sie Ihre E-Mail oder Ihren Benutzernamen ein",
|
||||||
"passwordRequired": "Bitte geben Sie Ihr Passwort ein",
|
"passwordRequired": "Bitte geben Sie Ihr Passwort ein",
|
||||||
"confirmPasswordRequired": "Bitte bestätigen Sie Ihr Passwort",
|
"confirmPasswordRequired": "Bitte bestätigen Sie Ihr Passwort",
|
||||||
"loading": "Laden..."
|
"loading": "Wird geladen...",
|
||||||
|
"confirm": "Bestätigen",
|
||||||
|
"noAccountLink": "Haben Sie kein Konto? Registrieren Sie sich",
|
||||||
|
"hasAccountLink": "Haben Sie bereits ein Konto? Anmelden",
|
||||||
|
"usernamePlaceholder": "Benutzername",
|
||||||
|
"emailPlaceholder": "E-Mail-Adresse",
|
||||||
|
"passwordPlaceholder": "Passwort",
|
||||||
|
"usernameOrEmailPlaceholder": "Benutzername oder E-Mail",
|
||||||
|
"loginFailed": "Anmeldung fehlgeschlagen",
|
||||||
|
"signUpFailed": "Registrierung fehlgeschlagen",
|
||||||
|
"fillAllFields": "Bitte füllen Sie alle Felder aus",
|
||||||
|
"enterCredentials": "Bitte geben Sie Benutzername und Passwort ein",
|
||||||
|
"forgotPassword": "Passwort vergessen",
|
||||||
|
"forgotPasswordHint": "Geben Sie Ihre E-Mail-Adresse ein und wir senden Ihnen einen Link zum Zurücksetzen Ihres Passworts.",
|
||||||
|
"sendResetEmail": "Reset-E-Mail senden",
|
||||||
|
"resetPasswordFailed": "Reset-E-Mail konnte nicht gesendet werden",
|
||||||
|
"resetPasswordEmailSent": "Reset-E-Mail erfolgreich gesendet",
|
||||||
|
"resetPasswordEmailSentHint": "Wir haben einen Link zum Zurücksetzen Ihres Passworts an Ihre E-Mail-Adresse gesendet. Bitte überprüfen Sie Ihren Posteingang.",
|
||||||
|
"checkYourEmail": "Überprüfen Sie Ihre E-Mail",
|
||||||
|
"backToLogin": "Zurück zur Anmeldung",
|
||||||
|
"resetPassword": "Passwort zurücksetzen",
|
||||||
|
"newPassword": "Neues Passwort",
|
||||||
|
"invalidToken": "Ungültiger oder abgelaufener Link",
|
||||||
|
"invalidTokenHint": "Dieser Link zum Zurücksetzen des Passworts ist ungültig oder abgelaufen. Bitte fordern Sie einen neuen an.",
|
||||||
|
"requestNewToken": "Neuen Reset-Link anfordern",
|
||||||
|
"resetPasswordSuccess": "Passwort erfolgreich zurückgesetzt",
|
||||||
|
"resetPasswordSuccessTitle": "Passwort-Zurücksetzung abgeschlossen",
|
||||||
|
"resetPasswordSuccessHint": "Ihr Passwort wurde erfolgreich zurückgesetzt. Sie können sich jetzt mit Ihrem neuen Passwort anmelden."
|
||||||
},
|
},
|
||||||
"memorize": {
|
"memorize": {
|
||||||
"folder_selector": {
|
"folder_selector": {
|
||||||
"selectFolder": "Wählen Sie einen Ordner aus",
|
"selectFolder": "Wählen Sie einen Ordner",
|
||||||
"noFolders": "Keine Ordner gefunden",
|
"noFolders": "Keine Ordner gefunden",
|
||||||
"folderInfo": "{id}. {name} ({count})"
|
"folderInfo": "{id}. {name} ({count})"
|
||||||
},
|
},
|
||||||
@@ -144,7 +192,9 @@
|
|||||||
"sourceCode": "GitHub",
|
"sourceCode": "GitHub",
|
||||||
"sign_in": "Anmelden",
|
"sign_in": "Anmelden",
|
||||||
"profile": "Profil",
|
"profile": "Profil",
|
||||||
"folders": "Ordner"
|
"folders": "Ordner",
|
||||||
|
"explore": "Entdecken",
|
||||||
|
"favorites": "Favoriten"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"myProfile": "Mein Profil",
|
"myProfile": "Mein Profil",
|
||||||
@@ -170,21 +220,27 @@
|
|||||||
"uploaded": "Hochgeladen",
|
"uploaded": "Hochgeladen",
|
||||||
"notUploaded": "Nicht hochgeladen",
|
"notUploaded": "Nicht hochgeladen",
|
||||||
"upload": "Hochladen",
|
"upload": "Hochladen",
|
||||||
|
"uploadVideoButton": "Video hochladen",
|
||||||
|
"uploadSubtitleButton": "Untertitel hochladen",
|
||||||
|
"subtitleUploaded": "Untertitel hochgeladen ({count} Einträge)",
|
||||||
|
"subtitleNotUploaded": "Untertitel nicht hochgeladen",
|
||||||
"autoPauseStatus": "Auto-Pause: {enabled}",
|
"autoPauseStatus": "Auto-Pause: {enabled}",
|
||||||
"on": "Ein",
|
"on": "Ein",
|
||||||
"off": "Aus",
|
"off": "Aus",
|
||||||
"videoUploadFailed": "Video-Upload fehlgeschlagen",
|
"videoUploadFailed": "Video-Upload fehlgeschlagen",
|
||||||
"subtitleUploadFailed": "Untertitel-Upload fehlgeschlagen"
|
"subtitleUploadFailed": "Untertitel-Upload fehlgeschlagen",
|
||||||
|
"subtitleLoadSuccess": "Untertitel erfolgreich geladen",
|
||||||
|
"subtitleLoadFailed": "Laden der Untertitel fehlgeschlagen"
|
||||||
},
|
},
|
||||||
"text_speaker": {
|
"text_speaker": {
|
||||||
"generateIPA": "IPA generieren",
|
"generateIPA": "IPA generieren",
|
||||||
"viewSavedItems": "Gespeicherte Elemente anzeigen",
|
"viewSavedItems": "Gespeicherte Einträge anzeigen",
|
||||||
"confirmDeleteAll": "Sind Sie sicher, dass Sie alles löschen möchten? (J/N)"
|
"confirmDeleteAll": "Sind Sie sicher, dass Sie alles löschen möchten? (J/N)"
|
||||||
},
|
},
|
||||||
"translator": {
|
"translator": {
|
||||||
"detectLanguage": "Sprache erkennen",
|
"detectLanguage": "Sprache erkennen",
|
||||||
"generateIPA": "IPA generieren",
|
"generateIPA": "IPA generieren",
|
||||||
"translateInto": "Übersetzen in",
|
"translateInto": "übersetzen in",
|
||||||
"chinese": "Chinesisch",
|
"chinese": "Chinesisch",
|
||||||
"english": "Englisch",
|
"english": "Englisch",
|
||||||
"french": "Französisch",
|
"french": "Französisch",
|
||||||
@@ -196,49 +252,88 @@
|
|||||||
"russian": "Russisch",
|
"russian": "Russisch",
|
||||||
"spanish": "Spanisch",
|
"spanish": "Spanisch",
|
||||||
"other": "Andere",
|
"other": "Andere",
|
||||||
"translating": "Übersetzung läuft...",
|
"translating": "wird übersetzt...",
|
||||||
"translate": "Übersetzen",
|
"translate": "übersetzen",
|
||||||
"inputLanguage": "Geben Sie eine Sprache ein.",
|
"inputLanguage": "Geben Sie eine Sprache ein.",
|
||||||
"history": "Verlauf",
|
"history": "Verlauf",
|
||||||
"enterLanguage": "Sprache eingeben",
|
"enterLanguage": "Sprache eingeben",
|
||||||
"add_to_folder": {
|
"add_to_folder": {
|
||||||
"notAuthenticated": "Sie sind nicht authentifiziert",
|
"notAuthenticated": "Sie sind nicht authentifiziert",
|
||||||
"chooseFolder": "Wählen Sie einen Ordner zum Hinzufügen aus",
|
"chooseFolder": "Wählen Sie einen Ordner zum Hinzufügen",
|
||||||
"noFolders": "Keine Ordner gefunden",
|
"noFolders": "Keine Ordner gefunden",
|
||||||
"folderInfo": "{id}. {name}",
|
"folderInfo": "{id}. {name}",
|
||||||
"close": "Schließen",
|
"close": "Schließen",
|
||||||
"success": "Textpaar zum Ordner hinzugefügt",
|
"success": "Textpaar zum Ordner hinzugefügt",
|
||||||
"error": "Textpaar konnte nicht zum Ordner hinzugefügt werden"
|
"error": "Fehler beim Hinzufügen des Textpaars zum Ordner"
|
||||||
},
|
},
|
||||||
"autoSave": "Automatisch speichern"
|
"autoSave": "Autom. Speichern"
|
||||||
},
|
},
|
||||||
"dictionary": {
|
"dictionary": {
|
||||||
"title": "Wörterbuch",
|
"title": "Wörterbuch",
|
||||||
"description": "Wörter und Ausdrücke mit detaillierten Definitionen und Beispielen nachschlagen",
|
"description": "Wörter und Ausdrücke mit detaillierten Definitionen und Beispielen nachschlagen",
|
||||||
"searchPlaceholder": "Wort oder Ausdruck zum Nachschlagen eingeben...",
|
"searchPlaceholder": "Geben Sie ein Wort oder einen Ausdruck zum Nachschlagen ein...",
|
||||||
"searching": "Suche...",
|
"searching": "Suche läuft...",
|
||||||
"search": "Suchen",
|
"search": "Suchen",
|
||||||
"languageSettings": "Spracheinstellungen",
|
"languageSettings": "Spracheinstellungen",
|
||||||
"queryLanguage": "Abfragesprache",
|
"queryLanguage": "Abfragesprache",
|
||||||
"queryLanguageHint": "Welche Sprache hat das Wort/die Phrase, die Sie nachschlagen möchten",
|
"queryLanguageHint": "In welcher Sprache ist das Wort/der Ausdruck, den Sie nachschlagen möchten",
|
||||||
"definitionLanguage": "Definitionssprache",
|
"definitionLanguage": "Definitionssprache",
|
||||||
"definitionLanguageHint": "In welcher Sprache möchten Sie die Definitionen sehen",
|
"definitionLanguageHint": "In welcher Sprache möchten Sie die Definitionen",
|
||||||
"otherLanguagePlaceholder": "Oder eine andere Sprache eingeben...",
|
"otherLanguagePlaceholder": "Oder geben Sie eine andere Sprache ein...",
|
||||||
|
"other": "Andere",
|
||||||
"currentSettings": "Aktuelle Einstellungen: Abfrage {queryLang}, Definition {definitionLang}",
|
"currentSettings": "Aktuelle Einstellungen: Abfrage {queryLang}, Definition {definitionLang}",
|
||||||
"relookup": "Neu suchen",
|
"relookup": "Erneut suchen",
|
||||||
"saveToFolder": "In Ordner speichern",
|
"saveToFolder": "In Ordner speichern",
|
||||||
"loading": "Laden...",
|
"loading": "Wird geladen...",
|
||||||
"noResults": "Keine Ergebnisse gefunden",
|
"noResults": "Keine Ergebnisse gefunden",
|
||||||
"tryOtherWords": "Versuchen Sie andere Wörter oder Ausdrücke",
|
"tryOtherWords": "Versuchen Sie andere Wörter oder Ausdrücke",
|
||||||
"welcomeTitle": "Willkommen beim Wörterbuch",
|
"welcomeTitle": "Willkommen im Wörterbuch",
|
||||||
"welcomeHint": "Geben Sie oben im Suchfeld ein Wort oder einen Ausdruck ein, um zu suchen",
|
"welcomeHint": "Geben Sie oben in das Suchfeld ein Wort oder einen Ausdruck ein, um mit dem Nachschlagen zu beginnen",
|
||||||
"lookupFailed": "Suche fehlgeschlagen, bitte später erneut versuchen",
|
"lookupFailed": "Suche fehlgeschlagen, bitte versuchen Sie es später erneut",
|
||||||
"relookupSuccess": "Erfolgreich neu gesucht",
|
"relookupSuccess": "Erneute Suche erfolgreich",
|
||||||
"relookupFailed": "Wörterbuch Neu-Suche fehlgeschlagen",
|
"relookupFailed": "Erneute Wörterbuchsuche fehlgeschlagen",
|
||||||
"pleaseLogin": "Bitte melden Sie sich zuerst an",
|
"pleaseLogin": "Bitte melden Sie sich zuerst an",
|
||||||
"pleaseCreateFolder": "Bitte erstellen Sie zuerst einen Ordner",
|
"pleaseCreateFolder": "Bitte erstellen Sie zuerst einen Ordner",
|
||||||
"savedToFolder": "Im Ordner gespeichert: {folderName}",
|
"savedToFolder": "In Ordner gespeichert: {folderName}",
|
||||||
"saveFailed": "Speichern fehlgeschlagen, bitte später erneut versuchen"
|
"saveFailed": "Speichern fehlgeschlagen, bitte versuchen Sie es später erneut",
|
||||||
|
"definition": "Definition",
|
||||||
|
"example": "Beispiel"
|
||||||
|
},
|
||||||
|
"explore": {
|
||||||
|
"title": "Entdecken",
|
||||||
|
"subtitle": "Öffentliche Ordner entdecken",
|
||||||
|
"searchPlaceholder": "Öffentliche Ordner durchsuchen...",
|
||||||
|
"loading": "Wird geladen...",
|
||||||
|
"noFolders": "Keine öffentlichen Ordner gefunden",
|
||||||
|
"folderInfo": "{userName} • {totalPairs} Paare",
|
||||||
|
"unknownUser": "Unbekannter Benutzer",
|
||||||
|
"favorite": "Favorisieren",
|
||||||
|
"unfavorite": "Aus Favoriten entfernen",
|
||||||
|
"pleaseLogin": "Bitte melden Sie sich zuerst an",
|
||||||
|
"sortByFavorites": "Nach Favoriten sortieren",
|
||||||
|
"sortByFavoritesActive": "Sortierung nach Favoriten aufheben"
|
||||||
|
},
|
||||||
|
"exploreDetail": {
|
||||||
|
"title": "Ordnerdetails",
|
||||||
|
"createdBy": "Erstellt von: {name}",
|
||||||
|
"unknownUser": "Unbekannter Benutzer",
|
||||||
|
"totalPairs": "Gesamtpaare",
|
||||||
|
"favorites": "Favoriten",
|
||||||
|
"createdAt": "Erstellt am",
|
||||||
|
"viewContent": "Inhalt anzeigen",
|
||||||
|
"favorite": "Favorisieren",
|
||||||
|
"unfavorite": "Aus Favoriten entfernen",
|
||||||
|
"favorited": "Favorisiert",
|
||||||
|
"unfavorited": "Aus Favoriten entfernt",
|
||||||
|
"pleaseLogin": "Bitte melden Sie sich zuerst an"
|
||||||
|
},
|
||||||
|
"favorites": {
|
||||||
|
"title": "Meine Favoriten",
|
||||||
|
"subtitle": "Ordner, die Sie favorisiert haben",
|
||||||
|
"loading": "Wird geladen...",
|
||||||
|
"noFavorites": "Noch keine Favoriten",
|
||||||
|
"folderInfo": "{userName} • {totalPairs} Paare",
|
||||||
|
"unknownUser": "Unbekannter Benutzer"
|
||||||
},
|
},
|
||||||
"user_profile": {
|
"user_profile": {
|
||||||
"anonymous": "Anonym",
|
"anonymous": "Anonym",
|
||||||
@@ -251,14 +346,15 @@
|
|||||||
"displayName": "Anzeigename",
|
"displayName": "Anzeigename",
|
||||||
"notSet": "Nicht festgelegt",
|
"notSet": "Nicht festgelegt",
|
||||||
"memberSince": "Mitglied seit",
|
"memberSince": "Mitglied seit",
|
||||||
|
"logout": "Abmelden",
|
||||||
"folders": {
|
"folders": {
|
||||||
"title": "Ordner",
|
"title": "Ordner",
|
||||||
"noFolders": "Noch keine Ordner",
|
"noFolders": "Noch keine Ordner",
|
||||||
"folderName": "Ordnername",
|
"folderName": "Ordnername",
|
||||||
"totalPairs": "Anzahl der Paare",
|
"totalPairs": "Gesamtpaare",
|
||||||
"createdAt": "Erstellt am",
|
"createdAt": "Erstellt am",
|
||||||
"actions": "Aktionen",
|
"actions": "Aktionen",
|
||||||
"view": "Ansehen"
|
"view": "Anzeigen"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"alphabet": {
|
"alphabet": {
|
||||||
"chooseCharacters": "Please select the characters you want to learn",
|
"chooseCharacters": "Please select the characters you want to learn",
|
||||||
|
"chooseAlphabetHint": "Select an alphabet to start learning",
|
||||||
"japanese": "Japanese Kana",
|
"japanese": "Japanese Kana",
|
||||||
"english": "English Alphabet",
|
"english": "English Alphabet",
|
||||||
"uyghur": "Uyghur Alphabet",
|
"uyghur": "Uyghur Alphabet",
|
||||||
@@ -14,7 +15,11 @@
|
|||||||
"roman": "Romanization",
|
"roman": "Romanization",
|
||||||
"letter": "Letter",
|
"letter": "Letter",
|
||||||
"random": "Random Mode",
|
"random": "Random Mode",
|
||||||
"randomNext": "Random Next"
|
"randomNext": "Random Next",
|
||||||
|
"previousLetter": "Previous letter",
|
||||||
|
"nextLetter": "Next letter",
|
||||||
|
"keyboardHint": "Use left/right arrow keys or space for random, ESC to go back",
|
||||||
|
"swipeHint": "Use left/right arrow keys or swipe to navigate, ESC to go back"
|
||||||
},
|
},
|
||||||
"folders": {
|
"folders": {
|
||||||
"title": "Folders",
|
"title": "Folders",
|
||||||
@@ -107,7 +112,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"title": "Authentication",
|
"title": "Sign In",
|
||||||
|
"signUpTitle": "Sign Up",
|
||||||
"signIn": "Sign In",
|
"signIn": "Sign In",
|
||||||
"signUp": "Sign Up",
|
"signUp": "Sign Up",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
@@ -133,7 +139,34 @@
|
|||||||
"identifierRequired": "Please enter your email or username",
|
"identifierRequired": "Please enter your email or username",
|
||||||
"passwordRequired": "Please enter your password",
|
"passwordRequired": "Please enter your password",
|
||||||
"confirmPasswordRequired": "Please confirm your password",
|
"confirmPasswordRequired": "Please confirm your password",
|
||||||
"loading": "Loading..."
|
"loading": "Loading...",
|
||||||
|
"confirm": "Confirm",
|
||||||
|
"noAccountLink": "Don't have an account? Sign up",
|
||||||
|
"hasAccountLink": "Already have an account? Sign in",
|
||||||
|
"usernamePlaceholder": "Username",
|
||||||
|
"emailPlaceholder": "Email address",
|
||||||
|
"passwordPlaceholder": "Password",
|
||||||
|
"usernameOrEmailPlaceholder": "Username or email",
|
||||||
|
"loginFailed": "Login failed",
|
||||||
|
"signUpFailed": "Sign up failed",
|
||||||
|
"fillAllFields": "Please fill in all fields",
|
||||||
|
"enterCredentials": "Please enter username and password",
|
||||||
|
"forgotPassword": "Forgot Password",
|
||||||
|
"forgotPasswordHint": "Enter your email address and we'll send you a link to reset your password.",
|
||||||
|
"sendResetEmail": "Send Reset Email",
|
||||||
|
"resetPasswordFailed": "Failed to send reset email",
|
||||||
|
"resetPasswordEmailSent": "Reset email sent successfully",
|
||||||
|
"resetPasswordEmailSentHint": "We've sent a password reset link to your email address. Please check your inbox.",
|
||||||
|
"checkYourEmail": "Check Your Email",
|
||||||
|
"backToLogin": "Back to Login",
|
||||||
|
"resetPassword": "Reset Password",
|
||||||
|
"newPassword": "New Password",
|
||||||
|
"invalidToken": "Invalid or Expired Link",
|
||||||
|
"invalidTokenHint": "This password reset link is invalid or has expired. Please request a new one.",
|
||||||
|
"requestNewToken": "Request New Reset Link",
|
||||||
|
"resetPasswordSuccess": "Password reset successfully",
|
||||||
|
"resetPasswordSuccessTitle": "Password Reset Complete",
|
||||||
|
"resetPasswordSuccessHint": "Your password has been reset successfully. You can now log in with your new password."
|
||||||
},
|
},
|
||||||
"memorize": {
|
"memorize": {
|
||||||
"folder_selector": {
|
"folder_selector": {
|
||||||
@@ -187,11 +220,17 @@
|
|||||||
"uploaded": "Uploaded",
|
"uploaded": "Uploaded",
|
||||||
"notUploaded": "Not Uploaded",
|
"notUploaded": "Not Uploaded",
|
||||||
"upload": "Upload",
|
"upload": "Upload",
|
||||||
|
"uploadVideoButton": "Upload Video",
|
||||||
|
"uploadSubtitleButton": "Upload Subtitle",
|
||||||
|
"subtitleUploaded": "Subtitle Uploaded ({count} entries)",
|
||||||
|
"subtitleNotUploaded": "Subtitle Not Uploaded",
|
||||||
"autoPauseStatus": "Auto Pause: {enabled}",
|
"autoPauseStatus": "Auto Pause: {enabled}",
|
||||||
"on": "On",
|
"on": "On",
|
||||||
"off": "Off",
|
"off": "Off",
|
||||||
"videoUploadFailed": "Video upload failed",
|
"videoUploadFailed": "Video upload failed",
|
||||||
"subtitleUploadFailed": "Subtitle upload failed"
|
"subtitleUploadFailed": "Subtitle upload failed",
|
||||||
|
"subtitleLoadSuccess": "Subtitle loaded successfully",
|
||||||
|
"subtitleLoadFailed": "Subtitle load failed"
|
||||||
},
|
},
|
||||||
"text_speaker": {
|
"text_speaker": {
|
||||||
"generateIPA": "Generate IPA",
|
"generateIPA": "Generate IPA",
|
||||||
@@ -256,7 +295,9 @@
|
|||||||
"pleaseLogin": "Please log in first",
|
"pleaseLogin": "Please log in first",
|
||||||
"pleaseCreateFolder": "Please create a folder first",
|
"pleaseCreateFolder": "Please create a folder first",
|
||||||
"savedToFolder": "Saved to folder: {folderName}",
|
"savedToFolder": "Saved to folder: {folderName}",
|
||||||
"saveFailed": "Save failed, please try again later"
|
"saveFailed": "Save failed, please try again later",
|
||||||
|
"definition": "Definition",
|
||||||
|
"example": "Example"
|
||||||
},
|
},
|
||||||
"explore": {
|
"explore": {
|
||||||
"title": "Explore",
|
"title": "Explore",
|
||||||
@@ -272,6 +313,20 @@
|
|||||||
"sortByFavorites": "Sort by favorites",
|
"sortByFavorites": "Sort by favorites",
|
||||||
"sortByFavoritesActive": "Undo sort by favorites"
|
"sortByFavoritesActive": "Undo sort by favorites"
|
||||||
},
|
},
|
||||||
|
"exploreDetail": {
|
||||||
|
"title": "Folder Details",
|
||||||
|
"createdBy": "Created by: {name}",
|
||||||
|
"unknownUser": "Unknown User",
|
||||||
|
"totalPairs": "Total Pairs",
|
||||||
|
"favorites": "Favorites",
|
||||||
|
"createdAt": "Created At",
|
||||||
|
"viewContent": "View Content",
|
||||||
|
"favorite": "Favorite",
|
||||||
|
"unfavorite": "Unfavorite",
|
||||||
|
"favorited": "Favorited",
|
||||||
|
"unfavorited": "Unfavorited",
|
||||||
|
"pleaseLogin": "Please login first"
|
||||||
|
},
|
||||||
"favorites": {
|
"favorites": {
|
||||||
"title": "My Favorites",
|
"title": "My Favorites",
|
||||||
"subtitle": "Folders you've favorited",
|
"subtitle": "Folders you've favorited",
|
||||||
@@ -291,6 +346,7 @@
|
|||||||
"displayName": "Display Name",
|
"displayName": "Display Name",
|
||||||
"notSet": "Not Set",
|
"notSet": "Not Set",
|
||||||
"memberSince": "Member Since",
|
"memberSince": "Member Since",
|
||||||
|
"logout": "Logout",
|
||||||
"folders": {
|
"folders": {
|
||||||
"title": "Folders",
|
"title": "Folders",
|
||||||
"noFolders": "No folders yet",
|
"noFolders": "No folders yet",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"alphabet": {
|
"alphabet": {
|
||||||
"chooseCharacters": "Veuillez sélectionner les caractères que vous souhaitez apprendre",
|
"chooseCharacters": "Veuillez sélectionner les caractères que vous souhaitez apprendre",
|
||||||
|
"chooseAlphabetHint": "Sélectionnez un alphabet pour commencer à apprendre",
|
||||||
"japanese": "Kana japonais",
|
"japanese": "Kana japonais",
|
||||||
"english": "Alphabet anglais",
|
"english": "Alphabet anglais",
|
||||||
"uyghur": "Alphabet ouïghour",
|
"uyghur": "Alphabet ouïghour",
|
||||||
@@ -14,29 +15,48 @@
|
|||||||
"roman": "Romanisation",
|
"roman": "Romanisation",
|
||||||
"letter": "Lettre",
|
"letter": "Lettre",
|
||||||
"random": "Mode aléatoire",
|
"random": "Mode aléatoire",
|
||||||
"randomNext": "Suivant aléatoire"
|
"randomNext": "Suivant aléatoire",
|
||||||
|
"previousLetter": "Lettre précédente",
|
||||||
|
"nextLetter": "Lettre suivante",
|
||||||
|
"keyboardHint": "Utilisez les touches fléchées gauche/droite ou espace pour aléatoire, ÉCHAP pour revenir",
|
||||||
|
"swipeHint": "Utilisez les touches fléchées gauche/droite ou balayez pour naviguer, ÉCHAP pour revenir"
|
||||||
},
|
},
|
||||||
"folders": {
|
"folders": {
|
||||||
"title": "Dossiers",
|
"title": "Dossiers",
|
||||||
"subtitle": "Gérez vos collections",
|
"subtitle": "Gérez vos collections",
|
||||||
"newFolder": "Nouveau dossier",
|
"newFolder": "Nouveau dossier",
|
||||||
"creating": "Création...",
|
"creating": "Création...",
|
||||||
"noFoldersYet": "Aucun dossier pour le moment",
|
"noFoldersYet": "Pas encore de dossiers",
|
||||||
"folderInfo": "ID: {id} • {totalPairs} paires",
|
"folderInfo": "ID : {id} • {totalPairs} paires",
|
||||||
"enterFolderName": "Entrez le nom du dossier:",
|
"enterFolderName": "Entrez le nom du dossier :",
|
||||||
"confirmDelete": "Tapez \"{name}\" pour supprimer:"
|
"confirmDelete": "Tapez \"{name}\" pour supprimer :",
|
||||||
|
"myFolders": "Mes dossiers",
|
||||||
|
"publicFolders": "Dossiers publics",
|
||||||
|
"public": "Public",
|
||||||
|
"private": "Privé",
|
||||||
|
"setPublic": "Définir comme public",
|
||||||
|
"setPrivate": "Définir comme privé",
|
||||||
|
"publicFolderInfo": "{userName} • {totalPairs} paires",
|
||||||
|
"searchPlaceholder": "Rechercher des dossiers publics...",
|
||||||
|
"loading": "Chargement...",
|
||||||
|
"noPublicFolders": "Aucun dossier public trouvé",
|
||||||
|
"unknownUser": "Utilisateur inconnu",
|
||||||
|
"enterNewName": "Entrez le nouveau nom :",
|
||||||
|
"favorite": "Favori",
|
||||||
|
"unfavorite": "Retirer des favoris",
|
||||||
|
"pleaseLogin": "Veuillez vous connecter d'abord"
|
||||||
},
|
},
|
||||||
"folder_id": {
|
"folder_id": {
|
||||||
"unauthorized": "Vous n'êtes pas le propriétaire de ce dossier",
|
"unauthorized": "Vous n'êtes pas le propriétaire de ce dossier",
|
||||||
"back": "Retour",
|
"back": "Retour",
|
||||||
"textPairs": "Paires de textes",
|
"textPairs": "Paires de texte",
|
||||||
"itemsCount": "{count} éléments",
|
"itemsCount": "{count} éléments",
|
||||||
"memorize": "Mémoriser",
|
"memorize": "Mémoriser",
|
||||||
"loadingTextPairs": "Chargement des paires de textes...",
|
"loadingTextPairs": "Chargement des paires de texte...",
|
||||||
"noTextPairs": "Aucune paire de textes dans ce dossier",
|
"noTextPairs": "Aucune paire de texte dans ce dossier",
|
||||||
"addNewTextPair": "Ajouter une nouvelle paire de textes",
|
"addNewTextPair": "Ajouter une nouvelle paire de texte",
|
||||||
"add": "Ajouter",
|
"add": "Ajouter",
|
||||||
"updateTextPair": "Mettre à jour la paire de textes",
|
"updateTextPair": "Mettre à jour la paire de texte",
|
||||||
"update": "Mettre à jour",
|
"update": "Mettre à jour",
|
||||||
"text1": "Texte 1",
|
"text1": "Texte 1",
|
||||||
"text2": "Texte 2",
|
"text2": "Texte 2",
|
||||||
@@ -56,15 +76,15 @@
|
|||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"title": "Apprendre les langues",
|
"title": "Apprendre les langues",
|
||||||
"description": "Voici un site web très utile pour vous aider à apprendre presque toutes les langues du monde, y compris les langues construites.",
|
"description": "Voici un site Web très utile pour vous aider à apprendre presque toutes les langues du monde, y compris les langues construites.",
|
||||||
"explore": "Explorer",
|
"explore": "Explorer",
|
||||||
"fortune": {
|
"fortune": {
|
||||||
"quote": "Stay hungry, stay foolish.",
|
"quote": "Restez affamés, restez fous.",
|
||||||
"author": "— Steve Jobs"
|
"author": "— Steve Jobs"
|
||||||
},
|
},
|
||||||
"translator": {
|
"translator": {
|
||||||
"name": "Traducteur",
|
"name": "Traducteur",
|
||||||
"description": "Traduire dans n'importe quelle langue et annoter avec l'alphabet phonétique international (API)"
|
"description": "Traduire vers n'importe quelle langue et annoter avec l'Alphabet Phonétique International (API)"
|
||||||
},
|
},
|
||||||
"textSpeaker": {
|
"textSpeaker": {
|
||||||
"name": "Lecteur de texte",
|
"name": "Lecteur de texte",
|
||||||
@@ -76,15 +96,15 @@
|
|||||||
},
|
},
|
||||||
"alphabet": {
|
"alphabet": {
|
||||||
"name": "Alphabet",
|
"name": "Alphabet",
|
||||||
"description": "Commencer à apprendre une nouvelle langue par l'alphabet"
|
"description": "Commencez à apprendre une nouvelle langue à partir de l'alphabet"
|
||||||
},
|
},
|
||||||
"memorize": {
|
"memorize": {
|
||||||
"name": "Mémoriser",
|
"name": "Mémoriser",
|
||||||
"description": "Langue A vers langue B, langue B vers langue A, prend en charge la dictée"
|
"description": "Langue A vers Langue B, Langue B vers Langue A, prend en charge la dictée"
|
||||||
},
|
},
|
||||||
"dictionary": {
|
"dictionary": {
|
||||||
"name": "Dictionnaire",
|
"name": "Dictionnaire",
|
||||||
"description": "Rechercher des mots et des phrases avec des définitions détaillées et des exemples"
|
"description": "Rechercher des mots et des expressions avec des définitions détaillées et des exemples"
|
||||||
},
|
},
|
||||||
"moreFeatures": {
|
"moreFeatures": {
|
||||||
"name": "Plus de fonctionnalités",
|
"name": "Plus de fonctionnalités",
|
||||||
@@ -92,7 +112,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"title": "Authentification",
|
"title": "Se connecter",
|
||||||
|
"signUpTitle": "S'inscrire",
|
||||||
"signIn": "Se connecter",
|
"signIn": "Se connecter",
|
||||||
"signUp": "S'inscrire",
|
"signUp": "S'inscrire",
|
||||||
"email": "E-mail",
|
"email": "E-mail",
|
||||||
@@ -103,22 +124,49 @@
|
|||||||
"emailOrUsername": "E-mail ou nom d'utilisateur",
|
"emailOrUsername": "E-mail ou nom d'utilisateur",
|
||||||
"signInButton": "Se connecter",
|
"signInButton": "Se connecter",
|
||||||
"signUpButton": "S'inscrire",
|
"signUpButton": "S'inscrire",
|
||||||
"noAccount": "Vous n'avez pas de compte?",
|
"noAccount": "Vous n'avez pas de compte ?",
|
||||||
"hasAccount": "Vous avez déjà un compte?",
|
"hasAccount": "Vous avez déjà un compte ?",
|
||||||
"signInWithGitHub": "Se connecter avec GitHub",
|
"signInWithGitHub": "Se connecter avec GitHub",
|
||||||
"signUpWithGitHub": "S'inscrire avec GitHub",
|
"signUpWithGitHub": "S'inscrire avec GitHub",
|
||||||
"invalidEmail": "Veuillez entrer une adresse e-mail valide",
|
"invalidEmail": "Veuillez entrer une adresse e-mail valide",
|
||||||
"passwordTooShort": "Le mot de passe doit contenir au moins 8 caractères",
|
"passwordTooShort": "Le mot de passe doit contenir au moins 8 caractères",
|
||||||
"passwordsNotMatch": "Les mots de passe ne correspondent pas",
|
"passwordsNotMatch": "Les mots de passe ne correspondent pas",
|
||||||
"nameRequired": "Veuillez entrer votre nom",
|
"nameRequired": "Veuillez entrer votre nom",
|
||||||
"usernameRequired": "Veuillez entrer votre nom d'utilisateur",
|
"usernameRequired": "Veuillez entrer un nom d'utilisateur",
|
||||||
"usernameTooShort": "Le nom d'utilisateur doit contenir au moins 3 caractères",
|
"usernameTooShort": "Le nom d'utilisateur doit contenir au moins 3 caractères",
|
||||||
"usernameInvalid": "Le nom d'utilisateur ne peut contenir que des lettres, des chiffres et des underscores",
|
"usernameInvalid": "Le nom d'utilisateur ne peut contenir que des lettres, des chiffres et des underscores",
|
||||||
"emailRequired": "Veuillez entrer votre e-mail",
|
"emailRequired": "Veuillez entrer votre e-mail",
|
||||||
"identifierRequired": "Veuillez entrer votre e-mail ou nom d'utilisateur",
|
"identifierRequired": "Veuillez entrer votre e-mail ou nom d'utilisateur",
|
||||||
"passwordRequired": "Veuillez entrer votre mot de passe",
|
"passwordRequired": "Veuillez entrer votre mot de passe",
|
||||||
"confirmPasswordRequired": "Veuillez confirmer votre mot de passe",
|
"confirmPasswordRequired": "Veuillez confirmer votre mot de passe",
|
||||||
"loading": "Chargement..."
|
"loading": "Chargement...",
|
||||||
|
"confirm": "Confirmer",
|
||||||
|
"noAccountLink": "Vous n'avez pas de compte ? Inscrivez-vous",
|
||||||
|
"hasAccountLink": "Vous avez déjà un compte ? Connectez-vous",
|
||||||
|
"usernamePlaceholder": "Nom d'utilisateur",
|
||||||
|
"emailPlaceholder": "Adresse e-mail",
|
||||||
|
"passwordPlaceholder": "Mot de passe",
|
||||||
|
"usernameOrEmailPlaceholder": "Nom d'utilisateur ou e-mail",
|
||||||
|
"loginFailed": "Échec de la connexion",
|
||||||
|
"signUpFailed": "Échec de l'inscription",
|
||||||
|
"fillAllFields": "Veuillez remplir tous les champs",
|
||||||
|
"enterCredentials": "Veuillez entrer le nom d'utilisateur et le mot de passe",
|
||||||
|
"forgotPassword": "Mot de passe oublié",
|
||||||
|
"forgotPasswordHint": "Entrez votre adresse e-mail et nous vous enverrons un lien pour réinitialiser votre mot de passe.",
|
||||||
|
"sendResetEmail": "Envoyer l'e-mail de réinitialisation",
|
||||||
|
"resetPasswordFailed": "Échec de l'envoi de l'e-mail de réinitialisation",
|
||||||
|
"resetPasswordEmailSent": "E-mail de réinitialisation envoyé avec succès",
|
||||||
|
"resetPasswordEmailSentHint": "Nous avons envoyé un lien de réinitialisation de mot de passe à votre adresse e-mail. Veuillez vérifier votre boîte de réception.",
|
||||||
|
"checkYourEmail": "Vérifiez votre e-mail",
|
||||||
|
"backToLogin": "Retour à la connexion",
|
||||||
|
"resetPassword": "Réinitialiser le mot de passe",
|
||||||
|
"newPassword": "Nouveau mot de passe",
|
||||||
|
"invalidToken": "Lien invalide ou expiré",
|
||||||
|
"invalidTokenHint": "Ce lien de réinitialisation de mot de passe est invalide ou a expiré. Veuillez en demander un nouveau.",
|
||||||
|
"requestNewToken": "Demander un nouveau lien de réinitialisation",
|
||||||
|
"resetPasswordSuccess": "Mot de passe réinitialisé avec succès",
|
||||||
|
"resetPasswordSuccessTitle": "Réinitialisation du mot de passe terminée",
|
||||||
|
"resetPasswordSuccessHint": "Votre mot de passe a été réinitialisé avec succès. Vous pouvez maintenant vous connecter avec votre nouveau mot de passe."
|
||||||
},
|
},
|
||||||
"memorize": {
|
"memorize": {
|
||||||
"folder_selector": {
|
"folder_selector": {
|
||||||
@@ -131,7 +179,7 @@
|
|||||||
"next": "Suivant",
|
"next": "Suivant",
|
||||||
"reverse": "Inverser",
|
"reverse": "Inverser",
|
||||||
"dictation": "Dictée",
|
"dictation": "Dictée",
|
||||||
"noTextPairs": "Aucune paire de textes disponible",
|
"noTextPairs": "Aucune paire de texte disponible",
|
||||||
"disorder": "Désordre",
|
"disorder": "Désordre",
|
||||||
"previous": "Précédent"
|
"previous": "Précédent"
|
||||||
},
|
},
|
||||||
@@ -140,46 +188,54 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"navbar": {
|
"navbar": {
|
||||||
"title": "learn-languages",
|
"title": "apprendre-langues",
|
||||||
"sourceCode": "GitHub",
|
"sourceCode": "GitHub",
|
||||||
"sign_in": "Se connecter",
|
"sign_in": "Se connecter",
|
||||||
"profile": "Profil",
|
"profile": "Profil",
|
||||||
"folders": "Dossiers"
|
"folders": "Dossiers",
|
||||||
|
"explore": "Explorer",
|
||||||
|
"favorites": "Favoris"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"myProfile": "Mon profil",
|
"myProfile": "Mon profil",
|
||||||
"email": "E-mail: {email}",
|
"email": "E-mail : {email}",
|
||||||
"logout": "Se déconnecter"
|
"logout": "Déconnexion"
|
||||||
},
|
},
|
||||||
"srt_player": {
|
"srt_player": {
|
||||||
"uploadVideo": "Télécharger une vidéo",
|
"uploadVideo": "Télécharger la vidéo",
|
||||||
"uploadSubtitle": "Télécharger des sous-titres",
|
"uploadSubtitle": "Télécharger les sous-titres",
|
||||||
"pause": "Pause",
|
"pause": "Pause",
|
||||||
"play": "Lire",
|
"play": "Lecture",
|
||||||
"previous": "Précédent",
|
"previous": "Précédent",
|
||||||
"next": "Suivant",
|
"next": "Suivant",
|
||||||
"restart": "Redémarrer",
|
"restart": "Recommencer",
|
||||||
"autoPause": "Pause automatique ({enabled})",
|
"autoPause": "Pause automatique ({enabled})",
|
||||||
"uploadVideoAndSubtitle": "Veuillez télécharger des fichiers vidéo et de sous-titres",
|
"uploadVideoAndSubtitle": "Veuillez télécharger les fichiers vidéo et sous-titres",
|
||||||
"uploadVideoFile": "Veuillez télécharger un fichier vidéo",
|
"uploadVideoFile": "Veuillez télécharger le fichier vidéo",
|
||||||
"uploadSubtitleFile": "Veuillez télécharger un fichier de sous-titres",
|
"uploadSubtitleFile": "Veuillez télécharger le fichier de sous-titres",
|
||||||
"processingSubtitle": "Traitement du fichier de sous-titres...",
|
"processingSubtitle": "Traitement du fichier de sous-titres...",
|
||||||
"needBothFiles": "Les fichiers vidéo et de sous-titres sont requis pour commencer l'apprentissage",
|
"needBothFiles": "Les fichiers vidéo et sous-titres sont tous deux requis pour commencer l'apprentissage",
|
||||||
"videoFile": "Fichier vidéo",
|
"videoFile": "Fichier vidéo",
|
||||||
"subtitleFile": "Fichier de sous-titres",
|
"subtitleFile": "Fichier de sous-titres",
|
||||||
"uploaded": "Téléchargé",
|
"uploaded": "Téléchargé",
|
||||||
"notUploaded": "Non téléchargé",
|
"notUploaded": "Non téléchargé",
|
||||||
"upload": "Télécharger",
|
"upload": "Télécharger",
|
||||||
"autoPauseStatus": "Pause automatique: {enabled}",
|
"uploadVideoButton": "Télécharger la vidéo",
|
||||||
|
"uploadSubtitleButton": "Télécharger les sous-titres",
|
||||||
|
"subtitleUploaded": "Sous-titres téléchargés ({count} entrées)",
|
||||||
|
"subtitleNotUploaded": "Sous-titres non téléchargés",
|
||||||
|
"autoPauseStatus": "Pause automatique : {enabled}",
|
||||||
"on": "Activé",
|
"on": "Activé",
|
||||||
"off": "Désactivé",
|
"off": "Désactivé",
|
||||||
"videoUploadFailed": "Échec du téléchargement de la vidéo",
|
"videoUploadFailed": "Échec du téléchargement de la vidéo",
|
||||||
"subtitleUploadFailed": "Échec du téléchargement des sous-titres"
|
"subtitleUploadFailed": "Échec du téléchargement des sous-titres",
|
||||||
|
"subtitleLoadSuccess": "Sous-titres chargés avec succès",
|
||||||
|
"subtitleLoadFailed": "Échec du chargement des sous-titres"
|
||||||
},
|
},
|
||||||
"text_speaker": {
|
"text_speaker": {
|
||||||
"generateIPA": "Générer l'API",
|
"generateIPA": "Générer l'API",
|
||||||
"viewSavedItems": "Voir les éléments enregistrés",
|
"viewSavedItems": "Voir les éléments enregistrés",
|
||||||
"confirmDeleteAll": "Êtes-vous sûr de vouloir tout supprimer? (O/N)"
|
"confirmDeleteAll": "Êtes-vous sûr de vouloir tout supprimer ? (O/N)"
|
||||||
},
|
},
|
||||||
"translator": {
|
"translator": {
|
||||||
"detectLanguage": "détecter la langue",
|
"detectLanguage": "détecter la langue",
|
||||||
@@ -200,45 +256,84 @@
|
|||||||
"translate": "traduire",
|
"translate": "traduire",
|
||||||
"inputLanguage": "Entrez une langue.",
|
"inputLanguage": "Entrez une langue.",
|
||||||
"history": "Historique",
|
"history": "Historique",
|
||||||
"enterLanguage": "Entrer la langue",
|
"enterLanguage": "Entrez la langue",
|
||||||
"add_to_folder": {
|
"add_to_folder": {
|
||||||
"notAuthenticated": "Vous n'êtes pas authentifié",
|
"notAuthenticated": "Vous n'êtes pas authentifié",
|
||||||
"chooseFolder": "Choisir un dossier à ajouter",
|
"chooseFolder": "Choisissez un dossier à ajouter",
|
||||||
"noFolders": "Aucun dossier trouvé",
|
"noFolders": "Aucun dossier trouvé",
|
||||||
"folderInfo": "{id}. {name}",
|
"folderInfo": "{id}. {name}",
|
||||||
"close": "Fermer",
|
"close": "Fermer",
|
||||||
"success": "Paire de textes ajoutée au dossier",
|
"success": "Paire de texte ajoutée au dossier",
|
||||||
"error": "Échec de l'ajout de la paire de textes au dossier"
|
"error": "Échec de l'ajout de la paire de texte au dossier"
|
||||||
},
|
},
|
||||||
"autoSave": "Sauvegarde automatique"
|
"autoSave": "Sauvegarde automatique"
|
||||||
},
|
},
|
||||||
"dictionary": {
|
"dictionary": {
|
||||||
"title": "Dictionnaire",
|
"title": "Dictionnaire",
|
||||||
"description": "Rechercher des mots et des phrases avec des définitions détaillées et des exemples",
|
"description": "Rechercher des mots et des expressions avec des définitions détaillées et des exemples",
|
||||||
"searchPlaceholder": "Entrez un mot ou une phrase à rechercher...",
|
"searchPlaceholder": "Entrez un mot ou une expression à rechercher...",
|
||||||
"searching": "Recherche...",
|
"searching": "Recherche...",
|
||||||
"search": "Rechercher",
|
"search": "Rechercher",
|
||||||
"languageSettings": "Paramètres linguistiques",
|
"languageSettings": "Paramètres de langue",
|
||||||
"queryLanguage": "Langue de requête",
|
"queryLanguage": "Langue de requête",
|
||||||
"queryLanguageHint": "Quelle est la langue du mot/phrase que vous souhaitez rechercher",
|
"queryLanguageHint": "Dans quelle langue est le mot/l'expression que vous voulez rechercher",
|
||||||
"definitionLanguage": "Langue de définition",
|
"definitionLanguage": "Langue de définition",
|
||||||
"definitionLanguageHint": "Dans quelle langue souhaitez-vous voir les définitions",
|
"definitionLanguageHint": "Dans quelle langue voulez-vous les définitions",
|
||||||
"otherLanguagePlaceholder": "Ou entrez une autre langue...",
|
"otherLanguagePlaceholder": "Ou entrez une autre langue...",
|
||||||
|
"other": "Autre",
|
||||||
"currentSettings": "Paramètres actuels : Requête {queryLang}, Définition {definitionLang}",
|
"currentSettings": "Paramètres actuels : Requête {queryLang}, Définition {definitionLang}",
|
||||||
"relookup": "Rechercher à nouveau",
|
"relookup": "Rechercher à nouveau",
|
||||||
"saveToFolder": "Enregistrer dans le dossier",
|
"saveToFolder": "Enregistrer dans le dossier",
|
||||||
"loading": "Chargement...",
|
"loading": "Chargement...",
|
||||||
"noResults": "Aucun résultat trouvé",
|
"noResults": "Aucun résultat trouvé",
|
||||||
"tryOtherWords": "Essayez d'autres mots ou phrases",
|
"tryOtherWords": "Essayez d'autres mots ou expressions",
|
||||||
"welcomeTitle": "Bienvenue dans le dictionnaire",
|
"welcomeTitle": "Bienvenue dans le dictionnaire",
|
||||||
"welcomeHint": "Entrez un mot ou une phrase dans la zone de recherche ci-dessus pour commencer",
|
"welcomeHint": "Entrez un mot ou une expression dans la zone de recherche ci-dessus pour commencer la recherche",
|
||||||
"lookupFailed": "Recherche échouée, veuillez réessayer plus tard",
|
"lookupFailed": "La recherche a échoué, veuillez réessayer plus tard",
|
||||||
"relookupSuccess": "Recherche répétée avec succès",
|
"relookupSuccess": "Recherche effectuée avec succès",
|
||||||
"relookupFailed": "Nouvelle recherche de dictionnaire échouée",
|
"relookupFailed": "La nouvelle recherche dans le dictionnaire a échoué",
|
||||||
"pleaseLogin": "Veuillez d'abord vous connecter",
|
"pleaseLogin": "Veuillez vous connecter d'abord",
|
||||||
"pleaseCreateFolder": "Veuillez d'abord créer un dossier",
|
"pleaseCreateFolder": "Veuillez créer un dossier d'abord",
|
||||||
"savedToFolder": "Enregistré dans le dossier : {folderName}",
|
"savedToFolder": "Enregistré dans le dossier : {folderName}",
|
||||||
"saveFailed": "Échec de l'enregistrement, veuillez réessayer plus tard"
|
"saveFailed": "Échec de l'enregistrement, veuillez réessayer plus tard",
|
||||||
|
"definition": "Définition",
|
||||||
|
"example": "Exemple"
|
||||||
|
},
|
||||||
|
"explore": {
|
||||||
|
"title": "Explorer",
|
||||||
|
"subtitle": "Découvrir les dossiers publics",
|
||||||
|
"searchPlaceholder": "Rechercher des dossiers publics...",
|
||||||
|
"loading": "Chargement...",
|
||||||
|
"noFolders": "Aucun dossier public trouvé",
|
||||||
|
"folderInfo": "{userName} • {totalPairs} paires",
|
||||||
|
"unknownUser": "Utilisateur inconnu",
|
||||||
|
"favorite": "Favori",
|
||||||
|
"unfavorite": "Retirer des favoris",
|
||||||
|
"pleaseLogin": "Veuillez vous connecter d'abord",
|
||||||
|
"sortByFavorites": "Trier par favoris",
|
||||||
|
"sortByFavoritesActive": "Annuler le tri par favoris"
|
||||||
|
},
|
||||||
|
"exploreDetail": {
|
||||||
|
"title": "Détails du dossier",
|
||||||
|
"createdBy": "Créé par : {name}",
|
||||||
|
"unknownUser": "Utilisateur inconnu",
|
||||||
|
"totalPairs": "Total des paires",
|
||||||
|
"favorites": "Favoris",
|
||||||
|
"createdAt": "Créé le",
|
||||||
|
"viewContent": "Voir le contenu",
|
||||||
|
"favorite": "Favori",
|
||||||
|
"unfavorite": "Retirer des favoris",
|
||||||
|
"favorited": "Ajouté aux favoris",
|
||||||
|
"unfavorited": "Retiré des favoris",
|
||||||
|
"pleaseLogin": "Veuillez vous connecter d'abord"
|
||||||
|
},
|
||||||
|
"favorites": {
|
||||||
|
"title": "Mes favoris",
|
||||||
|
"subtitle": "Les dossiers que vous avez mis en favoris",
|
||||||
|
"loading": "Chargement...",
|
||||||
|
"noFavorites": "Pas encore de favoris",
|
||||||
|
"folderInfo": "{userName} • {totalPairs} paires",
|
||||||
|
"unknownUser": "Utilisateur inconnu"
|
||||||
},
|
},
|
||||||
"user_profile": {
|
"user_profile": {
|
||||||
"anonymous": "Anonyme",
|
"anonymous": "Anonyme",
|
||||||
@@ -251,11 +346,12 @@
|
|||||||
"displayName": "Nom d'affichage",
|
"displayName": "Nom d'affichage",
|
||||||
"notSet": "Non défini",
|
"notSet": "Non défini",
|
||||||
"memberSince": "Membre depuis",
|
"memberSince": "Membre depuis",
|
||||||
|
"logout": "Déconnexion",
|
||||||
"folders": {
|
"folders": {
|
||||||
"title": "Dossiers",
|
"title": "Dossiers",
|
||||||
"noFolders": "Aucun dossier pour le moment",
|
"noFolders": "Pas encore de dossiers",
|
||||||
"folderName": "Nom du dossier",
|
"folderName": "Nom du dossier",
|
||||||
"totalPairs": "Nombre de paires",
|
"totalPairs": "Total des paires",
|
||||||
"createdAt": "Créé le",
|
"createdAt": "Créé le",
|
||||||
"actions": "Actions",
|
"actions": "Actions",
|
||||||
"view": "Voir"
|
"view": "Voir"
|
||||||
|
|||||||
@@ -1,48 +1,68 @@
|
|||||||
{
|
{
|
||||||
"alphabet": {
|
"alphabet": {
|
||||||
"chooseCharacters": "Seleziona i caratteri che desideri imparare",
|
"chooseCharacters": "Seleziona i caratteri che vuoi imparare",
|
||||||
"japanese": "Kana giapponese",
|
"chooseAlphabetHint": "Seleziona un alfabeto per iniziare a imparare",
|
||||||
"english": "Alfabeto inglese",
|
"japanese": "Kana Giapponese",
|
||||||
"uyghur": "Alfabeto uiguro",
|
"english": "Alfabeto Inglese",
|
||||||
"esperanto": "Alfabeto esperanto",
|
"uyghur": "Alfabeto Uiguro",
|
||||||
|
"esperanto": "Alfabeto Esperanto",
|
||||||
"loading": "Caricamento...",
|
"loading": "Caricamento...",
|
||||||
"loadFailed": "Caricamento fallito, riprova",
|
"loadFailed": "Caricamento fallito, riprova",
|
||||||
"hideLetter": "Nascondi lettera",
|
"hideLetter": "Nascondi Lettera",
|
||||||
"showLetter": "Mostra lettera",
|
"showLetter": "Mostra Lettera",
|
||||||
"hideIPA": "Nascondi IPA",
|
"hideIPA": "Nascondi IPA",
|
||||||
"showIPA": "Mostra IPA",
|
"showIPA": "Mostra IPA",
|
||||||
"roman": "Romanizzazione",
|
"roman": "Romanizzazione",
|
||||||
"letter": "Lettera",
|
"letter": "Lettera",
|
||||||
"random": "Modalità casuale",
|
"random": "Modalità Casuale",
|
||||||
"randomNext": "Successivo casuale"
|
"randomNext": "Prossimo Casuale",
|
||||||
|
"previousLetter": "Lettera precedente",
|
||||||
|
"nextLetter": "Lettera successiva",
|
||||||
|
"keyboardHint": "Usa le frecce sinistra/destra o spazio per casuale, ESC per tornare indietro",
|
||||||
|
"swipeHint": "Usa le frecce sinistra/destra o scorri per navigare, ESC per tornare indietro"
|
||||||
},
|
},
|
||||||
"folders": {
|
"folders": {
|
||||||
"title": "Cartelle",
|
"title": "Cartelle",
|
||||||
"subtitle": "Gestisci le tue collezioni",
|
"subtitle": "Gestisci le tue collezioni",
|
||||||
"newFolder": "Nuova cartella",
|
"newFolder": "Nuova Cartella",
|
||||||
"creating": "Creazione...",
|
"creating": "Creazione...",
|
||||||
"noFoldersYet": "Nessuna cartella ancora",
|
"noFoldersYet": "Nessuna cartella ancora",
|
||||||
"folderInfo": "ID: {id} • {totalPairs} coppie",
|
"folderInfo": "ID: {id} • {totalPairs} coppie",
|
||||||
"enterFolderName": "Inserisci nome cartella:",
|
"enterFolderName": "Inserisci il nome della cartella:",
|
||||||
"confirmDelete": "Digita \"{name}\" per eliminare:"
|
"confirmDelete": "Digita \"{name}\" per eliminare:",
|
||||||
|
"myFolders": "Le Mie Cartelle",
|
||||||
|
"publicFolders": "Cartelle Pubbliche",
|
||||||
|
"public": "Pubblica",
|
||||||
|
"private": "Privata",
|
||||||
|
"setPublic": "Imposta Pubblica",
|
||||||
|
"setPrivate": "Imposta Privata",
|
||||||
|
"publicFolderInfo": "{userName} • {totalPairs} coppie",
|
||||||
|
"searchPlaceholder": "Cerca cartelle pubbliche...",
|
||||||
|
"loading": "Caricamento...",
|
||||||
|
"noPublicFolders": "Nessuna cartella pubblica trovata",
|
||||||
|
"unknownUser": "Utente Sconosciuto",
|
||||||
|
"enterNewName": "Inserisci nuovo nome:",
|
||||||
|
"favorite": "Preferito",
|
||||||
|
"unfavorite": "Rimuovi dai preferiti",
|
||||||
|
"pleaseLogin": "Per favore accedi prima"
|
||||||
},
|
},
|
||||||
"folder_id": {
|
"folder_id": {
|
||||||
"unauthorized": "Non sei il proprietario di questa cartella",
|
"unauthorized": "Non sei il proprietario di questa cartella",
|
||||||
"back": "Indietro",
|
"back": "Indietro",
|
||||||
"textPairs": "Coppie di testi",
|
"textPairs": "Coppie di Testo",
|
||||||
"itemsCount": "{count} elementi",
|
"itemsCount": "{count} elementi",
|
||||||
"memorize": "Memorizza",
|
"memorize": "Memorizza",
|
||||||
"loadingTextPairs": "Caricamento coppie di testi...",
|
"loadingTextPairs": "Caricamento coppie di testo...",
|
||||||
"noTextPairs": "Nessuna coppia di testi in questa cartella",
|
"noTextPairs": "Nessuna coppia di testo in questa cartella",
|
||||||
"addNewTextPair": "Aggiungi nuova coppia di testi",
|
"addNewTextPair": "Aggiungi Nuova Coppia di Testo",
|
||||||
"add": "Aggiungi",
|
"add": "Aggiungi",
|
||||||
"updateTextPair": "Aggiorna coppia di testi",
|
"updateTextPair": "Aggiorna Coppia di Testo",
|
||||||
"update": "Aggiorna",
|
"update": "Aggiorna",
|
||||||
"text1": "Testo 1",
|
"text1": "Testo 1",
|
||||||
"text2": "Testo 2",
|
"text2": "Testo 2",
|
||||||
"language1": "Lingua 1",
|
"language1": "Locale 1",
|
||||||
"language2": "Lingua 2",
|
"language2": "Locale 2",
|
||||||
"enterLanguageName": "Inserisci il nome della lingua",
|
"enterLanguageName": "Per favore inserisci il nome della lingua",
|
||||||
"edit": "Modifica",
|
"edit": "Modifica",
|
||||||
"delete": "Elimina",
|
"delete": "Elimina",
|
||||||
"permissionDenied": "Non hai il permesso di eseguire questa azione",
|
"permissionDenied": "Non hai il permesso di eseguire questa azione",
|
||||||
@@ -55,8 +75,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"title": "Impara le lingue",
|
"title": "Impara le Lingue",
|
||||||
"description": "Questo è un sito web molto utile che ti aiuta a imparare quasi tutte le lingue del mondo, incluse quelle costruite.",
|
"description": "Ecco un sito molto utile per aiutarti a imparare quasi tutte le lingue del mondo, incluse quelle costruite.",
|
||||||
"explore": "Esplora",
|
"explore": "Esplora",
|
||||||
"fortune": {
|
"fortune": {
|
||||||
"quote": "Stay hungry, stay foolish.",
|
"quote": "Stay hungry, stay foolish.",
|
||||||
@@ -64,15 +84,15 @@
|
|||||||
},
|
},
|
||||||
"translator": {
|
"translator": {
|
||||||
"name": "Traduttore",
|
"name": "Traduttore",
|
||||||
"description": "Traduci in qualsiasi lingua e annota con l'alfabeto fonetico internazionale (IPA)"
|
"description": "Traduci in qualsiasi lingua e annota con l'Alfabeto Fonetico Internazionale (IPA)"
|
||||||
},
|
},
|
||||||
"textSpeaker": {
|
"textSpeaker": {
|
||||||
"name": "Lettore di testo",
|
"name": "Lettore Testo",
|
||||||
"description": "Riconosce e legge il testo ad alta voce, supporta la riproduzione in loop e la regolazione della velocità"
|
"description": "Riconosci e leggi il testo ad alta voce, supporta riproduzione in loop e regolazione della velocità"
|
||||||
},
|
},
|
||||||
"srtPlayer": {
|
"srtPlayer": {
|
||||||
"name": "Lettore video SRT",
|
"name": "Lettore Video SRT",
|
||||||
"description": "Riproduci video frase per frase basandoti su file di sottotitoli SRT per imitare la pronuncia dei madrelingua"
|
"description": "Riproduci video frase per frase basandoti sui file di sottotitoli SRT per imitare la pronuncia dei madrelingua"
|
||||||
},
|
},
|
||||||
"alphabet": {
|
"alphabet": {
|
||||||
"name": "Alfabeto",
|
"name": "Alfabeto",
|
||||||
@@ -80,45 +100,73 @@
|
|||||||
},
|
},
|
||||||
"memorize": {
|
"memorize": {
|
||||||
"name": "Memorizza",
|
"name": "Memorizza",
|
||||||
"description": "Lingua A verso lingua B, lingua B verso lingua A, supporta dettatura"
|
"description": "Lingua A a Lingua B, Lingua B a Lingua A, supporta dettatura"
|
||||||
},
|
},
|
||||||
"dictionary": {
|
"dictionary": {
|
||||||
"name": "Dizionario",
|
"name": "Dizionario",
|
||||||
"description": "Cerca parole e frasi con definizioni dettagliate ed esempi"
|
"description": "Cerca parole e frasi con definizioni dettagliate ed esempi"
|
||||||
},
|
},
|
||||||
"moreFeatures": {
|
"moreFeatures": {
|
||||||
"name": "Altre funzionalità",
|
"name": "Altre Funzionalità",
|
||||||
"description": "In sviluppo, rimani sintonizzato"
|
"description": "In sviluppo, resta sintonizzato"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"title": "Autenticazione",
|
"title": "Accedi",
|
||||||
|
"signUpTitle": "Registrati",
|
||||||
"signIn": "Accedi",
|
"signIn": "Accedi",
|
||||||
"signUp": "Registrati",
|
"signUp": "Registrati",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"confirmPassword": "Conferma password",
|
"confirmPassword": "Conferma Password",
|
||||||
"name": "Nome",
|
"name": "Nome",
|
||||||
"username": "Nome utente",
|
"username": "Nome Utente",
|
||||||
"emailOrUsername": "Email o nome utente",
|
"emailOrUsername": "Email o Nome Utente",
|
||||||
"signInButton": "Accedi",
|
"signInButton": "Accedi",
|
||||||
"signUpButton": "Registrati",
|
"signUpButton": "Registrati",
|
||||||
"noAccount": "Non hai un account?",
|
"noAccount": "Non hai un account?",
|
||||||
"hasAccount": "Hai già un account?",
|
"hasAccount": "Hai già un account?",
|
||||||
"signInWithGitHub": "Accedi con GitHub",
|
"signInWithGitHub": "Accedi con GitHub",
|
||||||
"signUpWithGitHub": "Registrati con GitHub",
|
"signUpWithGitHub": "Registrati con GitHub",
|
||||||
"invalidEmail": "Inserisci un indirizzo email valido",
|
"invalidEmail": "Per favore inserisci un indirizzo email valido",
|
||||||
"passwordTooShort": "La password deve essere di almeno 8 caratteri",
|
"passwordTooShort": "La password deve essere di almeno 8 caratteri",
|
||||||
"passwordsNotMatch": "Le password non corrispondono",
|
"passwordsNotMatch": "Le password non corrispondono",
|
||||||
"nameRequired": "Inserisci il tuo nome",
|
"nameRequired": "Per favore inserisci il tuo nome",
|
||||||
"usernameRequired": "Inserisci il tuo nome utente",
|
"usernameRequired": "Per favore inserisci un nome utente",
|
||||||
"usernameTooShort": "Il nome utente deve essere di almeno 3 caratteri",
|
"usernameTooShort": "Il nome utente deve essere di almeno 3 caratteri",
|
||||||
"usernameInvalid": "Il nome utente può contenere solo lettere, numeri e underscore",
|
"usernameInvalid": "Il nome utente può contenere solo lettere, numeri e trattini bassi",
|
||||||
"emailRequired": "Inserisci la tua email",
|
"emailRequired": "Per favore inserisci la tua email",
|
||||||
"identifierRequired": "Inserisci la tua email o nome utente",
|
"identifierRequired": "Per favore inserisci la tua email o nome utente",
|
||||||
"passwordRequired": "Inserisci la tua password",
|
"passwordRequired": "Per favore inserisci la tua password",
|
||||||
"confirmPasswordRequired": "Conferma la tua password",
|
"confirmPasswordRequired": "Per favore conferma la tua password",
|
||||||
"loading": "Caricamento..."
|
"loading": "Caricamento...",
|
||||||
|
"confirm": "Conferma",
|
||||||
|
"noAccountLink": "Non hai un account? Registrati",
|
||||||
|
"hasAccountLink": "Hai già un account? Accedi",
|
||||||
|
"usernamePlaceholder": "Nome utente",
|
||||||
|
"emailPlaceholder": "Indirizzo email",
|
||||||
|
"passwordPlaceholder": "Password",
|
||||||
|
"usernameOrEmailPlaceholder": "Nome utente o email",
|
||||||
|
"loginFailed": "Accesso fallito",
|
||||||
|
"signUpFailed": "Registrazione fallita",
|
||||||
|
"fillAllFields": "Per favore compila tutti i campi",
|
||||||
|
"enterCredentials": "Per favore inserisci nome utente e password",
|
||||||
|
"forgotPassword": "Password Dimenticata",
|
||||||
|
"forgotPasswordHint": "Inserisci il tuo indirizzo email e ti invieremo un link per reimpostare la password.",
|
||||||
|
"sendResetEmail": "Invia Email di Reset",
|
||||||
|
"resetPasswordFailed": "Impossibile inviare email di reset",
|
||||||
|
"resetPasswordEmailSent": "Email di reset inviata con successo",
|
||||||
|
"resetPasswordEmailSentHint": "Abbiamo inviato un link per reimpostare la password al tuo indirizzo email. Controlla la tua casella di posta.",
|
||||||
|
"checkYourEmail": "Controlla la tua Email",
|
||||||
|
"backToLogin": "Torna al Login",
|
||||||
|
"resetPassword": "Reimposta Password",
|
||||||
|
"newPassword": "Nuova Password",
|
||||||
|
"invalidToken": "Link Non Valido o Scaduto",
|
||||||
|
"invalidTokenHint": "Questo link per reimpostare la password non è valido o è scaduto. Richiedine uno nuovo.",
|
||||||
|
"requestNewToken": "Richiedi Nuovo Link di Reset",
|
||||||
|
"resetPasswordSuccess": "Password reimpostata con successo",
|
||||||
|
"resetPasswordSuccessTitle": "Reimpostazione Password Completata",
|
||||||
|
"resetPasswordSuccessHint": "La tua password è stata reimpostata con successo. Ora puoi accedere con la tua nuova password."
|
||||||
},
|
},
|
||||||
"memorize": {
|
"memorize": {
|
||||||
"folder_selector": {
|
"folder_selector": {
|
||||||
@@ -131,8 +179,8 @@
|
|||||||
"next": "Successivo",
|
"next": "Successivo",
|
||||||
"reverse": "Inverti",
|
"reverse": "Inverti",
|
||||||
"dictation": "Dettatura",
|
"dictation": "Dettatura",
|
||||||
"noTextPairs": "Nessuna coppia di testi disponibile",
|
"noTextPairs": "Nessuna coppia di testo disponibile",
|
||||||
"disorder": "Disordine",
|
"disorder": "Disordina",
|
||||||
"previous": "Precedente"
|
"previous": "Precedente"
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
@@ -140,45 +188,53 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"navbar": {
|
"navbar": {
|
||||||
"title": "learn-languages",
|
"title": "impara-lingue",
|
||||||
"sourceCode": "GitHub",
|
"sourceCode": "GitHub",
|
||||||
"sign_in": "Accedi",
|
"sign_in": "Accedi",
|
||||||
"profile": "Profilo",
|
"profile": "Profilo",
|
||||||
"folders": "Cartelle"
|
"folders": "Cartelle",
|
||||||
|
"explore": "Esplora",
|
||||||
|
"favorites": "Preferiti"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"myProfile": "Il mio profilo",
|
"myProfile": "Il Mio Profilo",
|
||||||
"email": "Email: {email}",
|
"email": "Email: {email}",
|
||||||
"logout": "Esci"
|
"logout": "Esci"
|
||||||
},
|
},
|
||||||
"srt_player": {
|
"srt_player": {
|
||||||
"uploadVideo": "Carica video",
|
"uploadVideo": "Carica Video",
|
||||||
"uploadSubtitle": "Carica sottotitoli",
|
"uploadSubtitle": "Carica Sottotitoli",
|
||||||
"pause": "Pausa",
|
"pause": "Pausa",
|
||||||
"play": "Riproduci",
|
"play": "Riproduci",
|
||||||
"previous": "Precedente",
|
"previous": "Precedente",
|
||||||
"next": "Successivo",
|
"next": "Successivo",
|
||||||
"restart": "Riavvia",
|
"restart": "Riavvia",
|
||||||
"autoPause": "Pausa automatica ({enabled})",
|
"autoPause": "Pausa Automatica ({enabled})",
|
||||||
"uploadVideoAndSubtitle": "Carica i file video e sottotitoli",
|
"uploadVideoAndSubtitle": "Per favore carica file video e sottotitoli",
|
||||||
"uploadVideoFile": "Carica un file video",
|
"uploadVideoFile": "Per favore carica il file video",
|
||||||
"uploadSubtitleFile": "Carica un file di sottotitoli",
|
"uploadSubtitleFile": "Per favore carica il file sottotitoli",
|
||||||
"processingSubtitle": "Elaborazione file sottotitoli...",
|
"processingSubtitle": "Elaborazione file sottotitoli...",
|
||||||
"needBothFiles": "Sono richiesti sia i file video che i sottotitoli per iniziare l'apprendimento",
|
"needBothFiles": "Sono richiesti sia il file video che quello dei sottotitoli per iniziare a imparare",
|
||||||
"videoFile": "File video",
|
"videoFile": "File Video",
|
||||||
"subtitleFile": "File sottotitoli",
|
"subtitleFile": "File Sottotitoli",
|
||||||
"uploaded": "Caricato",
|
"uploaded": "Caricato",
|
||||||
"notUploaded": "Non caricato",
|
"notUploaded": "Non Caricato",
|
||||||
"upload": "Carica",
|
"upload": "Carica",
|
||||||
"autoPauseStatus": "Pausa automatica: {enabled}",
|
"uploadVideoButton": "Carica Video",
|
||||||
|
"uploadSubtitleButton": "Carica Sottotitoli",
|
||||||
|
"subtitleUploaded": "Sottotitoli Caricati ({count} voci)",
|
||||||
|
"subtitleNotUploaded": "Sottotitoli Non Caricati",
|
||||||
|
"autoPauseStatus": "Pausa Automatica: {enabled}",
|
||||||
"on": "Attivo",
|
"on": "Attivo",
|
||||||
"off": "Disattivo",
|
"off": "Disattivo",
|
||||||
"videoUploadFailed": "Caricamento video fallito",
|
"videoUploadFailed": "Caricamento video fallito",
|
||||||
"subtitleUploadFailed": "Caricamento sottotitoli fallito"
|
"subtitleUploadFailed": "Caricamento sottotitoli fallito",
|
||||||
|
"subtitleLoadSuccess": "Sottotitoli caricati con successo",
|
||||||
|
"subtitleLoadFailed": "Caricamento sottotitoli fallito"
|
||||||
},
|
},
|
||||||
"text_speaker": {
|
"text_speaker": {
|
||||||
"generateIPA": "Genera IPA",
|
"generateIPA": "Genera IPA",
|
||||||
"viewSavedItems": "Visualizza elementi salvati",
|
"viewSavedItems": "Visualizza Elementi Salvati",
|
||||||
"confirmDeleteAll": "Sei sicuro di voler eliminare tutto? (S/N)"
|
"confirmDeleteAll": "Sei sicuro di voler eliminare tutto? (S/N)"
|
||||||
},
|
},
|
||||||
"translator": {
|
"translator": {
|
||||||
@@ -203,14 +259,14 @@
|
|||||||
"enterLanguage": "Inserisci lingua",
|
"enterLanguage": "Inserisci lingua",
|
||||||
"add_to_folder": {
|
"add_to_folder": {
|
||||||
"notAuthenticated": "Non sei autenticato",
|
"notAuthenticated": "Non sei autenticato",
|
||||||
"chooseFolder": "Scegli una cartella a cui aggiungere",
|
"chooseFolder": "Scegli una Cartella a cui Aggiungere",
|
||||||
"noFolders": "Nessuna cartella trovata",
|
"noFolders": "Nessuna cartella trovata",
|
||||||
"folderInfo": "{id}. {name}",
|
"folderInfo": "{id}. {name}",
|
||||||
"close": "Chiudi",
|
"close": "Chiudi",
|
||||||
"success": "Coppia di testi aggiunta alla cartella",
|
"success": "Coppia di testo aggiunta alla cartella",
|
||||||
"error": "Impossibile aggiungere la coppia di testi alla cartella"
|
"error": "Impossibile aggiungere coppia di testo alla cartella"
|
||||||
},
|
},
|
||||||
"autoSave": "Salvataggio automatico"
|
"autoSave": "Salvataggio Automatico"
|
||||||
},
|
},
|
||||||
"dictionary": {
|
"dictionary": {
|
||||||
"title": "Dizionario",
|
"title": "Dizionario",
|
||||||
@@ -218,45 +274,85 @@
|
|||||||
"searchPlaceholder": "Inserisci una parola o frase da cercare...",
|
"searchPlaceholder": "Inserisci una parola o frase da cercare...",
|
||||||
"searching": "Ricerca...",
|
"searching": "Ricerca...",
|
||||||
"search": "Cerca",
|
"search": "Cerca",
|
||||||
"languageSettings": "Impostazioni lingua",
|
"languageSettings": "Impostazioni Lingua",
|
||||||
"queryLanguage": "Lingua di interrogazione",
|
"queryLanguage": "Lingua di Query",
|
||||||
"queryLanguageHint": "Quale è la lingua della parola/frase che vuoi cercare",
|
"queryLanguageHint": "In che lingua è la parola/frase che vuoi cercare",
|
||||||
"definitionLanguage": "Lingua di definizione",
|
"definitionLanguage": "Lingua delle Definizioni",
|
||||||
"definitionLanguageHint": "In quale lingua vuoi vedere le definizioni",
|
"definitionLanguageHint": "In che lingua vuoi le definizioni",
|
||||||
"otherLanguagePlaceholder": "Oppure inserisci un'altra lingua...",
|
"otherLanguagePlaceholder": "Oppure inserisci un'altra lingua...",
|
||||||
"currentSettings": "Impostazioni attuali: Interrogazione {queryLang}, Definizione {definitionLang}",
|
"other": "Altro",
|
||||||
|
"currentSettings": "Impostazioni attuali: Query {queryLang}, Definizione {definitionLang}",
|
||||||
"relookup": "Ricerca di nuovo",
|
"relookup": "Ricerca di nuovo",
|
||||||
"saveToFolder": "Salva nella cartella",
|
"saveToFolder": "Salva nella cartella",
|
||||||
"loading": "Caricamento...",
|
"loading": "Caricamento...",
|
||||||
"noResults": "Nessun risultato trovato",
|
"noResults": "Nessun risultato trovato",
|
||||||
"tryOtherWords": "Prova altre parole o frasi",
|
"tryOtherWords": "Prova altre parole o frasi",
|
||||||
"welcomeTitle": "Benvenuto nel dizionario",
|
"welcomeTitle": "Benvenuto nel Dizionario",
|
||||||
"welcomeHint": "Inserisci una parola o frase nella casella di ricerca sopra per iniziare",
|
"welcomeHint": "Inserisci una parola o frase nella casella di ricerca sopra per iniziare a cercare",
|
||||||
"lookupFailed": "Ricerca fallita, riprova più tardi",
|
"lookupFailed": "Ricerca fallita, riprova più tardi",
|
||||||
"relookupSuccess": "Ricerca ripetuta con successo",
|
"relookupSuccess": "Ricerca effettuata con successo",
|
||||||
"relookupFailed": "Nuova ricerca del dizionario fallita",
|
"relookupFailed": "Ricerca dizionario fallita",
|
||||||
"pleaseLogin": "Accedi prima",
|
"pleaseLogin": "Per favore accedi prima",
|
||||||
"pleaseCreateFolder": "Crea prima una cartella",
|
"pleaseCreateFolder": "Per favore crea prima una cartella",
|
||||||
"savedToFolder": "Salvato nella cartella: {folderName}",
|
"savedToFolder": "Salvato nella cartella: {folderName}",
|
||||||
"saveFailed": "Salvataggio fallito, riprova più tardi"
|
"saveFailed": "Salvataggio fallito, riprova più tardi",
|
||||||
|
"definition": "Definizione",
|
||||||
|
"example": "Esempio"
|
||||||
|
},
|
||||||
|
"explore": {
|
||||||
|
"title": "Esplora",
|
||||||
|
"subtitle": "Scopri cartelle pubbliche",
|
||||||
|
"searchPlaceholder": "Cerca cartelle pubbliche...",
|
||||||
|
"loading": "Caricamento...",
|
||||||
|
"noFolders": "Nessuna cartella pubblica trovata",
|
||||||
|
"folderInfo": "{userName} • {totalPairs} coppie",
|
||||||
|
"unknownUser": "Utente Sconosciuto",
|
||||||
|
"favorite": "Preferito",
|
||||||
|
"unfavorite": "Rimuovi dai preferiti",
|
||||||
|
"pleaseLogin": "Per favore accedi prima",
|
||||||
|
"sortByFavorites": "Ordina per preferiti",
|
||||||
|
"sortByFavoritesActive": "Annulla ordinamento per preferiti"
|
||||||
|
},
|
||||||
|
"exploreDetail": {
|
||||||
|
"title": "Dettagli Cartella",
|
||||||
|
"createdBy": "Creata da: {name}",
|
||||||
|
"unknownUser": "Utente Sconosciuto",
|
||||||
|
"totalPairs": "Coppie Totali",
|
||||||
|
"favorites": "Preferiti",
|
||||||
|
"createdAt": "Creata Il",
|
||||||
|
"viewContent": "Visualizza Contenuto",
|
||||||
|
"favorite": "Preferito",
|
||||||
|
"unfavorite": "Rimuovi dai preferiti",
|
||||||
|
"favorited": "Aggiunto ai preferiti",
|
||||||
|
"unfavorited": "Rimosso dai preferiti",
|
||||||
|
"pleaseLogin": "Per favore accedi prima"
|
||||||
|
},
|
||||||
|
"favorites": {
|
||||||
|
"title": "I Miei Preferiti",
|
||||||
|
"subtitle": "Cartelle che hai aggiunto ai preferiti",
|
||||||
|
"loading": "Caricamento...",
|
||||||
|
"noFavorites": "Nessun preferito ancora",
|
||||||
|
"folderInfo": "{userName} • {totalPairs} coppie",
|
||||||
|
"unknownUser": "Utente Sconosciuto"
|
||||||
},
|
},
|
||||||
"user_profile": {
|
"user_profile": {
|
||||||
"anonymous": "Anonimo",
|
"anonymous": "Anonimo",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"verified": "Verificato",
|
"verified": "Verificato",
|
||||||
"unverified": "Non verificato",
|
"unverified": "Non Verificato",
|
||||||
"accountInfo": "Informazioni account",
|
"accountInfo": "Informazioni Account",
|
||||||
"userId": "ID utente",
|
"userId": "ID Utente",
|
||||||
"username": "Nome utente",
|
"username": "Nome Utente",
|
||||||
"displayName": "Nome visualizzato",
|
"displayName": "Nome Visualizzato",
|
||||||
"notSet": "Non impostato",
|
"notSet": "Non Impostato",
|
||||||
"memberSince": "Membro dal",
|
"memberSince": "Membro Dal",
|
||||||
|
"logout": "Esci",
|
||||||
"folders": {
|
"folders": {
|
||||||
"title": "Cartelle",
|
"title": "Cartelle",
|
||||||
"noFolders": "Nessuna cartella ancora",
|
"noFolders": "Nessuna cartella ancora",
|
||||||
"folderName": "Nome cartella",
|
"folderName": "Nome Cartella",
|
||||||
"totalPairs": "Numero di coppie",
|
"totalPairs": "Coppie Totali",
|
||||||
"createdAt": "Creato il",
|
"createdAt": "Creata Il",
|
||||||
"actions": "Azioni",
|
"actions": "Azioni",
|
||||||
"view": "Visualizza"
|
"view": "Visualizza"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
"alphabet": {
|
"alphabet": {
|
||||||
"chooseCharacters": "学習したい文字を選択してください",
|
"chooseCharacters": "学習したい文字を選択してください",
|
||||||
|
"chooseAlphabetHint": "学習を始めるアルファベットを選択してください",
|
||||||
"japanese": "日本語仮名",
|
"japanese": "日本語仮名",
|
||||||
"english": "英語アルファベット",
|
"english": "英語アルファベット",
|
||||||
"uyghur": "ウイグル文字",
|
"uyghur": "ウイグル語アルファベット",
|
||||||
"esperanto": "エスペラント文字",
|
"esperanto": "エスペラント語アルファベット",
|
||||||
"loading": "読み込み中...",
|
"loading": "読み込み中...",
|
||||||
"loadFailed": "読み込みに失敗しました。もう一度お試しください",
|
"loadFailed": "読み込みに失敗しました。もう一度お試しください",
|
||||||
"hideLetter": "文字を非表示",
|
"hideLetter": "文字を非表示",
|
||||||
@@ -14,23 +15,42 @@
|
|||||||
"roman": "ローマ字",
|
"roman": "ローマ字",
|
||||||
"letter": "文字",
|
"letter": "文字",
|
||||||
"random": "ランダムモード",
|
"random": "ランダムモード",
|
||||||
"randomNext": "ランダムで次へ"
|
"randomNext": "ランダム次へ",
|
||||||
|
"previousLetter": "前の文字",
|
||||||
|
"nextLetter": "次の文字",
|
||||||
|
"keyboardHint": "左右の矢印キーまたはスペースキーでランダム移動、ESCで戻る",
|
||||||
|
"swipeHint": "左右の矢印キーまたはスワイプで移動、ESCで戻る"
|
||||||
},
|
},
|
||||||
"folders": {
|
"folders": {
|
||||||
"title": "フォルダー",
|
"title": "フォルダー",
|
||||||
"subtitle": "コレクションを管理",
|
"subtitle": "コレクションを管理",
|
||||||
"newFolder": "新規フォルダー",
|
"newFolder": "新規フォルダー",
|
||||||
"creating": "作成中...",
|
"creating": "作成中...",
|
||||||
"noFoldersYet": "フォルダーがありません",
|
"noFoldersYet": "まだフォルダーがありません",
|
||||||
"folderInfo": "ID: {id} • {totalPairs}組",
|
"folderInfo": "ID: {id} • {totalPairs} ペア",
|
||||||
"enterFolderName": "フォルダー名を入力:",
|
"enterFolderName": "フォルダー名を入力:",
|
||||||
"confirmDelete": "削除するには「{name}」と入力してください:"
|
"confirmDelete": "削除するには「{name}」と入力してください:",
|
||||||
|
"myFolders": "マイフォルダー",
|
||||||
|
"publicFolders": "公開フォルダー",
|
||||||
|
"public": "公開",
|
||||||
|
"private": "非公開",
|
||||||
|
"setPublic": "公開に設定",
|
||||||
|
"setPrivate": "非公開に設定",
|
||||||
|
"publicFolderInfo": "{userName} • {totalPairs} ペア",
|
||||||
|
"searchPlaceholder": "公開フォルダーを検索...",
|
||||||
|
"loading": "読み込み中...",
|
||||||
|
"noPublicFolders": "公開フォルダーが見つかりません",
|
||||||
|
"unknownUser": "不明なユーザー",
|
||||||
|
"enterNewName": "新しい名前を入力:",
|
||||||
|
"favorite": "お気に入り",
|
||||||
|
"unfavorite": "お気に入り解除",
|
||||||
|
"pleaseLogin": "まずログインしてください"
|
||||||
},
|
},
|
||||||
"folder_id": {
|
"folder_id": {
|
||||||
"unauthorized": "あなたはこのフォルダーの所有者ではありません",
|
"unauthorized": "このフォルダーの所有者ではありません",
|
||||||
"back": "戻る",
|
"back": "戻る",
|
||||||
"textPairs": "テキストペア",
|
"textPairs": "テキストペア",
|
||||||
"itemsCount": "{count}項目",
|
"itemsCount": "{count} 項目",
|
||||||
"memorize": "暗記",
|
"memorize": "暗記",
|
||||||
"loadingTextPairs": "テキストペアを読み込み中...",
|
"loadingTextPairs": "テキストペアを読み込み中...",
|
||||||
"noTextPairs": "このフォルダーにはテキストペアがありません",
|
"noTextPairs": "このフォルダーにはテキストペアがありません",
|
||||||
@@ -45,34 +65,34 @@
|
|||||||
"enterLanguageName": "言語名を入力してください",
|
"enterLanguageName": "言語名を入力してください",
|
||||||
"edit": "編集",
|
"edit": "編集",
|
||||||
"delete": "削除",
|
"delete": "削除",
|
||||||
"permissionDenied": "この操作を実行する権限がありません",
|
"permissionDenied": "このアクションを実行する権限がありません",
|
||||||
"error": {
|
"error": {
|
||||||
"update": "この項目を更新する権限がありません。",
|
"update": "この項目を更新する権限がありません。",
|
||||||
"delete": "この項目を削除する権限がありません。",
|
"delete": "この項目を削除する権限がありません。",
|
||||||
"add": "このフォルダーに項目を追加する権限がありません。",
|
"add": "このフォルダーに項目を追加する権限がありません。",
|
||||||
"rename": "このフォルダー名を変更する権限がありません。",
|
"rename": "このフォルダーの名前を変更する権限がありません。",
|
||||||
"deleteFolder": "このフォルダーを削除する権限がありません。"
|
"deleteFolder": "このフォルダーを削除する権限がありません。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"title": "言語を学ぶ",
|
"title": "言語を学ぶ",
|
||||||
"description": "これは、人工言語を含む世界中のほぼすべての言語を学ぶのに役立つ非常に便利なウェブサイトです。",
|
"description": "ここは世界のほぼすべての言語(人工言語を含む)を学ぶのに役立つ非常に便利なウェブサイトです。",
|
||||||
"explore": "探索",
|
"explore": "探索",
|
||||||
"fortune": {
|
"fortune": {
|
||||||
"quote": "Stay hungry, stay foolish.",
|
"quote": "Stay hungry, stay foolish.",
|
||||||
"author": "— スティーブ・ジョブズ"
|
"author": "— Steve Jobs"
|
||||||
},
|
},
|
||||||
"translator": {
|
"translator": {
|
||||||
"name": "翻訳",
|
"name": "翻訳者",
|
||||||
"description": "任意の言語に翻訳し、国際音声記号(IPA)で注釈を付けます"
|
"description": "あらゆる言語に翻訳し、国際音声記号(IPA)で注釈を付けます"
|
||||||
},
|
},
|
||||||
"textSpeaker": {
|
"textSpeaker": {
|
||||||
"name": "テキストスピーカー",
|
"name": "テキストスピーカー",
|
||||||
"description": "テキストを認識して音読します。ループ再生と速度調整をサポート"
|
"description": "テキストを認識して読み上げ、ループ再生と速度調整をサポート"
|
||||||
},
|
},
|
||||||
"srtPlayer": {
|
"srtPlayer": {
|
||||||
"name": "SRTビデオプレーヤー",
|
"name": "SRTビデオプレーヤー",
|
||||||
"description": "SRT字幕ファイルに基づいてビデオを文ごとに再生し、ネイティブスピーカーの発音を模倣します"
|
"description": "SRT字幕ファイルに基づいて文ごとにビデオを再生し、ネイティブスピーカーの発音を模倣"
|
||||||
},
|
},
|
||||||
"alphabet": {
|
"alphabet": {
|
||||||
"name": "アルファベット",
|
"name": "アルファベット",
|
||||||
@@ -80,32 +100,33 @@
|
|||||||
},
|
},
|
||||||
"memorize": {
|
"memorize": {
|
||||||
"name": "暗記",
|
"name": "暗記",
|
||||||
"description": "言語Aから言語B、言語Bから言語A、ディクテーションをサポート"
|
"description": "言語Aから言語B、言語Bから言語A、書き取りをサポート"
|
||||||
},
|
},
|
||||||
"dictionary": {
|
"dictionary": {
|
||||||
"name": "辞書",
|
"name": "辞書",
|
||||||
"description": "単語やフレーズを調べ、詳細な定義と例を表示"
|
"description": "詳細な定義と例文で単語やフレーズを検索"
|
||||||
},
|
},
|
||||||
"moreFeatures": {
|
"moreFeatures": {
|
||||||
"name": "その他の機能",
|
"name": "その他の機能",
|
||||||
"description": "開発中です。お楽しみに"
|
"description": "開発中、お楽しみに"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"title": "認証",
|
"title": "サインイン",
|
||||||
"signIn": "ログイン",
|
"signUpTitle": "新規登録",
|
||||||
|
"signIn": "サインイン",
|
||||||
"signUp": "新規登録",
|
"signUp": "新規登録",
|
||||||
"email": "メールアドレス",
|
"email": "メールアドレス",
|
||||||
"password": "パスワード",
|
"password": "パスワード",
|
||||||
"confirmPassword": "パスワード(確認)",
|
"confirmPassword": "パスワード確認",
|
||||||
"name": "名前",
|
"name": "名前",
|
||||||
"username": "ユーザー名",
|
"username": "ユーザー名",
|
||||||
"emailOrUsername": "メールアドレスまたはユーザー名",
|
"emailOrUsername": "メールアドレスまたはユーザー名",
|
||||||
"signInButton": "ログイン",
|
"signInButton": "サインイン",
|
||||||
"signUpButton": "新規登録",
|
"signUpButton": "新規登録",
|
||||||
"noAccount": "アカウントをお持ちでないですか?",
|
"noAccount": "アカウントをお持ちでないですか?",
|
||||||
"hasAccount": "すでにアカウントをお持ちですか?",
|
"hasAccount": "すでにアカウントをお持ちですか?",
|
||||||
"signInWithGitHub": "GitHubでログイン",
|
"signInWithGitHub": "GitHubでサインイン",
|
||||||
"signUpWithGitHub": "GitHubで新規登録",
|
"signUpWithGitHub": "GitHubで新規登録",
|
||||||
"invalidEmail": "有効なメールアドレスを入力してください",
|
"invalidEmail": "有効なメールアドレスを入力してください",
|
||||||
"passwordTooShort": "パスワードは8文字以上である必要があります",
|
"passwordTooShort": "パスワードは8文字以上である必要があります",
|
||||||
@@ -113,12 +134,39 @@
|
|||||||
"nameRequired": "名前を入力してください",
|
"nameRequired": "名前を入力してください",
|
||||||
"usernameRequired": "ユーザー名を入力してください",
|
"usernameRequired": "ユーザー名を入力してください",
|
||||||
"usernameTooShort": "ユーザー名は3文字以上である必要があります",
|
"usernameTooShort": "ユーザー名は3文字以上である必要があります",
|
||||||
"usernameInvalid": "ユーザー名には英数字とアンダースコアのみ使用できます",
|
"usernameInvalid": "ユーザー名には文字、数字、アンダースコアのみ使用できます",
|
||||||
"emailRequired": "メールアドレスを入力してください",
|
"emailRequired": "メールアドレスを入力してください",
|
||||||
"identifierRequired": "メールアドレスまたはユーザー名を入力してください",
|
"identifierRequired": "メールアドレスまたはユーザー名を入力してください",
|
||||||
"passwordRequired": "パスワードを入力してください",
|
"passwordRequired": "パスワードを入力してください",
|
||||||
"confirmPasswordRequired": "パスワード(確認)を入力してください",
|
"confirmPasswordRequired": "パスワードを確認してください",
|
||||||
"loading": "読み込み中..."
|
"loading": "読み込み中...",
|
||||||
|
"confirm": "確認",
|
||||||
|
"noAccountLink": "アカウントをお持ちでないですか? 新規登録",
|
||||||
|
"hasAccountLink": "すでにアカウントをお持ちですか? サインイン",
|
||||||
|
"usernamePlaceholder": "ユーザー名",
|
||||||
|
"emailPlaceholder": "メールアドレス",
|
||||||
|
"passwordPlaceholder": "パスワード",
|
||||||
|
"usernameOrEmailPlaceholder": "ユーザー名またはメールアドレス",
|
||||||
|
"loginFailed": "ログインに失敗しました",
|
||||||
|
"signUpFailed": "新規登録に失敗しました",
|
||||||
|
"fillAllFields": "すべてのフィールドに入力してください",
|
||||||
|
"enterCredentials": "ユーザー名とパスワードを入力してください",
|
||||||
|
"forgotPassword": "パスワードをお忘れですか",
|
||||||
|
"forgotPasswordHint": "メールアドレスを入力してください。パスワードリセット用のリンクをお送りします。",
|
||||||
|
"sendResetEmail": "リセットメールを送信",
|
||||||
|
"resetPasswordFailed": "リセットメールの送信に失敗しました",
|
||||||
|
"resetPasswordEmailSent": "リセットメールを送信しました",
|
||||||
|
"resetPasswordEmailSentHint": "パスワードリセット用のリンクをメールでお送りしました。受信トレイをご確認ください。",
|
||||||
|
"checkYourEmail": "メールをご確認ください",
|
||||||
|
"backToLogin": "ログインに戻る",
|
||||||
|
"resetPassword": "パスワードをリセット",
|
||||||
|
"newPassword": "新しいパスワード",
|
||||||
|
"invalidToken": "無効または期限切れのリンク",
|
||||||
|
"invalidTokenHint": "このパスワードリセットリンクは無効または期限切れです。新しいものをリクエストしてください。",
|
||||||
|
"requestNewToken": "新しいリセットリンクをリクエスト",
|
||||||
|
"resetPasswordSuccess": "パスワードのリセットに成功しました",
|
||||||
|
"resetPasswordSuccessTitle": "パスワードリセット完了",
|
||||||
|
"resetPasswordSuccessHint": "パスワードが正常にリセットされました。新しいパスワードでログインできます。"
|
||||||
},
|
},
|
||||||
"memorize": {
|
"memorize": {
|
||||||
"folder_selector": {
|
"folder_selector": {
|
||||||
@@ -127,12 +175,12 @@
|
|||||||
"folderInfo": "{id}. {name} ({count})"
|
"folderInfo": "{id}. {name} ({count})"
|
||||||
},
|
},
|
||||||
"memorize": {
|
"memorize": {
|
||||||
"answer": "回答",
|
"answer": "答え",
|
||||||
"next": "次へ",
|
"next": "次へ",
|
||||||
"reverse": "逆順",
|
"reverse": "逆順",
|
||||||
"dictation": "ディクテーション",
|
"dictation": "書き取り",
|
||||||
"noTextPairs": "利用可能なテキストペアがありません",
|
"noTextPairs": "利用可能なテキストペアがありません",
|
||||||
"disorder": "ランダム",
|
"disorder": "シャッフル",
|
||||||
"previous": "前へ"
|
"previous": "前へ"
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
@@ -142,13 +190,15 @@
|
|||||||
"navbar": {
|
"navbar": {
|
||||||
"title": "learn-languages",
|
"title": "learn-languages",
|
||||||
"sourceCode": "GitHub",
|
"sourceCode": "GitHub",
|
||||||
"sign_in": "ログイン",
|
"sign_in": "サインイン",
|
||||||
"profile": "プロフィール",
|
"profile": "プロフィール",
|
||||||
"folders": "フォルダー"
|
"folders": "フォルダー",
|
||||||
|
"explore": "探索",
|
||||||
|
"favorites": "お気に入り"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"myProfile": "マイプロフィール",
|
"myProfile": "マイプロフィール",
|
||||||
"email": "メールアドレス: {email}",
|
"email": "メール: {email}",
|
||||||
"logout": "ログアウト"
|
"logout": "ログアウト"
|
||||||
},
|
},
|
||||||
"srt_player": {
|
"srt_player": {
|
||||||
@@ -170,21 +220,27 @@
|
|||||||
"uploaded": "アップロード済み",
|
"uploaded": "アップロード済み",
|
||||||
"notUploaded": "未アップロード",
|
"notUploaded": "未アップロード",
|
||||||
"upload": "アップロード",
|
"upload": "アップロード",
|
||||||
|
"uploadVideoButton": "ビデオをアップロード",
|
||||||
|
"uploadSubtitleButton": "字幕をアップロード",
|
||||||
|
"subtitleUploaded": "字幕をアップロード済み ({count} エントリ)",
|
||||||
|
"subtitleNotUploaded": "字幕がアップロードされていません",
|
||||||
"autoPauseStatus": "自動一時停止: {enabled}",
|
"autoPauseStatus": "自動一時停止: {enabled}",
|
||||||
"on": "オン",
|
"on": "オン",
|
||||||
"off": "オフ",
|
"off": "オフ",
|
||||||
"videoUploadFailed": "ビデオのアップロードに失敗しました",
|
"videoUploadFailed": "ビデオのアップロードに失敗しました",
|
||||||
"subtitleUploadFailed": "字幕のアップロードに失敗しました"
|
"subtitleUploadFailed": "字幕のアップロードに失敗しました",
|
||||||
|
"subtitleLoadSuccess": "字幕の読み込みに成功しました",
|
||||||
|
"subtitleLoadFailed": "字幕の読み込みに失敗しました"
|
||||||
},
|
},
|
||||||
"text_speaker": {
|
"text_speaker": {
|
||||||
"generateIPA": "IPAを生成",
|
"generateIPA": "IPAを生成",
|
||||||
"viewSavedItems": "保存済みアイテムを表示",
|
"viewSavedItems": "保存済み項目を表示",
|
||||||
"confirmDeleteAll": "本当にすべて削除しますか? (Y/N)"
|
"confirmDeleteAll": "すべて削除してもよろしいですか? (Y/N)"
|
||||||
},
|
},
|
||||||
"translator": {
|
"translator": {
|
||||||
"detectLanguage": "言語を検出",
|
"detectLanguage": "言語を検出",
|
||||||
"generateIPA": "IPAを生成",
|
"generateIPA": "ipaを生成",
|
||||||
"translateInto": "翻訳",
|
"translateInto": "翻訳先",
|
||||||
"chinese": "中国語",
|
"chinese": "中国語",
|
||||||
"english": "英語",
|
"english": "英語",
|
||||||
"french": "フランス語",
|
"french": "フランス語",
|
||||||
@@ -207,38 +263,77 @@
|
|||||||
"noFolders": "フォルダーが見つかりません",
|
"noFolders": "フォルダーが見つかりません",
|
||||||
"folderInfo": "{id}. {name}",
|
"folderInfo": "{id}. {name}",
|
||||||
"close": "閉じる",
|
"close": "閉じる",
|
||||||
"success": "テキストペアをフォルダーに追加しました",
|
"success": "テキストペアがフォルダーに追加されました",
|
||||||
"error": "テキストペアの追加に失敗しました"
|
"error": "テキストペアをフォルダーに追加できませんでした"
|
||||||
},
|
},
|
||||||
"autoSave": "自動保存"
|
"autoSave": "自動保存"
|
||||||
},
|
},
|
||||||
"dictionary": {
|
"dictionary": {
|
||||||
"title": "辞書",
|
"title": "辞書",
|
||||||
"description": "詳細な定義と例で単語やフレーズを検索",
|
"description": "詳細な定義と例文で単語やフレーズを検索",
|
||||||
"searchPlaceholder": "検索する単語やフレーズを入力...",
|
"searchPlaceholder": "検索する単語やフレーズを入力...",
|
||||||
"searching": "検索中...",
|
"searching": "検索中...",
|
||||||
"search": "検索",
|
"search": "検索",
|
||||||
"languageSettings": "言語設定",
|
"languageSettings": "言語設定",
|
||||||
"queryLanguage": "クエリ言語",
|
"queryLanguage": "クエリ言語",
|
||||||
"queryLanguageHint": "検索する単語/フレーズの言語",
|
"queryLanguageHint": "検索したい単語/フレーズの言語",
|
||||||
"definitionLanguage": "定義言語",
|
"definitionLanguage": "定義言語",
|
||||||
"definitionLanguageHint": "定義を表示する言語",
|
"definitionLanguageHint": "定義を表示する言語",
|
||||||
"otherLanguagePlaceholder": "または他の言語を入力...",
|
"otherLanguagePlaceholder": "または別の言語を入力...",
|
||||||
"currentSettings": "現在の設定:クエリ {queryLang}、定義 {definitionLang}",
|
"other": "その他",
|
||||||
|
"currentSettings": "現在の設定: クエリ {queryLang}, 定義 {definitionLang}",
|
||||||
"relookup": "再検索",
|
"relookup": "再検索",
|
||||||
"saveToFolder": "フォルダに保存",
|
"saveToFolder": "フォルダーに保存",
|
||||||
"loading": "読み込み中...",
|
"loading": "読み込み中...",
|
||||||
"noResults": "結果が見つかりません",
|
"noResults": "結果が見つかりません",
|
||||||
"tryOtherWords": "他の単語やフレーズを試してください",
|
"tryOtherWords": "別の単語やフレーズを試してください",
|
||||||
"welcomeTitle": "辞書へようこそ",
|
"welcomeTitle": "辞書へようこそ",
|
||||||
"welcomeHint": "上の検索ボックスに単語やフレーズを入力して検索を開始",
|
"welcomeHint": "上の検索ボックスに単語やフレーズを入力して検索を始めましょう",
|
||||||
"lookupFailed": "検索に失敗しました。後でもう一度お試しください",
|
"lookupFailed": "検索に失敗しました。後でもう一度お試しください",
|
||||||
"relookupSuccess": "再検索しました",
|
"relookupSuccess": "再検索に成功しました",
|
||||||
"relookupFailed": "辞書の再検索に失敗しました",
|
"relookupFailed": "辞書の再検索に失敗しました",
|
||||||
"pleaseLogin": "まずログインしてください",
|
"pleaseLogin": "まずログインしてください",
|
||||||
"pleaseCreateFolder": "まずフォルダを作成してください",
|
"pleaseCreateFolder": "まずフォルダーを作成してください",
|
||||||
"savedToFolder": "フォルダに保存しました:{folderName}",
|
"savedToFolder": "フォルダーに保存しました: {folderName}",
|
||||||
"saveFailed": "保存に失敗しました。後でもう一度お試しください"
|
"saveFailed": "保存に失敗しました。後でもう一度お試しください",
|
||||||
|
"definition": "定義",
|
||||||
|
"example": "例文"
|
||||||
|
},
|
||||||
|
"explore": {
|
||||||
|
"title": "探索",
|
||||||
|
"subtitle": "公開フォルダーを発見",
|
||||||
|
"searchPlaceholder": "公開フォルダーを検索...",
|
||||||
|
"loading": "読み込み中...",
|
||||||
|
"noFolders": "公開フォルダーが見つかりません",
|
||||||
|
"folderInfo": "{userName} • {totalPairs} ペア",
|
||||||
|
"unknownUser": "不明なユーザー",
|
||||||
|
"favorite": "お気に入り",
|
||||||
|
"unfavorite": "お気に入り解除",
|
||||||
|
"pleaseLogin": "まずログインしてください",
|
||||||
|
"sortByFavorites": "お気に入り順に並べ替え",
|
||||||
|
"sortByFavoritesActive": "お気に入り順の並べ替えを解除"
|
||||||
|
},
|
||||||
|
"exploreDetail": {
|
||||||
|
"title": "フォルダー詳細",
|
||||||
|
"createdBy": "作成者: {name}",
|
||||||
|
"unknownUser": "不明なユーザー",
|
||||||
|
"totalPairs": "合計ペア数",
|
||||||
|
"favorites": "お気に入り",
|
||||||
|
"createdAt": "作成日",
|
||||||
|
"viewContent": "コンテンツを表示",
|
||||||
|
"favorite": "お気に入り",
|
||||||
|
"unfavorite": "お気に入り解除",
|
||||||
|
"favorited": "お気に入りに追加しました",
|
||||||
|
"unfavorited": "お気に入りから削除しました",
|
||||||
|
"pleaseLogin": "まずログインしてください"
|
||||||
|
},
|
||||||
|
"favorites": {
|
||||||
|
"title": "マイお気に入り",
|
||||||
|
"subtitle": "お気に入りに追加したフォルダー",
|
||||||
|
"loading": "読み込み中...",
|
||||||
|
"noFavorites": "まだお気に入りがありません",
|
||||||
|
"folderInfo": "{userName} • {totalPairs} ペア",
|
||||||
|
"unknownUser": "不明なユーザー"
|
||||||
},
|
},
|
||||||
"user_profile": {
|
"user_profile": {
|
||||||
"anonymous": "匿名",
|
"anonymous": "匿名",
|
||||||
@@ -251,13 +346,14 @@
|
|||||||
"displayName": "表示名",
|
"displayName": "表示名",
|
||||||
"notSet": "未設定",
|
"notSet": "未設定",
|
||||||
"memberSince": "登録日",
|
"memberSince": "登録日",
|
||||||
|
"logout": "ログアウト",
|
||||||
"folders": {
|
"folders": {
|
||||||
"title": "フォルダー",
|
"title": "フォルダー",
|
||||||
"noFolders": "フォルダーがありません",
|
"noFolders": "まだフォルダーがありません",
|
||||||
"folderName": "フォルダー名",
|
"folderName": "フォルダー名",
|
||||||
"totalPairs": "テキストペア数",
|
"totalPairs": "合計ペア数",
|
||||||
"createdAt": "作成日",
|
"createdAt": "作成日",
|
||||||
"actions": "操作",
|
"actions": "アクション",
|
||||||
"view": "表示"
|
"view": "表示"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
{
|
{
|
||||||
"alphabet": {
|
"alphabet": {
|
||||||
"chooseCharacters": "학습할 문자를 선택하세요",
|
"chooseCharacters": "배우고 싶은 문자를 선택하세요",
|
||||||
|
"chooseAlphabetHint": "학습을 시작할 알파벳을 선택하세요",
|
||||||
"japanese": "일본어 가나",
|
"japanese": "일본어 가나",
|
||||||
"english": "영문 알파벳",
|
"english": "영어 알파벳",
|
||||||
"uyghur": "위구르 문자",
|
"uyghur": "위구르어 알파벳",
|
||||||
"esperanto": "에스페란토 문자",
|
"esperanto": "에스페란토 알파벳",
|
||||||
"loading": "로딩 중...",
|
"loading": "로딩 중...",
|
||||||
"loadFailed": "로딩 실패, 다시 시도해 주세요",
|
"loadFailed": "로딩 실패, 다시 시도해주세요",
|
||||||
"hideLetter": "문자 숨기기",
|
"hideLetter": "문자 숨기기",
|
||||||
"showLetter": "문자 표시",
|
"showLetter": "문자 표시",
|
||||||
"hideIPA": "IPA 숨기기",
|
"hideIPA": "IPA 숨기기",
|
||||||
@@ -14,17 +15,36 @@
|
|||||||
"roman": "로마자 표기",
|
"roman": "로마자 표기",
|
||||||
"letter": "문자",
|
"letter": "문자",
|
||||||
"random": "무작위 모드",
|
"random": "무작위 모드",
|
||||||
"randomNext": "무작위 다음"
|
"randomNext": "무작위 다음",
|
||||||
|
"previousLetter": "이전 문자",
|
||||||
|
"nextLetter": "다음 문자",
|
||||||
|
"keyboardHint": "왼쪽/오른쪽 화살표 키 또는 스페이스바로 무작위, ESC로 뒤로가기",
|
||||||
|
"swipeHint": "왼쪽/오른쪽 화살표 키 또는 스와이프로 탐색, ESC로 뒤로가기"
|
||||||
},
|
},
|
||||||
"folders": {
|
"folders": {
|
||||||
"title": "폴더",
|
"title": "폴더",
|
||||||
"subtitle": "컬렉션 관리",
|
"subtitle": "컬렉션 관리",
|
||||||
"newFolder": "새 폴더",
|
"newFolder": "새 폴더",
|
||||||
"creating": "생성 중...",
|
"creating": "생성 중...",
|
||||||
"noFoldersYet": "폴더가 없습니다",
|
"noFoldersYet": "아직 폴더가 없습니다",
|
||||||
"folderInfo": "ID: {id} • {totalPairs}쌍",
|
"folderInfo": "ID: {id} • {totalPairs} 쌍",
|
||||||
"enterFolderName": "폴더 이름 입력:",
|
"enterFolderName": "폴더 이름 입력:",
|
||||||
"confirmDelete": "삭제하려면 \"{name}\"을(를) 입력하세요:"
|
"confirmDelete": "삭제하려면 \"{name}\"을(를) 입력하세요:",
|
||||||
|
"myFolders": "내 폴더",
|
||||||
|
"publicFolders": "공개 폴더",
|
||||||
|
"public": "공개",
|
||||||
|
"private": "비공개",
|
||||||
|
"setPublic": "공개로 설정",
|
||||||
|
"setPrivate": "비공개로 설정",
|
||||||
|
"publicFolderInfo": "{userName} • {totalPairs} 쌍",
|
||||||
|
"searchPlaceholder": "공개 폴더 검색...",
|
||||||
|
"loading": "로딩 중...",
|
||||||
|
"noPublicFolders": "공개 폴더를 찾을 수 없습니다",
|
||||||
|
"unknownUser": "알 수 없는 사용자",
|
||||||
|
"enterNewName": "새 이름 입력:",
|
||||||
|
"favorite": "즐겨찾기",
|
||||||
|
"unfavorite": "즐겨찾기 해제",
|
||||||
|
"pleaseLogin": "먼저 로그인해주세요"
|
||||||
},
|
},
|
||||||
"folder_id": {
|
"folder_id": {
|
||||||
"unauthorized": "이 폴더의 소유자가 아닙니다",
|
"unauthorized": "이 폴더의 소유자가 아닙니다",
|
||||||
@@ -36,39 +56,39 @@
|
|||||||
"noTextPairs": "이 폴더에 텍스트 쌍이 없습니다",
|
"noTextPairs": "이 폴더에 텍스트 쌍이 없습니다",
|
||||||
"addNewTextPair": "새 텍스트 쌍 추가",
|
"addNewTextPair": "새 텍스트 쌍 추가",
|
||||||
"add": "추가",
|
"add": "추가",
|
||||||
"updateTextPair": "텍스트 쌍 업데이트",
|
"updateTextPair": "텍스트 쌍 수정",
|
||||||
"update": "업데이트",
|
"update": "수정",
|
||||||
"text1": "텍스트 1",
|
"text1": "텍스트 1",
|
||||||
"text2": "텍스트 2",
|
"text2": "텍스트 2",
|
||||||
"language1": "언어 1",
|
"language1": "로캘 1",
|
||||||
"language2": "언어 2",
|
"language2": "로캘 2",
|
||||||
"enterLanguageName": "언어 이름을 입력하세요",
|
"enterLanguageName": "언어 이름을 입력하세요",
|
||||||
"edit": "편집",
|
"edit": "편집",
|
||||||
"delete": "삭제",
|
"delete": "삭제",
|
||||||
"permissionDenied": "이 작업을 수행할 권한이 없습니다",
|
"permissionDenied": "이 작업을 수행할 권한이 없습니다",
|
||||||
"error": {
|
"error": {
|
||||||
"update": "이 항목을 업데이트할 권한이 없습니다.",
|
"update": "이 항목을 수정할 권한이 없습니다.",
|
||||||
"delete": "이 항목을 삭제할 권한이 없습니다.",
|
"delete": "이 항목을 삭제할 권한이 없습니다.",
|
||||||
"add": "이 폴더에 항목을 추가할 권한이 없습니다.",
|
"add": "이 폴더에 항목을 추가할 권한이 없습니다.",
|
||||||
"rename": "이 폴더 이름을 변경할 권한이 없습니다.",
|
"rename": "이 폴더의 이름을 변경할 권한이 없습니다.",
|
||||||
"deleteFolder": "이 폴더를 삭제할 권한이 없습니다."
|
"deleteFolder": "이 폴더를 삭제할 권한이 없습니다."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"title": "언어 학습",
|
"title": "언어 배우기",
|
||||||
"description": "인공 언어를 포함하여 세상의 거의 모든 언어를 배우는 데 도움이 되는 매우 유용한 웹사이트입니다.",
|
"description": "세계의 거의 모든 언어(인공어 포함)를 배우는 데 도움이 되는 매우 유용한 웹사이트입니다.",
|
||||||
"explore": "탐색",
|
"explore": "탐색",
|
||||||
"fortune": {
|
"fortune": {
|
||||||
"quote": "Stay hungry, stay foolish.",
|
"quote": "Stay hungry, stay foolish.",
|
||||||
"author": "— 스티브 잡스"
|
"author": "— Steve Jobs"
|
||||||
},
|
},
|
||||||
"translator": {
|
"translator": {
|
||||||
"name": "번역기",
|
"name": "번역기",
|
||||||
"description": "모든 언어로 번역하고 국제 음성 기호(IPA)로 주석 추가"
|
"description": "모든 언어로 번역하고 국제 음성 기호(IPA)로 주석 달기"
|
||||||
},
|
},
|
||||||
"textSpeaker": {
|
"textSpeaker": {
|
||||||
"name": "텍스트 스피커",
|
"name": "텍스트 스피커",
|
||||||
"description": "텍스트를 인식하고 읽어줍니다. 반복 재생 및 속도 조정 지원"
|
"description": "텍스트 인식 및 낭독, 반복 재생 및 속도 조절 지원"
|
||||||
},
|
},
|
||||||
"srtPlayer": {
|
"srtPlayer": {
|
||||||
"name": "SRT 비디오 플레이어",
|
"name": "SRT 비디오 플레이어",
|
||||||
@@ -84,15 +104,16 @@
|
|||||||
},
|
},
|
||||||
"dictionary": {
|
"dictionary": {
|
||||||
"name": "사전",
|
"name": "사전",
|
||||||
"description": "단어와 구문을 조회하고 자세한 정의와 예제 제공"
|
"description": "상세한 정의와 예문으로 단어 및 구문 검색"
|
||||||
},
|
},
|
||||||
"moreFeatures": {
|
"moreFeatures": {
|
||||||
"name": "더 많은 기능",
|
"name": "더 많은 기능",
|
||||||
"description": "개발 중, 기대해 주세요"
|
"description": "개발 중, 기대해주세요"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"title": "인증",
|
"title": "로그인",
|
||||||
|
"signUpTitle": "회원가입",
|
||||||
"signIn": "로그인",
|
"signIn": "로그인",
|
||||||
"signUp": "회원가입",
|
"signUp": "회원가입",
|
||||||
"email": "이메일",
|
"email": "이메일",
|
||||||
@@ -113,12 +134,39 @@
|
|||||||
"nameRequired": "이름을 입력하세요",
|
"nameRequired": "이름을 입력하세요",
|
||||||
"usernameRequired": "사용자명을 입력하세요",
|
"usernameRequired": "사용자명을 입력하세요",
|
||||||
"usernameTooShort": "사용자명은 최소 3자 이상이어야 합니다",
|
"usernameTooShort": "사용자명은 최소 3자 이상이어야 합니다",
|
||||||
"usernameInvalid": "사용자명은 영문, 숫자, 밑줄만 포함할 수 있습니다",
|
"usernameInvalid": "사용자명은 문자, 숫자, 밑줄만 포함할 수 있습니다",
|
||||||
"emailRequired": "이메일을 입력하세요",
|
"emailRequired": "이메일을 입력하세요",
|
||||||
"identifierRequired": "이메일 또는 사용자명을 입력하세요",
|
"identifierRequired": "이메일 또는 사용자명을 입력하세요",
|
||||||
"passwordRequired": "비밀번호를 입력하세요",
|
"passwordRequired": "비밀번호를 입력하세요",
|
||||||
"confirmPasswordRequired": "비밀번호 확인을 입력하세요",
|
"confirmPasswordRequired": "비밀번호를 확인하세요",
|
||||||
"loading": "로딩 중..."
|
"loading": "로딩 중...",
|
||||||
|
"confirm": "확인",
|
||||||
|
"noAccountLink": "계정이 없으신가요? 회원가입",
|
||||||
|
"hasAccountLink": "이미 계정이 있으신가요? 로그인",
|
||||||
|
"usernamePlaceholder": "사용자명",
|
||||||
|
"emailPlaceholder": "이메일 주소",
|
||||||
|
"passwordPlaceholder": "비밀번호",
|
||||||
|
"usernameOrEmailPlaceholder": "사용자명 또는 이메일",
|
||||||
|
"loginFailed": "로그인 실패",
|
||||||
|
"signUpFailed": "회원가입 실패",
|
||||||
|
"fillAllFields": "모든 필드를 입력하세요",
|
||||||
|
"enterCredentials": "사용자명과 비밀번호를 입력하세요",
|
||||||
|
"forgotPassword": "비밀번호 찾기",
|
||||||
|
"forgotPasswordHint": "이메일 주소를 입력하시면 비밀번호 재설정 링크를 보내드립니다.",
|
||||||
|
"sendResetEmail": "재설정 이메일 보내기",
|
||||||
|
"resetPasswordFailed": "재설정 이메일 전송 실패",
|
||||||
|
"resetPasswordEmailSent": "재설정 이메일이 전송되었습니다",
|
||||||
|
"resetPasswordEmailSentHint": "비밀번호 재설정 링크를 이메일로 보냈습니다. 받은 편지함을 확인해주세요.",
|
||||||
|
"checkYourEmail": "이메일을 확인하세요",
|
||||||
|
"backToLogin": "로그인으로 돌아가기",
|
||||||
|
"resetPassword": "비밀번호 재설정",
|
||||||
|
"newPassword": "새 비밀번호",
|
||||||
|
"invalidToken": "유효하지 않거나 만료된 링크",
|
||||||
|
"invalidTokenHint": "이 비밀번호 재설정 링크는 유효하지 않거나 만료되었습니다. 새로 요청해 주세요.",
|
||||||
|
"requestNewToken": "새 재설정 링크 요청",
|
||||||
|
"resetPasswordSuccess": "비밀번호 재설정 성공",
|
||||||
|
"resetPasswordSuccessTitle": "비밀번호 재설정 완료",
|
||||||
|
"resetPasswordSuccessHint": "비밀번호가 성공적으로 재설정되었습니다. 새 비밀번호로 로그인할 수 있습니다."
|
||||||
},
|
},
|
||||||
"memorize": {
|
"memorize": {
|
||||||
"folder_selector": {
|
"folder_selector": {
|
||||||
@@ -136,7 +184,7 @@
|
|||||||
"previous": "이전"
|
"previous": "이전"
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
"unauthorized": "이 폴더에 액세스할 권한이 없습니다"
|
"unauthorized": "이 폴더에 접근할 권한이 없습니다"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"navbar": {
|
"navbar": {
|
||||||
@@ -144,7 +192,9 @@
|
|||||||
"sourceCode": "GitHub",
|
"sourceCode": "GitHub",
|
||||||
"sign_in": "로그인",
|
"sign_in": "로그인",
|
||||||
"profile": "프로필",
|
"profile": "프로필",
|
||||||
"folders": "폴더"
|
"folders": "폴더",
|
||||||
|
"explore": "탐색",
|
||||||
|
"favorites": "즐겨찾기"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"myProfile": "내 프로필",
|
"myProfile": "내 프로필",
|
||||||
@@ -158,7 +208,7 @@
|
|||||||
"play": "재생",
|
"play": "재생",
|
||||||
"previous": "이전",
|
"previous": "이전",
|
||||||
"next": "다음",
|
"next": "다음",
|
||||||
"restart": "처음부터",
|
"restart": "다시 시작",
|
||||||
"autoPause": "자동 일시정지 ({enabled})",
|
"autoPause": "자동 일시정지 ({enabled})",
|
||||||
"uploadVideoAndSubtitle": "비디오와 자막 파일을 업로드하세요",
|
"uploadVideoAndSubtitle": "비디오와 자막 파일을 업로드하세요",
|
||||||
"uploadVideoFile": "비디오 파일을 업로드하세요",
|
"uploadVideoFile": "비디오 파일을 업로드하세요",
|
||||||
@@ -170,21 +220,27 @@
|
|||||||
"uploaded": "업로드됨",
|
"uploaded": "업로드됨",
|
||||||
"notUploaded": "업로드되지 않음",
|
"notUploaded": "업로드되지 않음",
|
||||||
"upload": "업로드",
|
"upload": "업로드",
|
||||||
|
"uploadVideoButton": "비디오 업로드",
|
||||||
|
"uploadSubtitleButton": "자막 업로드",
|
||||||
|
"subtitleUploaded": "자막 업로드됨 ({count}개 항목)",
|
||||||
|
"subtitleNotUploaded": "자막 업로드되지 않음",
|
||||||
"autoPauseStatus": "자동 일시정지: {enabled}",
|
"autoPauseStatus": "자동 일시정지: {enabled}",
|
||||||
"on": "켜기",
|
"on": "켜기",
|
||||||
"off": "끄기",
|
"off": "끄기",
|
||||||
"videoUploadFailed": "비디오 업로드 실패",
|
"videoUploadFailed": "비디오 업로드 실패",
|
||||||
"subtitleUploadFailed": "자막 업로드 실패"
|
"subtitleUploadFailed": "자막 업로드 실패",
|
||||||
|
"subtitleLoadSuccess": "자막 로드 성공",
|
||||||
|
"subtitleLoadFailed": "자막 로드 실패"
|
||||||
},
|
},
|
||||||
"text_speaker": {
|
"text_speaker": {
|
||||||
"generateIPA": "IPA 생성",
|
"generateIPA": "IPA 생성",
|
||||||
"viewSavedItems": "저장된 항목 보기",
|
"viewSavedItems": "저장된 항목 보기",
|
||||||
"confirmDeleteAll": "정말 모두 삭제하시겠습니까? (Y/N)"
|
"confirmDeleteAll": "모든 것을 삭제하시겠습니까? (Y/N)"
|
||||||
},
|
},
|
||||||
"translator": {
|
"translator": {
|
||||||
"detectLanguage": "언어 감지",
|
"detectLanguage": "언어 감지",
|
||||||
"generateIPA": "IPA 생성",
|
"generateIPA": "IPA 생성",
|
||||||
"translateInto": "번역",
|
"translateInto": "번역할 언어",
|
||||||
"chinese": "중국어",
|
"chinese": "중국어",
|
||||||
"english": "영어",
|
"english": "영어",
|
||||||
"french": "프랑스어",
|
"french": "프랑스어",
|
||||||
@@ -207,38 +263,77 @@
|
|||||||
"noFolders": "폴더를 찾을 수 없습니다",
|
"noFolders": "폴더를 찾을 수 없습니다",
|
||||||
"folderInfo": "{id}. {name}",
|
"folderInfo": "{id}. {name}",
|
||||||
"close": "닫기",
|
"close": "닫기",
|
||||||
"success": "텍스트 쌍을 폴더에 추가했습니다",
|
"success": "텍스트 쌍이 폴더에 추가됨",
|
||||||
"error": "텍스트 쌍 추가 실패"
|
"error": "폴더에 텍스트 쌍 추가 실패"
|
||||||
},
|
},
|
||||||
"autoSave": "자동 저장"
|
"autoSave": "자동 저장"
|
||||||
},
|
},
|
||||||
"dictionary": {
|
"dictionary": {
|
||||||
"title": "사전",
|
"title": "사전",
|
||||||
"description": "상세한 정의와 예제로 단어 및 구문 검색",
|
"description": "상세한 정의와 예문으로 단어 및 구문 검색",
|
||||||
"searchPlaceholder": "검색할 단어나 구문을 입력하세요...",
|
"searchPlaceholder": "검색할 단어나 구문을 입력하세요...",
|
||||||
"searching": "검색 중...",
|
"searching": "검색 중...",
|
||||||
"search": "검색",
|
"search": "검색",
|
||||||
"languageSettings": "언어 설정",
|
"languageSettings": "언어 설정",
|
||||||
"queryLanguage": "쿼리 언어",
|
"queryLanguage": "질의 언어",
|
||||||
"queryLanguageHint": "검색하려는 단어/구문의 언어",
|
"queryLanguageHint": "검색할 단어/구문의 언어",
|
||||||
"definitionLanguage": "정의 언어",
|
"definitionLanguage": "정의 언어",
|
||||||
"definitionLanguageHint": "정의를 표시할 언어",
|
"definitionLanguageHint": "정의를 표시할 언어",
|
||||||
"otherLanguagePlaceholder": "또는 다른 언어를 입력하세요...",
|
"otherLanguagePlaceholder": "또는 다른 언어 입력...",
|
||||||
"currentSettings": "현재 설정: 쿼리 {queryLang}, 정의 {definitionLang}",
|
"other": "기타",
|
||||||
"relookup": "재검색",
|
"currentSettings": "현재 설정: 질의 {queryLang}, 정의 {definitionLang}",
|
||||||
|
"relookup": "다시 검색",
|
||||||
"saveToFolder": "폴더에 저장",
|
"saveToFolder": "폴더에 저장",
|
||||||
"loading": "로드 중...",
|
"loading": "로딩 중...",
|
||||||
"noResults": "결과를 찾을 수 없습니다",
|
"noResults": "검색 결과 없음",
|
||||||
"tryOtherWords": "다른 단어나 구문을 시도하세요",
|
"tryOtherWords": "다른 단어나 구문을 시도하세요",
|
||||||
"welcomeTitle": "사전에 오신 것을 환영합니다",
|
"welcomeTitle": "사전에 오신 것을 환영합니다",
|
||||||
"welcomeHint": "위 검색 상자에 단어나 구문을 입력하여 검색을 시작하세요",
|
"welcomeHint": "위의 검색 상자에 단어나 구문을 입력하여 검색을 시작하세요",
|
||||||
"lookupFailed": "검색 실패, 나중에 다시 시도하세요",
|
"lookupFailed": "검색 실패, 나중에 다시 시도하세요",
|
||||||
"relookupSuccess": "재검색했습니다",
|
"relookupSuccess": "다시 검색 성공",
|
||||||
"relookupFailed": "사전 재검색 실패",
|
"relookupFailed": "사전 다시 검색 실패",
|
||||||
"pleaseLogin": "먼저 로그인하세요",
|
"pleaseLogin": "먼저 로그인하세요",
|
||||||
"pleaseCreateFolder": "먼저 폴더를 만드세요",
|
"pleaseCreateFolder": "먼저 폴더를 생성하세요",
|
||||||
"savedToFolder": "폴더에 저장됨: {folderName}",
|
"savedToFolder": "폴더에 저장됨: {folderName}",
|
||||||
"saveFailed": "저장 실패, 나중에 다시 시도하세요"
|
"saveFailed": "저장 실패, 나중에 다시 시도하세요",
|
||||||
|
"definition": "정의",
|
||||||
|
"example": "예문"
|
||||||
|
},
|
||||||
|
"explore": {
|
||||||
|
"title": "탐색",
|
||||||
|
"subtitle": "공개 폴더 발견",
|
||||||
|
"searchPlaceholder": "공개 폴더 검색...",
|
||||||
|
"loading": "로딩 중...",
|
||||||
|
"noFolders": "공개 폴더를 찾을 수 없습니다",
|
||||||
|
"folderInfo": "{userName} • {totalPairs} 쌍",
|
||||||
|
"unknownUser": "알 수 없는 사용자",
|
||||||
|
"favorite": "즐겨찾기",
|
||||||
|
"unfavorite": "즐겨찾기 해제",
|
||||||
|
"pleaseLogin": "먼저 로그인해주세요",
|
||||||
|
"sortByFavorites": "즐겨찾기순 정렬",
|
||||||
|
"sortByFavoritesActive": "즐겨찾기순 정렬 해제"
|
||||||
|
},
|
||||||
|
"exploreDetail": {
|
||||||
|
"title": "폴더 상세",
|
||||||
|
"createdBy": "생성자: {name}",
|
||||||
|
"unknownUser": "알 수 없는 사용자",
|
||||||
|
"totalPairs": "총 쌍",
|
||||||
|
"favorites": "즐겨찾기",
|
||||||
|
"createdAt": "생성일",
|
||||||
|
"viewContent": "내용 보기",
|
||||||
|
"favorite": "즐겨찾기",
|
||||||
|
"unfavorite": "즐겨찾기 해제",
|
||||||
|
"favorited": "즐겨찾기됨",
|
||||||
|
"unfavorited": "즐겨찾기 해제됨",
|
||||||
|
"pleaseLogin": "먼저 로그인해주세요"
|
||||||
|
},
|
||||||
|
"favorites": {
|
||||||
|
"title": "내 즐겨찾기",
|
||||||
|
"subtitle": "즐겨찾기한 폴더",
|
||||||
|
"loading": "로딩 중...",
|
||||||
|
"noFavorites": "아직 즐겨찾기가 없습니다",
|
||||||
|
"folderInfo": "{userName} • {totalPairs} 쌍",
|
||||||
|
"unknownUser": "알 수 없는 사용자"
|
||||||
},
|
},
|
||||||
"user_profile": {
|
"user_profile": {
|
||||||
"anonymous": "익명",
|
"anonymous": "익명",
|
||||||
@@ -251,11 +346,12 @@
|
|||||||
"displayName": "표시 이름",
|
"displayName": "표시 이름",
|
||||||
"notSet": "설정되지 않음",
|
"notSet": "설정되지 않음",
|
||||||
"memberSince": "가입일",
|
"memberSince": "가입일",
|
||||||
|
"logout": "로그아웃",
|
||||||
"folders": {
|
"folders": {
|
||||||
"title": "폴더",
|
"title": "폴더",
|
||||||
"noFolders": "폴더가 없습니다",
|
"noFolders": "아직 폴더가 없습니다",
|
||||||
"folderName": "폴더 이름",
|
"folderName": "폴더 이름",
|
||||||
"totalPairs": "텍스트 쌍 수",
|
"totalPairs": "총 쌍",
|
||||||
"createdAt": "생성일",
|
"createdAt": "생성일",
|
||||||
"actions": "작업",
|
"actions": "작업",
|
||||||
"view": "보기"
|
"view": "보기"
|
||||||
|
|||||||
@@ -1,128 +1,176 @@
|
|||||||
{
|
{
|
||||||
"alphabet": {
|
"alphabet": {
|
||||||
"chooseCharacters": "ئۆگىنەرلىك ھەرپلەرنى تاللاڭ",
|
"chooseCharacters": "ئۆگەنمەكچى بولغان ھەرپلەرنى تاللاڭ",
|
||||||
"japanese": "ياپونىيە كانا",
|
"chooseAlphabetHint": "ئۆگىنىشنى باشلاش ئۈچۈن بىر ئېلىپبە تاللاڭ",
|
||||||
"english": "ئىنگلىز ئېلىپبې",
|
"japanese": "ياپون يېزىقى",
|
||||||
"uyghur": "ئۇيغۇر ئېلىپبېسى",
|
"english": "ئىنگلىز ئېلىپبەسى",
|
||||||
"esperanto": "ئېسپېرانتو ئېلىپبېسى",
|
"uyghur": "ئۇيغۇر ئېلىپبەسى",
|
||||||
"loading": "چىقىرىۋېتىلىۋاتىدۇ...",
|
"esperanto": "ئېسپېرانتو ئېلىپبەسى",
|
||||||
"loadFailed": "چىقىرىش مەغلۇب بولدى، قايتا سىناڭ",
|
"loading": "يۈكلىنىۋاتىدۇ...",
|
||||||
"hideLetter": "ھەرپنى يوشۇرۇش",
|
"loadFailed": "يۈكلەش مەغلۇپ بولدى، قايتا سىناڭ",
|
||||||
"showLetter": "ھەرپنى كۆرسىتىش",
|
"hideLetter": "ھەرپنى يوشۇر",
|
||||||
"hideIPA": "IPA نى يوشۇرۇش",
|
"showLetter": "ھەرپنى كۆرسەت",
|
||||||
"showIPA": "IPA نى كۆرسىتىش",
|
"hideIPA": "IPA نى يوشۇر",
|
||||||
"roman": "روماللاشتۇرۇش",
|
"showIPA": "IPA نى كۆرسەت",
|
||||||
|
"roman": "لاتىن يېزىقى",
|
||||||
"letter": "ھەرپ",
|
"letter": "ھەرپ",
|
||||||
"random": "ئىختىيارىي ھالەت",
|
"random": "ئىختىيارىي ھالەت",
|
||||||
"randomNext": "ئىختىيارىي كېيىنكى"
|
"randomNext": "ئىختىيارىي كېيىنكى",
|
||||||
|
"previousLetter": "ئالدىنقى ھەرپ",
|
||||||
|
"nextLetter": "كېيىنكى ھەرپ",
|
||||||
|
"keyboardHint": "سول/ئوڭ يا ئوق كۇنۇپكىلىرىنى ياكى بوشلۇق كۇنۇپكىسىنى ئىختىيارىي ئالماشتۇرۇش ئۈچۈن ئىشلىتىڭ، ESC قايتىش ئۈچۈن",
|
||||||
|
"swipeHint": "سول/ئوڭ يا ئوق كۇنۇپكىلىرىنى ياكى سىيرىشنى ئىشلىتىپ يۆنىلىڭ، ESC قايتىش ئۈچۈن"
|
||||||
},
|
},
|
||||||
"folders": {
|
"folders": {
|
||||||
"title": "قىسقۇچلار",
|
"title": "قىسقۇچلار",
|
||||||
"subtitle": "توپلىمىڭىزنى باشقۇرۇڭ",
|
"subtitle": "يىغىپ ساقلاشلىرىڭىزنى باشقۇرۇڭ",
|
||||||
"newFolder": "يېڭى قىسقۇچ",
|
"newFolder": "يېڭى قىسقۇچ",
|
||||||
"creating": "قۇرۇۋاتىدۇ...",
|
"creating": "قۇرۇۋاتىدۇ...",
|
||||||
"noFoldersYet": "قىسقۇچ يوق",
|
"noFoldersYet": "تېخى قىسقۇچ يوق",
|
||||||
"folderInfo": "كود: {id} • {totalPairs} جۈپ",
|
"folderInfo": "كىملىك: {id} • {totalPairs} جۈپ",
|
||||||
"enterFolderName": "قىسقۇچ نامىنى كىرگۈزۈڭ:",
|
"enterFolderName": "قىسقۇچ ئاتىنى كىرگۈزۈڭ:",
|
||||||
"confirmDelete": "ئۆچۈرۈش ئۈچۈن «{name}» نى كىرگۈزۈڭ:"
|
"confirmDelete": "ئۆچۈرۈش ئۈچۈن \"{name}\" نى كىرگۈزۈڭ:",
|
||||||
|
"myFolders": "قىسقۇچلىرىم",
|
||||||
|
"publicFolders": "ئاممىۋى قىسقۇچلار",
|
||||||
|
"public": "ئاممىۋى",
|
||||||
|
"private": "شەخسىي",
|
||||||
|
"setPublic": "ئاممىۋى قىلىپ تەڭشە",
|
||||||
|
"setPrivate": "شەخسىي قىلىپ تەڭشە",
|
||||||
|
"publicFolderInfo": "{userName} • {totalPairs} جۈپ",
|
||||||
|
"searchPlaceholder": "ئاممىۋى قىسقۇچلارنى ئىزدەڭ...",
|
||||||
|
"loading": "يۈكلىنىۋاتىدۇ...",
|
||||||
|
"noPublicFolders": "ئاممىۋى قىسقۇچ تېپىلمىدى",
|
||||||
|
"unknownUser": "نامەلۇم ئىشلەتكۈچى",
|
||||||
|
"enterNewName": "يېڭى ئات كىرگۈزۈڭ:",
|
||||||
|
"favorite": "يىغىپ ساقلا",
|
||||||
|
"unfavorite": "يىغىپ ساقلاشنى بىكار قىل",
|
||||||
|
"pleaseLogin": "ئاۋۋال تىزىمغا كىرىڭ"
|
||||||
},
|
},
|
||||||
"folder_id": {
|
"folder_id": {
|
||||||
"unauthorized": "سىز بۇ قىسقۇچنىڭ ئىگىسى ئەمەس",
|
"unauthorized": "بۇ قىسقۇچنىڭ ئىگىسى ئەمەسسىز",
|
||||||
"back": "كەينىگە",
|
"back": "قايتىش",
|
||||||
"textPairs": "تېكىست جۈپلىرى",
|
"textPairs": "تېكىست جۈپلىرى",
|
||||||
"itemsCount": "{count} تۈر",
|
"itemsCount": "{count} تۈر",
|
||||||
"memorize": "ئەستە ساقلاش",
|
"memorize": "يادلاش",
|
||||||
"loadingTextPairs": "تېكىست جۈپلىرى چىقىرىۋېتىلىۋاتىدۇ...",
|
"loadingTextPairs": "تېكىست جۈپلىرى يۈكلىنىۋاتىدۇ...",
|
||||||
"noTextPairs": "بۇ قىسقۇچتا تېكىست جۈپى يوق",
|
"noTextPairs": "بۇ قىسقۇچتا تېكىست جۈپى يوق",
|
||||||
"addNewTextPair": "يېڭى تېكىست جۈپى قوشۇڭ",
|
"addNewTextPair": "يېڭى تېكىست جۈپى قوشۇش",
|
||||||
"add": "قوشۇش",
|
"add": "قوشۇش",
|
||||||
"updateTextPair": "تېكىست جۈپىنى يېڭىلاڭ",
|
"updateTextPair": "تېكىست جۈپىنى يېڭىلاش",
|
||||||
"update": "يېڭىلاش",
|
"update": "يېڭىلاش",
|
||||||
"text1": "تېكىست 1",
|
"text1": "تېكىست 1",
|
||||||
"text2": "تېكىست 2",
|
"text2": "تېكىست 2",
|
||||||
"language1": "تىل 1",
|
"language1": "تىل 1",
|
||||||
"language2": "تىل 2",
|
"language2": "تىل 2",
|
||||||
"enterLanguageName": "تىل نامىنى كىرگۈزۈڭ",
|
"enterLanguageName": "تىل ئاتىنى كىرگۈزۈڭ",
|
||||||
"edit": "تەھرىرلەش",
|
"edit": "تەھرىرلەش",
|
||||||
"delete": "ئۆچۈرۈش",
|
"delete": "ئۆچۈرۈش",
|
||||||
"permissionDenied": "بۇ مەشغۇلاتنى ئىجرا قىلىش ھوقۇقىڭىز يوق",
|
"permissionDenied": "بۇ مەشغۇلاتنى ئېلىپ بېرىش ھوقۇقىڭىز يوق",
|
||||||
"error": {
|
"error": {
|
||||||
"update": "بۇ تۈرنى يېڭىلاش ھوقۇقىڭىز يوق.",
|
"update": "بۇ تۈرنى يېڭىلاش ھوقۇقىڭىز يوق.",
|
||||||
"delete": "بۇ تۈرنى ئۆچۈرۈش ھوقۇقىڭىز يوق.",
|
"delete": "بۇ تۈرنى ئۆچۈرۈش ھوقۇقىڭىز يوق.",
|
||||||
"add": "بۇ قىسقۇچقا تۈر قوشۇش ھوقۇقىڭىز يوق.",
|
"add": "بۇ قىسقۇچقا تۈر قوشۇش ھوقۇقىڭىز يوق.",
|
||||||
"rename": "بۇ قىسقۇچنىڭ نامىنى ئۆزگەرتىش ھوقۇقىڭىز يوق.",
|
"rename": "بۇ قىسقۇچنىڭ ئاتىنى ئۆزگەرتىش ھوقۇقىڭىز يوق.",
|
||||||
"deleteFolder": "بۇ قىسقۇچنى ئۆچۈرۈش ھوقۇقىڭىز يوق."
|
"deleteFolder": "بۇ قىسقۇچنى ئۆچۈرۈش ھوقۇقىڭىز يوق."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"title": "تىل ئۆگىنىڭ",
|
"title": "تىل ئۆگىنىش",
|
||||||
"description": "بۇ سىزنى دۇنيادىكى ھەممە تىلنى، جۈملىدىن سۈنئىي تىللارنىمۇ ئۆگىنىشىڭىزغا ياردەم بېرىدىغان ناھايىتى پايدىلىق تور بېكەت.",
|
"description": "بۇ دۇنيادىكى almost ھەر بىر تىلنى، جۈملىدىن سۈنئىي تىللارنى ئۆگىنىشىڭىزگە ياردەم بېرىدىغان ئىنتايىن قوللىنىشلىق تور بېكەت.",
|
||||||
"explore": "ئىزدىنىش",
|
"explore": "ئىزدىنىش",
|
||||||
"fortune": {
|
"fortune": {
|
||||||
"quote": "Stay hungry, stay foolish.",
|
"quote": "ئاچ قورساق، ئەخمەق بولۇپ تۇرۇڭ.",
|
||||||
"author": "— ستىۋ جوۋبس"
|
"author": "— Steve Jobs"
|
||||||
},
|
},
|
||||||
"translator": {
|
"translator": {
|
||||||
"name": "تەرجىمە",
|
"name": "تەرجىمان",
|
||||||
"description": "خالىغان تىلغا تەرجىمە قىلىپ خەلقئارالىق فونېتىك ئېلىپبې (IPA) بىلەن ئىزاھاتلاش"
|
"description": "ھەر قانداق تىلغا تەرجىمە قىلىڭ ۋە خەلقئارالىق فونېتىكىلىق ئېلىپبە (IPA) بىلەن ئىزاھلاڭ"
|
||||||
},
|
},
|
||||||
"textSpeaker": {
|
"textSpeaker": {
|
||||||
"name": "تېكىست ئوقۇغۇچى",
|
"name": "تېكىست ئوقۇغۇچى",
|
||||||
"description": "تېكىستنى پەرقلەندۈرۈپ ئوقىيدۇ، دەۋرىي ئوقۇش ۋە سۈرئەت تەڭشەشنى قوللايدۇ"
|
"description": "تېكىستنى تونۇپ ۋە ئۈنلۈك ئوقۇپ بېرىدۇ، دەۋرىي قويۇش ۋە سۈرئەت تەڭشەشنى قوللايدۇ"
|
||||||
},
|
},
|
||||||
"srtPlayer": {
|
"srtPlayer": {
|
||||||
"name": "SRT سىن ئوپىراتورى",
|
"name": "SRT ۋىدېئو قويغۇچ",
|
||||||
"description": "SRT خەت ئاستى فايلى ئاساسىدا سىننى جۈملە-جۈملە قويۇپ، يەرلىك ئىخچام ئاۋازنى ئىمتىلايدۇ"
|
"description": "SRT تر پودكاست ھۆججەتلىرىگە ئاساسەن ۋىدېئولارنى جۈمە بويىچە قويۇپ، ئانا تىللىقلارنىڭ تەلەپپۇزىنى دوراڭ"
|
||||||
},
|
},
|
||||||
"alphabet": {
|
"alphabet": {
|
||||||
"name": "ئېلىپبې",
|
"name": "ئېلىپبە",
|
||||||
"description": "ئېلىپبېدىن يېڭى تىل ئۆگىنىشنى باشلاڭ"
|
"description": "يېڭى بىر تىلنى ئېلىپبەدىن باشلاپ ئۆگىنىڭ"
|
||||||
},
|
},
|
||||||
"memorize": {
|
"memorize": {
|
||||||
"name": "ئەستە ساقلاش",
|
"name": "يادلاش",
|
||||||
"description": "تىل A دىن تىل B غا، تىل B دىن تىل A غا، دىكتات قوللايدۇ"
|
"description": "تىل A دىن تىل B گە، تىل B دىن تىل A غا، دىكتات قىلىشنى قوللايدۇ"
|
||||||
},
|
},
|
||||||
"dictionary": {
|
"dictionary": {
|
||||||
"name": "لۇغەت",
|
"name": "لۇغەت",
|
||||||
"description": "سۆز ۋە ئىبارە ئىزدەپ، تەپسىلىي ئىزاھات ۋە مىساللار بىلەن تەمىنلەيدۇ"
|
"description": "سۆزلەر ۋە ئىبارىلەرنى تەپسىلىي ئېنىقلىما ۋە مىساللار بىلەن ئىزدەڭ"
|
||||||
},
|
},
|
||||||
"moreFeatures": {
|
"moreFeatures": {
|
||||||
"name": "تېخىمۇ كۆپ ئىقتىدار",
|
"name": "تېخىمۇ كۆپ ئىقتىدارلار",
|
||||||
"description": "ئىشلەۋاتىدۇ، كۈتكۈن بولۇڭ"
|
"description": "تەرەققىيات ئاستىدا، دىققەت قىلىپ تۇرۇڭ"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"title": "دەلىللەش",
|
"title": "كىرىش",
|
||||||
|
"signUpTitle": "تىزىملىتىش",
|
||||||
"signIn": "كىرىش",
|
"signIn": "كىرىش",
|
||||||
"signUp": "تىزىملىتىش",
|
"signUp": "تىزىملىتىش",
|
||||||
"email": "ئېلخەت",
|
"email": "ئېلخەت",
|
||||||
"password": "ئىم",
|
"password": "پارول",
|
||||||
"confirmPassword": "ئىمنى جەزملەش",
|
"confirmPassword": "پارولنى جەزىملەڭ",
|
||||||
"name": "نام",
|
"name": "ئىسىم",
|
||||||
"username": "ئىشلەتكۈچى نامى",
|
"username": "ئىشلەتكۈچى ئاتى",
|
||||||
"emailOrUsername": "ئېلخەت ياكى ئىشلەتكۈچى نامى",
|
"emailOrUsername": "ئېلخەت ياكى ئىشلەتكۈچى ئاتى",
|
||||||
"signInButton": "كىرىش",
|
"signInButton": "كىرىش",
|
||||||
"signUpButton": "تىزىملىتىش",
|
"signUpButton": "تىزىملىتىش",
|
||||||
"noAccount": "ھېساباتىڭىز يوقمۇ؟",
|
"noAccount": "ھېساباتىڭىز يوقمۇ؟",
|
||||||
"hasAccount": "ھېساباتىڭىز بارمۇ؟",
|
"hasAccount": "ھېساباتىڭىز بارمۇ؟",
|
||||||
"signInWithGitHub": "GitHub بىلەن كىرىڭ",
|
"signInWithGitHub": "GitHub بىلەن كىرىش",
|
||||||
"signUpWithGitHub": "GitHub بىلەن تىزىملىتىڭ",
|
"signUpWithGitHub": "GitHub بىلەن تىزىملىتىش",
|
||||||
"invalidEmail": "ئىناۋەتلىك ئېلخەت ئادرېسى كىرگۈزۈڭ",
|
"invalidEmail": "ئۈنۈملۈك ئېلخەت ئادرېسى كىرگۈزۈڭ",
|
||||||
"passwordTooShort": "ئىم كەم دېگەندە 8 ھەرپتىن تۇرۇشى كېرەك",
|
"passwordTooShort": "پارول ئەڭ ئاز 8 ھەرپ بولۇشى كېرەك",
|
||||||
"passwordsNotMatch": "ئىم ماس كەلمەيدۇ",
|
"passwordsNotMatch": "پاروللار ماس كەلمەيدۇ",
|
||||||
"nameRequired": "نامىڭىزنى كىرگۈزۈڭ",
|
"nameRequired": "ئىسىمىڭىزنى كىرگۈزۈڭ",
|
||||||
"usernameRequired": "ئىشلەتكۈچى نامىڭىزنى كىرگۈزۈڭ",
|
"usernameRequired": "ئىشلەتكۈچى ئاتىنى كىرگۈزۈڭ",
|
||||||
"usernameTooShort": "ئىشلەتكۈچى نامى كەم دېگەندە 3 ھەرپتىن تۇرۇشى كېرەك",
|
"usernameTooShort": "ئىشلەتكۈچى ئاتى ئەڭ ئاز 3 ھەرپ بولۇشى كېرەك",
|
||||||
"usernameInvalid": "ئىشلەتكۈچى نامى پەقەت ھەرپ، سان ۋە ئاستى سىزىقنى ئۆز ئىچىگە ئالىدۇ",
|
"usernameInvalid": "ئىشلەتكۈچى ئاتى پەقەت ھەرپ، سان ۋە ئاستى سىزىقنى ئۆز ئىچىگە ئالىدۇ",
|
||||||
"emailRequired": "ئېلخىتىڭىزنى كىرگۈزۈڭ",
|
"emailRequired": "ئېلخەت كىرگۈزۈڭ",
|
||||||
"identifierRequired": "ئېلخەت ياكى ئىشلەتكۈچى نامىڭىزنى كىرگۈزۈڭ",
|
"identifierRequired": "ئېلخەت ياكى ئىشلەتكۈچى ئاتىنى كىرگۈزۈڭ",
|
||||||
"passwordRequired": "ئىمىڭىزنى كىرگۈزۈڭ",
|
"passwordRequired": "پارول كىرگۈزۈڭ",
|
||||||
"confirmPasswordRequired": "ئىمىڭىزنى جەزملەڭ",
|
"confirmPasswordRequired": "پارولنى جەزىملەڭ",
|
||||||
"loading": "چىقىرىۋېتىلىۋاتىدۇ..."
|
"loading": "يۈكلىنىۋاتىدۇ...",
|
||||||
|
"confirm": "جەزىملەش",
|
||||||
|
"noAccountLink": "ھېساباتىڭىز يوقمۇ؟ تىزىملىتىڭ",
|
||||||
|
"hasAccountLink": "ھېساباتىڭىز بارمۇ؟ كىرىڭ",
|
||||||
|
"usernamePlaceholder": "ئىشلەتكۈچى ئاتى",
|
||||||
|
"emailPlaceholder": "ئېلخەت ئادرېسى",
|
||||||
|
"passwordPlaceholder": "پارول",
|
||||||
|
"usernameOrEmailPlaceholder": "ئىشلەتكۈچى ئاتى ياكى ئېلخەت",
|
||||||
|
"loginFailed": "كىرىش مەغلۇپ بولدى",
|
||||||
|
"signUpFailed": "تىزىملىتىش مەغلۇپ بولدى",
|
||||||
|
"fillAllFields": "ھەممە بۆلەكلەرنى تولدۇرۇڭ",
|
||||||
|
"enterCredentials": "ئىشلەتكۈچى ئاتى ۋە پارول كىرگۈزۈڭ",
|
||||||
|
"forgotPassword": "پارولنى ئۇنتۇپ قالدىڭىزمۇ",
|
||||||
|
"forgotPasswordHint": "ئېلخەت ئادرېسىڭىزنى كىرگۈزۈڭ، بىز سىزگە پارولنى ئەسلىگە قايتۇرۇش ئۇلانمىسى ئەۋەتىمىز.",
|
||||||
|
"sendResetEmail": "ئەسلىگە قايتۇرۇش ئېلخېتى ئەۋەتىش",
|
||||||
|
"resetPasswordFailed": "ئەسلىگە قايتۇرۇش ئېلخېتى ئەۋەتىش مەغلۇپ بولدى",
|
||||||
|
"resetPasswordEmailSent": "ئەسلىگە قايتۇرۇش ئېلخېتى مۇۋەپپەقىيەتلىك ئەۋەتىلدى",
|
||||||
|
"resetPasswordEmailSentHint": "پارولنى ئەسلىگە قايتۇرۇش ئۇلانمىسىنى ئېلخەت ئادرېسىڭىزغا ئەۋەتتۇق. ئېلخەت ساندۇقىڭىزنى تەكشۈرۈڭ.",
|
||||||
|
"checkYourEmail": "ئېلخېتىڭىزنى تەكشۈرۈڭ",
|
||||||
|
"backToLogin": "كىرىشكە قايتىش",
|
||||||
|
"resetPassword": "پارولنى ئەسلىگە قايتۇرۇش",
|
||||||
|
"newPassword": "يېڭى پارول",
|
||||||
|
"invalidToken": "ئۇلانما ئىناۋەتسىز ياكى ۋاقتى ئۆتكەن",
|
||||||
|
"invalidTokenHint": "بۇ پارولنى ئەسلىگە قايتۇرۇش ئۇلانمىسى ئىناۋەتسىز ياكى ۋاقتى ئۆتكەن. يېڭىدىن سوراڭ.",
|
||||||
|
"requestNewToken": "يېڭى ئەسلىگە قايتۇرۇش ئۇلانمىسى سوراش",
|
||||||
|
"resetPasswordSuccess": "پارول مۇۋەپپەقىيەتلىك ئەسلىگە قايتۇرۇلدى",
|
||||||
|
"resetPasswordSuccessTitle": "پارول ئەسلىگە قايتۇرۇش تاماملاندى",
|
||||||
|
"resetPasswordSuccessHint": "پارولىڭىز مۇۋەپپەقىيەتلىك ئەسلىگە قايتۇرۇلدى. يېڭى پارول بىلەن كىرسىڭىز بولىدۇ."
|
||||||
},
|
},
|
||||||
"memorize": {
|
"memorize": {
|
||||||
"folder_selector": {
|
"folder_selector": {
|
||||||
"selectFolder": "قىسقۇچ تاللاڭ",
|
"selectFolder": "بىر قىسقۇچ تاللاڭ",
|
||||||
"noFolders": "قىسقۇچ تېپىلمىدى",
|
"noFolders": "قىسقۇچ تېپىلمىدى",
|
||||||
"folderInfo": "{id}. {name} ({count})"
|
"folderInfo": "{id}. {name} ({count})"
|
||||||
},
|
},
|
||||||
@@ -131,63 +179,71 @@
|
|||||||
"next": "كېيىنكى",
|
"next": "كېيىنكى",
|
||||||
"reverse": "تەتۈر",
|
"reverse": "تەتۈر",
|
||||||
"dictation": "دىكتات",
|
"dictation": "دىكتات",
|
||||||
"noTextPairs": "ئىشلەتكىلى بولىدىغان تېكىست جۈپى يوق",
|
"noTextPairs": "تېكىست جۈپى يوق",
|
||||||
"disorder": "بەت ئارلاش",
|
"disorder": "قالايمىقانلاشتۇرۇش",
|
||||||
"previous": "ئىلگىرىكى"
|
"previous": "ئالدىنقى"
|
||||||
},
|
},
|
||||||
"page": {
|
"page": {
|
||||||
"unauthorized": "بۇ قىسقۇچنى زىيارەت قىلىشقا ھوقۇقىڭىز يوق"
|
"unauthorized": "بۇ قىسقۇچنى زىيارەت قىلىش ھوقۇقىڭىز يوق"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"navbar": {
|
"navbar": {
|
||||||
"title": "تىل ئۆگىنىش",
|
"title": "تىل-ئۆگىنىش",
|
||||||
"sourceCode": "GitHub",
|
"sourceCode": "GitHub",
|
||||||
"sign_in": "كىرىش",
|
"sign_in": "كىرىش",
|
||||||
"profile": "پروفىل",
|
"profile": "شەخسىي ئۇچۇر",
|
||||||
"folders": "قىسقۇچلار"
|
"folders": "قىسقۇچلار",
|
||||||
|
"explore": "ئىزدىنىش",
|
||||||
|
"favorites": "يىغىپ ساقلانغانلار"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"myProfile": "مېنىڭ پروفىلىم",
|
"myProfile": "شەخسىي ئۇچۇرۇم",
|
||||||
"email": "ئېلخەت: {email}",
|
"email": "ئېلخەت: {email}",
|
||||||
"logout": "چىقىش"
|
"logout": "چىكىنىش"
|
||||||
},
|
},
|
||||||
"srt_player": {
|
"srt_player": {
|
||||||
"uploadVideo": "سىن يۈكلەڭ",
|
"uploadVideo": "ۋىدېئو يۈكلەش",
|
||||||
"uploadSubtitle": "خەت ئاستى يۈكلەڭ",
|
"uploadSubtitle": "تر پودكاست يۈكلەش",
|
||||||
"pause": "ۋاقىتلىق توختىتىش",
|
"pause": "ۋاقىتلىق توختىتىش",
|
||||||
"play": "قويۇش",
|
"play": "قويۇش",
|
||||||
"previous": "ئىلگىرىكى",
|
"previous": "ئالدىنقى",
|
||||||
"next": "كېيىنكى",
|
"next": "كېيىنكى",
|
||||||
"restart": "قايتا باشلاش",
|
"restart": "قايتا باشلاش",
|
||||||
"autoPause": "ئاپتوماتىك توختىتىش ({enabled})",
|
"autoPause": "ئاپتوماتىك توختىتىش ({enabled})",
|
||||||
"uploadVideoAndSubtitle": "سىن ھەم خەت ئاستى فايلىنى يۈكلەڭ",
|
"uploadVideoAndSubtitle": "ۋىدېئو ۋە تر پودكاست ھۆججەتلىرىنى يۈكلەڭ",
|
||||||
"uploadVideoFile": "سىن فايلى يۈكلەڭ",
|
"uploadVideoFile": "ۋىدېئو ھۆججىتى يۈكلەڭ",
|
||||||
"uploadSubtitleFile": "خەت ئاستى فايلى يۈكلەڭ",
|
"uploadSubtitleFile": "تر پودكاست ھۆججىتى يۈكلەڭ",
|
||||||
"processingSubtitle": "خەت ئاستى فايلى بىر تەرەپ قىلىۋاتىدۇ...",
|
"processingSubtitle": "تر پودكاست ھۆججىتى بىر تەرەپ قىلىنىۋاتىدۇ...",
|
||||||
"needBothFiles": "ئۆگىنىشنى باشلاش ئۈچۈن سىن ھەم خەت ئاستى فايلىنىڭ ھەممىسى لازىم",
|
"needBothFiles": "ئۆگىنىشنى باشلاش ئۈچۈن ۋىدېئو ۋە تر پودكاست ھۆججەتلىرى كېرەك",
|
||||||
"videoFile": "سىن فايلى",
|
"videoFile": "ۋىدېئو ھۆججىتى",
|
||||||
"subtitleFile": "خەت ئاستى فايلى",
|
"subtitleFile": "تر پودكاست ھۆججىتى",
|
||||||
"uploaded": "يۈكلەندى",
|
"uploaded": "يۈكلەندى",
|
||||||
"notUploaded": "يۈكلەنمىدى",
|
"notUploaded": "يۈكلەنمىدى",
|
||||||
"upload": "يۈكلەش",
|
"upload": "يۈكلەش",
|
||||||
|
"uploadVideoButton": "ۋىدېئو يۈكلەش",
|
||||||
|
"uploadSubtitleButton": "تر پودكاست يۈكلەش",
|
||||||
|
"subtitleUploaded": "تر پودكاست يۈكلەندى ({count} تۈر)",
|
||||||
|
"subtitleNotUploaded": "تر پودكاست يۈكلەنمىدى",
|
||||||
"autoPauseStatus": "ئاپتوماتىك توختىتىش: {enabled}",
|
"autoPauseStatus": "ئاپتوماتىك توختىتىش: {enabled}",
|
||||||
"on": "ئوچۇق",
|
"on": "ئوچۇق",
|
||||||
"off": "تاقاق",
|
"off": "تاقاق",
|
||||||
"videoUploadFailed": "سىن يۈكلەش مەغلۇب بولدى",
|
"videoUploadFailed": "ۋىدېئو يۈكلەش مەغلۇپ بولدى",
|
||||||
"subtitleUploadFailed": "خەت ئاستى يۈكلەش مەغلۇب بولدى"
|
"subtitleUploadFailed": "تر پودكاست يۈكلەش مەغلۇپ بولدى",
|
||||||
|
"subtitleLoadSuccess": "تر پودكاست مۇۋەپپەقىيەتلىك يۈكلەندى",
|
||||||
|
"subtitleLoadFailed": "تر پودكاست يۈكلەش مەغلۇپ بولدى"
|
||||||
},
|
},
|
||||||
"text_speaker": {
|
"text_speaker": {
|
||||||
"generateIPA": "IPA ھاسىل قىلىش",
|
"generateIPA": "IPA ھاسىل قىلىش",
|
||||||
"viewSavedItems": "ساقلانغان تۈرلەرنى كۆرۈش",
|
"viewSavedItems": "ساقلانغان تۈرلەرنى كۆرۈش",
|
||||||
"confirmDeleteAll": "ھەممىنى ئۆچۈرەمسىز؟ (H/Y)"
|
"confirmDeleteAll": "ھەممىنى ئۆچۈرەمسىز؟ (Y/N)"
|
||||||
},
|
},
|
||||||
"translator": {
|
"translator": {
|
||||||
"detectLanguage": "تىل پەرقلەندۈرۈش",
|
"detectLanguage": "تىلنى تونۇش",
|
||||||
"generateIPA": "IPA ھاسىل قىلىش",
|
"generateIPA": "ipa ھاسىل قىلىش",
|
||||||
"translateInto": "تەرجىمە قىلىش",
|
"translateInto": "تەرجىمە قىلىش",
|
||||||
"chinese": "خەنزۇچە",
|
"chinese": "خەنزۇچە",
|
||||||
"english": "ئىنگلىزچە",
|
"english": "ئىنگلىزچە",
|
||||||
"french": "فرانسۇزچە",
|
"french": "فىرانسۇزچە",
|
||||||
"german": "گېرمانچە",
|
"german": "گېرمانچە",
|
||||||
"italian": "ئىتاليانچە",
|
"italian": "ئىتاليانچە",
|
||||||
"japanese": "ياپونچە",
|
"japanese": "ياپونچە",
|
||||||
@@ -196,68 +252,108 @@
|
|||||||
"russian": "رۇسچە",
|
"russian": "رۇسچە",
|
||||||
"spanish": "ئىسپانچە",
|
"spanish": "ئىسپانچە",
|
||||||
"other": "باشقا",
|
"other": "باشقا",
|
||||||
"translating": "تەرجىمە قىلىۋاتىدۇ...",
|
"translating": "تەرجىمە قىلىنىۋاتىدۇ...",
|
||||||
"translate": "تەرجىمە قىلىش",
|
"translate": "تەرجىمە قىلىش",
|
||||||
"inputLanguage": "بىر تىل كىرگۈزۈڭ.",
|
"inputLanguage": "بىر تىل كىرگۈزۈڭ.",
|
||||||
"history": "تارىخ",
|
"history": "تارىخ",
|
||||||
"enterLanguage": "تىل كىرگۈزۈڭ",
|
"enterLanguage": "تىل كىرگۈزۈڭ",
|
||||||
"add_to_folder": {
|
"add_to_folder": {
|
||||||
"notAuthenticated": "دەلىتلەنمىدىڭىز",
|
"notAuthenticated": "تىزىملىتىلمىدىڭىز",
|
||||||
"chooseFolder": "قوشۇلىدىغان قىسقۇچنى تاللاڭ",
|
"chooseFolder": "قوشۇش ئۈچۈن قىسقۇچ تاللاڭ",
|
||||||
"noFolders": "قىسقۇچ تېپىلمىدى",
|
"noFolders": "قىسقۇچ تېپىلمىدى",
|
||||||
"folderInfo": "{id}. {name}",
|
"folderInfo": "{id}. {name}",
|
||||||
"close": "تاقاش",
|
"close": "تاقاش",
|
||||||
"success": "تېكىست جۈپى قىسقۇچقا قوشۇلدى",
|
"success": "تېكىست جۈپى قىسقۇچقا قوشۇلدى",
|
||||||
"error": "تېكىست جۈپىنى قىسقۇچقا قوشۇش مەغلۇب بولدى"
|
"error": "تېكىست جۈپىنى قىسقۇچقا قوشۇش مەغلۇپ بولدى"
|
||||||
},
|
},
|
||||||
"autoSave": "ئاپتوماتىك ساقلاش"
|
"autoSave": "ئاپتوماتىك ساقلاش"
|
||||||
},
|
},
|
||||||
"dictionary": {
|
"dictionary": {
|
||||||
"title": "لۇغەت",
|
"title": "لۇغەت",
|
||||||
"description": "تەپسىلىي ئىزاھات ۋە مىساللار بىلەن سۆز ۋە ئىبارە ئىزدەش",
|
"description": "سۆزلەر ۋە ئىبارىلەرنى تەپسىلىي ئېنىقلىما ۋە مىساللار بىلەن ئىزدەڭ",
|
||||||
"searchPlaceholder": "ئىزدەيدىغان سۆز ياكى ئىبارە كىرگۈزۈڭ...",
|
"searchPlaceholder": "ئىزدەش ئۈچۈن سۆز ياكى ئىبارە كىرگۈزۈڭ...",
|
||||||
"searching": "ئىزدەۋاتىدۇ...",
|
"searching": "ئىزدەۋاتىدۇ...",
|
||||||
"search": "ئىزدە",
|
"search": "ئىزدەش",
|
||||||
"languageSettings": "تىل تەڭشىكى",
|
"languageSettings": "تىل تەڭشەكلىرى",
|
||||||
"queryLanguage": "سۈرەشتۈرۈش تىلى",
|
"queryLanguage": "سۈرۈشتۈرۈش تىلى",
|
||||||
"queryLanguageHint": "ئىزدەمدەكچى بولغان سۆز/ئىبارە قايسى تىلدا",
|
"queryLanguageHint": "ئىزدىمەكچى بولغان سۆز/ئىبارە قايسى تىلدا",
|
||||||
"definitionLanguage": "ئىزاھات تىلى",
|
"definitionLanguage": "ئېنىقلىما تىلى",
|
||||||
"definitionLanguageHint": "ئىزاھاتنى قايسى تىلدا كۆرۈشنى ئويلىشىسىز",
|
"definitionLanguageHint": "ئېنىقلىمىلارنى قايسى تىلدا كۆرمەكچى",
|
||||||
"otherLanguagePlaceholder": "ياكى باشقا تىل كىرگۈزۈڭ...",
|
"otherLanguagePlaceholder": "ياكى باشقا تىل كىرگۈزۈڭ...",
|
||||||
"currentSettings": "نۆۋەتتىكى تەڭشەك: سۈرەشتۈرۈش {queryLang}، ئىزاھات {definitionLang}",
|
"other": "باشقا",
|
||||||
"relookup": "قايتا ئىزدە",
|
"currentSettings": "نۆۋەتتىكى تەڭشەكلەر: سۈرۈشتۈرۈش {queryLang}، ئېنىقلىما {definitionLang}",
|
||||||
"saveToFolder": "قىسقۇچقا ساقلا",
|
"relookup": "قايتا ئىزدەش",
|
||||||
"loading": "يۈكلىۋاتىدۇ...",
|
"saveToFolder": "قىسقۇچقا ساقلاش",
|
||||||
|
"loading": "يۈكلىنىۋاتىدۇ...",
|
||||||
"noResults": "نەتىجە تېپىلمىدى",
|
"noResults": "نەتىجە تېپىلمىدى",
|
||||||
"tryOtherWords": "باشقا سۆز ياكى ئىبارە سىناڭ",
|
"tryOtherWords": "باشقا سۆز ياكى ئىبارىلەرنى سىناڭ",
|
||||||
"welcomeTitle": "لۇغەتكە مەرھەمەت",
|
"welcomeTitle": "لۇغەتكە خۇش كەلدىڭىز",
|
||||||
"welcomeHint": "ئىزدەشنى باشلاش ئۈچۈن يۇقىرىدىكى ئىزدەش رامكىسىغا سۆز ياكى ئىبارە كىرگۈزۈڭ",
|
"welcomeHint": "ئىزدەشنى باشلاش ئۈچۈن يۇقىرىدىكى ئىزدەش رامكىسىغا سۆز ياكى ئىبارە كىرگۈزۈڭ",
|
||||||
"lookupFailed": "ئىزدەش مەغلۇب بولدى، كېيىنرەك قايتا سىناڭ",
|
"lookupFailed": "ئىزدەش مەغلۇپ بولدى، كېيىن قايتا سىناڭ",
|
||||||
"relookupSuccess": "مۇۋەپپەقىيەتلىك قايتا ئىزدىدى",
|
"relookupSuccess": "مۇۋەپپەقىيەتلىك قايتا ئىزدەلدى",
|
||||||
"relookupFailed": "لۇغەت قايتا ئىزدىشى مەغلۇب بولدى",
|
"relookupFailed": "لۇغەت قايتا ئىزدەش مەغلۇپ بولدى",
|
||||||
"pleaseLogin": "ئاۋۋال تىزىملىتىڭ",
|
"pleaseLogin": "ئاۋۋال تىزىمغا كىرىڭ",
|
||||||
"pleaseCreateFolder": "ئاۋۋال قىسقۇچ قۇرۇڭ",
|
"pleaseCreateFolder": "ئاۋۋال بىر قىسقۇچ قۇرۇڭ",
|
||||||
"savedToFolder": "قىسقۇچقا ساقلاندى: {folderName}",
|
"savedToFolder": "قىسقۇچقا ساقلاندى: {folderName}",
|
||||||
"saveFailed": "ساقلاش مەغلۇب بولدى، كېيىنرەك قايتا سىناڭ"
|
"saveFailed": "ساقلاش مەغلۇپ بولدى، كېيىن قايتا سىناڭ",
|
||||||
|
"definition": "ئېنىقلىما",
|
||||||
|
"example": "مىسال"
|
||||||
|
},
|
||||||
|
"explore": {
|
||||||
|
"title": "ئىزدىنىش",
|
||||||
|
"subtitle": "ئاممىۋى قىسقۇچلارنى بايقاڭ",
|
||||||
|
"searchPlaceholder": "ئاممىۋى قىسقۇچلارنى ئىزدەڭ...",
|
||||||
|
"loading": "يۈكلىنىۋاتىدۇ...",
|
||||||
|
"noFolders": "ئاممىۋى قىسقۇچ تېپىلمىدى",
|
||||||
|
"folderInfo": "{userName} • {totalPairs} جۈپ",
|
||||||
|
"unknownUser": "نامەلۇم ئىشلەتكۈچى",
|
||||||
|
"favorite": "يىغىپ ساقلا",
|
||||||
|
"unfavorite": "يىغىپ ساقلاشنى بىكار قىل",
|
||||||
|
"pleaseLogin": "ئاۋۋال تىزىمغا كىرىڭ",
|
||||||
|
"sortByFavorites": "يىغىپ ساقلاش بويىچە تەرتىپلەش",
|
||||||
|
"sortByFavoritesActive": "يىغىپ ساقلاش بويىچە تەرتىپلەشنى بىكار قىلىش"
|
||||||
|
},
|
||||||
|
"exploreDetail": {
|
||||||
|
"title": "قىسقۇچ تەپسىلاتلىرى",
|
||||||
|
"createdBy": "قۇرغۇچى: {name}",
|
||||||
|
"unknownUser": "نامەلۇم ئىشلەتكۈچى",
|
||||||
|
"totalPairs": "جەمئىي جۈپ",
|
||||||
|
"favorites": "يىغىپ ساقلانغانلار",
|
||||||
|
"createdAt": "قۇرۇلغان ۋاقتى",
|
||||||
|
"viewContent": "مەزمۇننى كۆرۈش",
|
||||||
|
"favorite": "يىغىپ ساقلا",
|
||||||
|
"unfavorite": "يىغىپ ساقلاشنى بىكار قىل",
|
||||||
|
"favorited": "يىغىپ ساقلاندى",
|
||||||
|
"unfavorited": "يىغىپ ساقلاش بىكار قىلىندى",
|
||||||
|
"pleaseLogin": "ئاۋۋال تىزىمغا كىرىڭ"
|
||||||
|
},
|
||||||
|
"favorites": {
|
||||||
|
"title": "يىغىپ ساقلىغانلىرىم",
|
||||||
|
"subtitle": "يىغىپ ساقلىغان قىسقۇچلىرىڭىز",
|
||||||
|
"loading": "يۈكلىنىۋاتىدۇ...",
|
||||||
|
"noFavorites": "تېخى يىغىپ ساقلانمىغان",
|
||||||
|
"folderInfo": "{userName} • {totalPairs} جۈپ",
|
||||||
|
"unknownUser": "نامەلۇم ئىشلەتكۈچى"
|
||||||
},
|
},
|
||||||
"user_profile": {
|
"user_profile": {
|
||||||
"anonymous": "ئىسىمسىز",
|
"anonymous": "نامسىز",
|
||||||
"email": "ئېلخەت",
|
"email": "ئېلخەت",
|
||||||
"verified": "دەلىللەندى",
|
"verified": "دەلىللەنگەن",
|
||||||
"unverified": "دەلىتلەنمىدى",
|
"unverified": "دەلىللەنمىگەن",
|
||||||
"accountInfo": "ھېسابات ئۇچۇرى",
|
"accountInfo": "ھېسابات ئۇچۇرلىرى",
|
||||||
"userId": "ئىشلەتكۈچى كودى",
|
"userId": "ئىشلەتكۈچى كىملىكى",
|
||||||
"username": "ئىشلەتكۈچى نامى",
|
"username": "ئىشلەتكۈچى ئاتى",
|
||||||
"displayName": "كۆرسىتىلىدىغان نام",
|
"displayName": "كۆرسىتىش ئاتى",
|
||||||
"notSet": "تەڭشەلمىگەن",
|
"notSet": "تەڭشەلمىگەن",
|
||||||
"memberSince": "تىزىملاتقان ۋاقىت",
|
"memberSince": "ئەزا بولغاندىن بېرى",
|
||||||
|
"logout": "چىكىنىش",
|
||||||
"folders": {
|
"folders": {
|
||||||
"title": "قىسقۇچلار",
|
"title": "قىسقۇچلار",
|
||||||
"noFolders": "قىسقۇچ يوق",
|
"noFolders": "تېخى قىسقۇچ يوق",
|
||||||
"folderName": "قىسقۇچ نامى",
|
"folderName": "قىسقۇچ ئاتى",
|
||||||
"totalPairs": "تېكىست جۈپ سانى",
|
"totalPairs": "جەمئىي جۈپ",
|
||||||
"createdAt": "قۇرۇلغان ۋاقىت",
|
"createdAt": "قۇرۇلغان ۋاقتى",
|
||||||
"actions": "مەشغۇلات",
|
"actions": "مەشغۇلاتلار",
|
||||||
"view": "كۆرۈش"
|
"view": "كۆرۈش"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"alphabet": {
|
"alphabet": {
|
||||||
"chooseCharacters": "请选择您想学习的字符",
|
"chooseCharacters": "请选择您想学习的字符",
|
||||||
|
"chooseAlphabetHint": "选择一种语言的字母表开始学习",
|
||||||
"japanese": "日语假名",
|
"japanese": "日语假名",
|
||||||
"english": "英文字母",
|
"english": "英文字母",
|
||||||
"uyghur": "维吾尔字母",
|
"uyghur": "维吾尔字母",
|
||||||
@@ -14,7 +15,11 @@
|
|||||||
"roman": "罗马音",
|
"roman": "罗马音",
|
||||||
"letter": "字母",
|
"letter": "字母",
|
||||||
"random": "随机模式",
|
"random": "随机模式",
|
||||||
"randomNext": "随机下一个"
|
"randomNext": "随机下一个",
|
||||||
|
"previousLetter": "上一个字母",
|
||||||
|
"nextLetter": "下一个字母",
|
||||||
|
"keyboardHint": "使用左右箭头键或空格键随机切换,ESC键返回",
|
||||||
|
"swipeHint": "使用左右箭头键或滑动切换字母"
|
||||||
},
|
},
|
||||||
"folders": {
|
"folders": {
|
||||||
"title": "文件夹",
|
"title": "文件夹",
|
||||||
@@ -108,6 +113,7 @@
|
|||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"title": "登录",
|
"title": "登录",
|
||||||
|
"signUpTitle": "注册",
|
||||||
"signIn": "登录",
|
"signIn": "登录",
|
||||||
"signUp": "注册",
|
"signUp": "注册",
|
||||||
"email": "邮箱",
|
"email": "邮箱",
|
||||||
@@ -133,7 +139,34 @@
|
|||||||
"identifierRequired": "请输入邮箱或用户名",
|
"identifierRequired": "请输入邮箱或用户名",
|
||||||
"passwordRequired": "请输入密码",
|
"passwordRequired": "请输入密码",
|
||||||
"confirmPasswordRequired": "请确认密码",
|
"confirmPasswordRequired": "请确认密码",
|
||||||
"loading": "加载中..."
|
"loading": "加载中...",
|
||||||
|
"confirm": "确认",
|
||||||
|
"noAccountLink": "没有账号?去注册",
|
||||||
|
"hasAccountLink": "已有账号?去登录",
|
||||||
|
"usernamePlaceholder": "用户名",
|
||||||
|
"emailPlaceholder": "邮箱地址",
|
||||||
|
"passwordPlaceholder": "密码",
|
||||||
|
"usernameOrEmailPlaceholder": "用户名或邮箱地址",
|
||||||
|
"loginFailed": "登录失败",
|
||||||
|
"signUpFailed": "注册失败",
|
||||||
|
"fillAllFields": "请填写所有字段",
|
||||||
|
"enterCredentials": "请输入用户名和密码",
|
||||||
|
"forgotPassword": "忘记密码",
|
||||||
|
"forgotPasswordHint": "输入您的邮箱地址,我们将向您发送重置密码的链接。",
|
||||||
|
"sendResetEmail": "发送重置邮件",
|
||||||
|
"resetPasswordFailed": "发送重置邮件失败",
|
||||||
|
"resetPasswordEmailSent": "重置邮件已发送",
|
||||||
|
"resetPasswordEmailSentHint": "我们已向您的邮箱发送了密码重置链接,请查收。",
|
||||||
|
"checkYourEmail": "请查收邮件",
|
||||||
|
"backToLogin": "返回登录",
|
||||||
|
"resetPassword": "重置密码",
|
||||||
|
"newPassword": "新密码",
|
||||||
|
"invalidToken": "链接无效或已过期",
|
||||||
|
"invalidTokenHint": "此密码重置链接无效或已过期,请重新申请。",
|
||||||
|
"requestNewToken": "重新申请重置链接",
|
||||||
|
"resetPasswordSuccess": "密码重置成功",
|
||||||
|
"resetPasswordSuccessTitle": "密码重置完成",
|
||||||
|
"resetPasswordSuccessHint": "您的密码已成功重置,现在可以使用新密码登录了。"
|
||||||
},
|
},
|
||||||
"memorize": {
|
"memorize": {
|
||||||
"folder_selector": {
|
"folder_selector": {
|
||||||
@@ -187,11 +220,17 @@
|
|||||||
"subtitleFile": "字幕文件",
|
"subtitleFile": "字幕文件",
|
||||||
"uploaded": "已上传",
|
"uploaded": "已上传",
|
||||||
"notUploaded": "未上传",
|
"notUploaded": "未上传",
|
||||||
|
"uploadVideoButton": "上传视频",
|
||||||
|
"uploadSubtitleButton": "上传字幕",
|
||||||
|
"subtitleUploaded": "字幕已上传 ({count} 条)",
|
||||||
|
"subtitleNotUploaded": "字幕未上传",
|
||||||
"autoPauseStatus": "自动暂停: {enabled}",
|
"autoPauseStatus": "自动暂停: {enabled}",
|
||||||
"on": "开",
|
"on": "开",
|
||||||
"off": "关",
|
"off": "关",
|
||||||
"videoUploadFailed": "视频上传失败",
|
"videoUploadFailed": "视频上传失败",
|
||||||
"subtitleUploadFailed": "字幕上传失败"
|
"subtitleUploadFailed": "字幕上传失败",
|
||||||
|
"subtitleLoadSuccess": "字幕加载成功",
|
||||||
|
"subtitleLoadFailed": "字幕加载失败"
|
||||||
},
|
},
|
||||||
"text_speaker": {
|
"text_speaker": {
|
||||||
"generateIPA": "生成IPA",
|
"generateIPA": "生成IPA",
|
||||||
@@ -256,7 +295,9 @@
|
|||||||
"pleaseLogin": "请先登录",
|
"pleaseLogin": "请先登录",
|
||||||
"pleaseCreateFolder": "请先创建文件夹",
|
"pleaseCreateFolder": "请先创建文件夹",
|
||||||
"savedToFolder": "已保存到文件夹:{folderName}",
|
"savedToFolder": "已保存到文件夹:{folderName}",
|
||||||
"saveFailed": "保存失败,请稍后重试"
|
"saveFailed": "保存失败,请稍后重试",
|
||||||
|
"definition": "释义",
|
||||||
|
"example": "例句"
|
||||||
},
|
},
|
||||||
"explore": {
|
"explore": {
|
||||||
"title": "探索",
|
"title": "探索",
|
||||||
@@ -272,13 +313,19 @@
|
|||||||
"sortByFavorites": "按收藏数排序",
|
"sortByFavorites": "按收藏数排序",
|
||||||
"sortByFavoritesActive": "取消按收藏数排序"
|
"sortByFavoritesActive": "取消按收藏数排序"
|
||||||
},
|
},
|
||||||
"favorites": {
|
"exploreDetail": {
|
||||||
"title": "收藏",
|
"title": "文件夹详情",
|
||||||
"subtitle": "我收藏的文件夹",
|
"createdBy": "创建者:{name}",
|
||||||
"loading": "加载中...",
|
"unknownUser": "未知用户",
|
||||||
"noFavorites": "还没有收藏",
|
"totalPairs": "词对数量",
|
||||||
"folderInfo": "{userName} • {totalPairs} 个文本对",
|
"favorites": "收藏数",
|
||||||
"unknownUser": "未知用户"
|
"createdAt": "创建时间",
|
||||||
|
"viewContent": "查看内容",
|
||||||
|
"favorite": "收藏",
|
||||||
|
"unfavorite": "取消收藏",
|
||||||
|
"favorited": "已收藏",
|
||||||
|
"unfavorited": "已取消收藏",
|
||||||
|
"pleaseLogin": "请先登录"
|
||||||
},
|
},
|
||||||
"favorites": {
|
"favorites": {
|
||||||
"title": "我的收藏",
|
"title": "我的收藏",
|
||||||
@@ -299,6 +346,7 @@
|
|||||||
"displayName": "显示名称",
|
"displayName": "显示名称",
|
||||||
"notSet": "未设置",
|
"notSet": "未设置",
|
||||||
"memberSince": "注册时间",
|
"memberSince": "注册时间",
|
||||||
|
"logout": "登出",
|
||||||
"folders": {
|
"folders": {
|
||||||
"title": "文件夹",
|
"title": "文件夹",
|
||||||
"noFolders": "还没有文件夹",
|
"noFolders": "还没有文件夹",
|
||||||
|
|||||||
@@ -21,6 +21,8 @@
|
|||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"next": "16.1.1",
|
"next": "16.1.1",
|
||||||
"next-intl": "^4.7.0",
|
"next-intl": "^4.7.0",
|
||||||
|
"nodemailer": "^8.0.2",
|
||||||
|
"openai": "^6.27.0",
|
||||||
"pg": "^8.16.3",
|
"pg": "^8.16.3",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-dom": "19.2.3",
|
"react-dom": "19.2.3",
|
||||||
@@ -36,6 +38,7 @@
|
|||||||
"@eslint/eslintrc": "^3.3.3",
|
"@eslint/eslintrc": "^3.3.3",
|
||||||
"@tailwindcss/postcss": "^4.1.18",
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
"@types/node": "^25.0.3",
|
"@types/node": "^25.0.3",
|
||||||
|
"@types/nodemailer": "^7.0.11",
|
||||||
"@types/react": "19.2.7",
|
"@types/react": "19.2.7",
|
||||||
"@types/react-dom": "19.2.3",
|
"@types/react-dom": "19.2.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.51.0",
|
"@typescript-eslint/eslint-plugin": "^8.51.0",
|
||||||
|
|||||||
38
pnpm-lock.yaml
generated
38
pnpm-lock.yaml
generated
@@ -42,6 +42,12 @@ importers:
|
|||||||
next-intl:
|
next-intl:
|
||||||
specifier: ^4.7.0
|
specifier: ^4.7.0
|
||||||
version: 4.7.0(next@16.1.1(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(typescript@5.9.3)
|
version: 4.7.0(next@16.1.1(@babel/core@7.28.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(typescript@5.9.3)
|
||||||
|
nodemailer:
|
||||||
|
specifier: ^8.0.2
|
||||||
|
version: 8.0.2
|
||||||
|
openai:
|
||||||
|
specifier: ^6.27.0
|
||||||
|
version: 6.27.0(zod@4.3.5)
|
||||||
pg:
|
pg:
|
||||||
specifier: ^8.16.3
|
specifier: ^8.16.3
|
||||||
version: 8.16.3
|
version: 8.16.3
|
||||||
@@ -82,6 +88,9 @@ importers:
|
|||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^25.0.3
|
specifier: ^25.0.3
|
||||||
version: 25.0.3
|
version: 25.0.3
|
||||||
|
'@types/nodemailer':
|
||||||
|
specifier: ^7.0.11
|
||||||
|
version: 7.0.11
|
||||||
'@types/react':
|
'@types/react':
|
||||||
specifier: 19.2.7
|
specifier: 19.2.7
|
||||||
version: 19.2.7
|
version: 19.2.7
|
||||||
@@ -1049,6 +1058,9 @@ packages:
|
|||||||
'@types/node@25.0.3':
|
'@types/node@25.0.3':
|
||||||
resolution: {integrity: sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==}
|
resolution: {integrity: sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==}
|
||||||
|
|
||||||
|
'@types/nodemailer@7.0.11':
|
||||||
|
resolution: {integrity: sha512-E+U4RzR2dKrx+u3N4DlsmLaDC6mMZOM/TPROxA0UAPiTgI0y4CEFBmZE+coGWTjakDriRsXG368lNk1u9Q0a2g==}
|
||||||
|
|
||||||
'@types/pg@8.15.6':
|
'@types/pg@8.15.6':
|
||||||
resolution: {integrity: sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==}
|
resolution: {integrity: sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==}
|
||||||
|
|
||||||
@@ -2661,6 +2673,10 @@ packages:
|
|||||||
node-releases@2.0.27:
|
node-releases@2.0.27:
|
||||||
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
|
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
|
||||||
|
|
||||||
|
nodemailer@8.0.2:
|
||||||
|
resolution: {integrity: sha512-zbj002pZAIkWQFxyAaqoxvn+zoIwRnS40hgjqTXudKOOJkiFFgBeNqjgD3/YCR12sZnrghWYBY+yP1ZucdDRpw==}
|
||||||
|
engines: {node: '>=6.0.0'}
|
||||||
|
|
||||||
normalize-path@3.0.0:
|
normalize-path@3.0.0:
|
||||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -2718,6 +2734,18 @@ packages:
|
|||||||
resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==}
|
resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
openai@6.27.0:
|
||||||
|
resolution: {integrity: sha512-osTKySlrdYrLYTt0zjhY8yp0JUBmWDCN+Q+QxsV4xMQnnoVFpylgKGgxwN8sSdTNw0G4y+WUXs4eCMWpyDNWZQ==}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
ws: ^8.18.0
|
||||||
|
zod: ^3.25 || ^4.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
ws:
|
||||||
|
optional: true
|
||||||
|
zod:
|
||||||
|
optional: true
|
||||||
|
|
||||||
optionator@0.9.4:
|
optionator@0.9.4:
|
||||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@@ -4393,6 +4421,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.16.0
|
undici-types: 7.16.0
|
||||||
|
|
||||||
|
'@types/nodemailer@7.0.11':
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 25.0.3
|
||||||
|
|
||||||
'@types/pg@8.15.6':
|
'@types/pg@8.15.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.0.3
|
'@types/node': 25.0.3
|
||||||
@@ -6068,6 +6100,8 @@ snapshots:
|
|||||||
|
|
||||||
node-releases@2.0.27: {}
|
node-releases@2.0.27: {}
|
||||||
|
|
||||||
|
nodemailer@8.0.2: {}
|
||||||
|
|
||||||
normalize-path@3.0.0: {}
|
normalize-path@3.0.0: {}
|
||||||
|
|
||||||
nypm@0.6.2:
|
nypm@0.6.2:
|
||||||
@@ -6143,6 +6177,10 @@ snapshots:
|
|||||||
is-inside-container: 1.0.0
|
is-inside-container: 1.0.0
|
||||||
wsl-utils: 0.1.0
|
wsl-utils: 0.1.0
|
||||||
|
|
||||||
|
openai@6.27.0(zod@4.3.5):
|
||||||
|
optionalDependencies:
|
||||||
|
zod: 4.3.5
|
||||||
|
|
||||||
optionator@0.9.4:
|
optionator@0.9.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
deep-is: 0.1.4
|
deep-is: 0.1.4
|
||||||
|
|||||||
@@ -1,120 +0,0 @@
|
|||||||
-- CreateTable
|
|
||||||
CREATE TABLE "pairs" (
|
|
||||||
"id" SERIAL NOT NULL,
|
|
||||||
"locale1" VARCHAR(10) NOT NULL,
|
|
||||||
"locale2" VARCHAR(10) NOT NULL,
|
|
||||||
"text1" TEXT NOT NULL,
|
|
||||||
"text2" TEXT NOT NULL,
|
|
||||||
"ipa1" TEXT,
|
|
||||||
"ipa2" TEXT,
|
|
||||||
"folder_id" INTEGER NOT NULL,
|
|
||||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
|
||||||
|
|
||||||
CONSTRAINT "pairs_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "folders" (
|
|
||||||
"id" SERIAL NOT NULL,
|
|
||||||
"name" TEXT NOT NULL,
|
|
||||||
"user_id" TEXT NOT NULL,
|
|
||||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
|
||||||
|
|
||||||
CONSTRAINT "folders_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "user" (
|
|
||||||
"id" TEXT NOT NULL,
|
|
||||||
"name" TEXT NOT NULL,
|
|
||||||
"email" TEXT NOT NULL,
|
|
||||||
"emailVerified" BOOLEAN NOT NULL DEFAULT false,
|
|
||||||
"image" TEXT,
|
|
||||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
||||||
|
|
||||||
CONSTRAINT "user_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "session" (
|
|
||||||
"id" TEXT NOT NULL,
|
|
||||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
|
||||||
"token" TEXT NOT NULL,
|
|
||||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
||||||
"ipAddress" TEXT,
|
|
||||||
"userAgent" TEXT,
|
|
||||||
"userId" TEXT NOT NULL,
|
|
||||||
|
|
||||||
CONSTRAINT "session_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "account" (
|
|
||||||
"id" TEXT NOT NULL,
|
|
||||||
"accountId" TEXT NOT NULL,
|
|
||||||
"providerId" TEXT NOT NULL,
|
|
||||||
"userId" TEXT NOT NULL,
|
|
||||||
"accessToken" TEXT,
|
|
||||||
"refreshToken" TEXT,
|
|
||||||
"idToken" TEXT,
|
|
||||||
"accessTokenExpiresAt" TIMESTAMP(3),
|
|
||||||
"refreshTokenExpiresAt" TIMESTAMP(3),
|
|
||||||
"scope" TEXT,
|
|
||||||
"password" TEXT,
|
|
||||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
||||||
|
|
||||||
CONSTRAINT "account_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "verification" (
|
|
||||||
"id" TEXT NOT NULL,
|
|
||||||
"identifier" TEXT NOT NULL,
|
|
||||||
"value" TEXT NOT NULL,
|
|
||||||
"expiresAt" TIMESTAMP(3) NOT NULL,
|
|
||||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
||||||
|
|
||||||
CONSTRAINT "verification_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "pairs_folder_id_idx" ON "pairs"("folder_id");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "pairs_folder_id_locale1_locale2_text1_key" ON "pairs"("folder_id", "locale1", "locale2", "text1");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "folders_user_id_idx" ON "folders"("user_id");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "user_email_key" ON "user"("email");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "session_userId_idx" ON "session"("userId");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "session_token_key" ON "session"("token");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "account_userId_idx" ON "account"("userId");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "verification_identifier_idx" ON "verification"("identifier");
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "pairs" ADD CONSTRAINT "pairs_folder_id_fkey" FOREIGN KEY ("folder_id") REFERENCES "folders"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "folders" ADD CONSTRAINT "folders_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "session" ADD CONSTRAINT "session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "account" ADD CONSTRAINT "account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
/*
|
|
||||||
Warnings:
|
|
||||||
|
|
||||||
- You are about to drop the column `ipa1` on the `pairs` table. All the data in the column will be lost.
|
|
||||||
- You are about to drop the column `ipa2` on the `pairs` table. All the data in the column will be lost.
|
|
||||||
|
|
||||||
*/
|
|
||||||
-- AlterTable
|
|
||||||
-- 重命名并修改类型为 TEXT
|
|
||||||
ALTER TABLE "pairs"
|
|
||||||
RENAME COLUMN "locale1" TO "language1";
|
|
||||||
|
|
||||||
ALTER TABLE "pairs"
|
|
||||||
ALTER COLUMN "language1" SET DATA TYPE VARCHAR(20);
|
|
||||||
|
|
||||||
ALTER TABLE "pairs"
|
|
||||||
RENAME COLUMN "locale2" TO "language2";
|
|
||||||
|
|
||||||
ALTER TABLE "pairs"
|
|
||||||
ALTER COLUMN "language2" SET DATA TYPE VARCHAR(20);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "dictionary_lookups" (
|
|
||||||
"id" SERIAL NOT NULL,
|
|
||||||
"user_id" TEXT,
|
|
||||||
"text" TEXT NOT NULL,
|
|
||||||
"query_lang" TEXT NOT NULL,
|
|
||||||
"definition_lang" TEXT NOT NULL,
|
|
||||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"dictionary_word_id" INTEGER,
|
|
||||||
"dictionary_phrase_id" INTEGER,
|
|
||||||
|
|
||||||
CONSTRAINT "dictionary_lookups_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "dictionary_words" (
|
|
||||||
"id" SERIAL NOT NULL,
|
|
||||||
"standard_form" TEXT NOT NULL,
|
|
||||||
"query_lang" TEXT NOT NULL,
|
|
||||||
"definition_lang" TEXT NOT NULL,
|
|
||||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
|
||||||
|
|
||||||
CONSTRAINT "dictionary_words_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "dictionary_phrases" (
|
|
||||||
"id" SERIAL NOT NULL,
|
|
||||||
"standard_form" TEXT NOT NULL,
|
|
||||||
"query_lang" TEXT NOT NULL,
|
|
||||||
"definition_lang" TEXT NOT NULL,
|
|
||||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
|
||||||
|
|
||||||
CONSTRAINT "dictionary_phrases_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "dictionary_word_entries" (
|
|
||||||
"id" SERIAL NOT NULL,
|
|
||||||
"word_id" INTEGER NOT NULL,
|
|
||||||
"ipa" TEXT NOT NULL,
|
|
||||||
"definition" TEXT NOT NULL,
|
|
||||||
"part_of_speech" TEXT NOT NULL,
|
|
||||||
"example" TEXT NOT NULL,
|
|
||||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
|
||||||
|
|
||||||
CONSTRAINT "dictionary_word_entries_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "dictionary_phrase_entries" (
|
|
||||||
"id" SERIAL NOT NULL,
|
|
||||||
"phrase_id" INTEGER NOT NULL,
|
|
||||||
"definition" TEXT NOT NULL,
|
|
||||||
"example" TEXT NOT NULL,
|
|
||||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
|
||||||
|
|
||||||
CONSTRAINT "dictionary_phrase_entries_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "dictionary_lookups_user_id_idx" ON "dictionary_lookups"("user_id");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "dictionary_lookups_created_at_idx" ON "dictionary_lookups"("created_at");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "dictionary_lookups_text_query_lang_definition_lang_idx" ON "dictionary_lookups"("text", "query_lang", "definition_lang");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "dictionary_words_standard_form_idx" ON "dictionary_words"("standard_form");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "dictionary_words_query_lang_definition_lang_idx" ON "dictionary_words"("query_lang", "definition_lang");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "dictionary_words_standard_form_query_lang_definition_lang_key" ON "dictionary_words"("standard_form", "query_lang", "definition_lang");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "dictionary_phrases_standard_form_idx" ON "dictionary_phrases"("standard_form");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "dictionary_phrases_query_lang_definition_lang_idx" ON "dictionary_phrases"("query_lang", "definition_lang");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "dictionary_phrases_standard_form_query_lang_definition_lang_key" ON "dictionary_phrases"("standard_form", "query_lang", "definition_lang");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "dictionary_word_entries_word_id_idx" ON "dictionary_word_entries"("word_id");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "dictionary_word_entries_created_at_idx" ON "dictionary_word_entries"("created_at");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "dictionary_phrase_entries_phrase_id_idx" ON "dictionary_phrase_entries"("phrase_id");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "dictionary_phrase_entries_created_at_idx" ON "dictionary_phrase_entries"("created_at");
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "dictionary_lookups" ADD CONSTRAINT "dictionary_lookups_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "dictionary_lookups" ADD CONSTRAINT "dictionary_lookups_dictionary_word_id_fkey" FOREIGN KEY ("dictionary_word_id") REFERENCES "dictionary_words"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "dictionary_lookups" ADD CONSTRAINT "dictionary_lookups_dictionary_phrase_id_fkey" FOREIGN KEY ("dictionary_phrase_id") REFERENCES "dictionary_phrases"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "dictionary_word_entries" ADD CONSTRAINT "dictionary_word_entries_word_id_fkey" FOREIGN KEY ("word_id") REFERENCES "dictionary_words"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "dictionary_phrase_entries" ADD CONSTRAINT "dictionary_phrase_entries_phrase_id_fkey" FOREIGN KEY ("phrase_id") REFERENCES "dictionary_phrases"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
-- DropIndex
|
|
||||||
DROP INDEX "dictionary_phrases_standard_form_query_lang_definition_lang_key";
|
|
||||||
|
|
||||||
-- DropIndex
|
|
||||||
DROP INDEX "dictionary_words_standard_form_query_lang_definition_lang_key";
|
|
||||||
|
|
||||||
-- RenameIndex
|
|
||||||
ALTER INDEX "pairs_folder_id_locale1_locale2_text1_key" RENAME TO "pairs_folder_id_language1_language2_text1_key";
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
-- CreateTable
|
|
||||||
CREATE TABLE "translation_history" (
|
|
||||||
"id" SERIAL NOT NULL,
|
|
||||||
"user_id" TEXT,
|
|
||||||
"source_text" TEXT NOT NULL,
|
|
||||||
"source_language" VARCHAR(20) NOT NULL,
|
|
||||||
"target_language" VARCHAR(20) NOT NULL,
|
|
||||||
"translated_text" TEXT NOT NULL,
|
|
||||||
"source_ipa" TEXT,
|
|
||||||
"target_ipa" TEXT,
|
|
||||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
|
||||||
|
|
||||||
CONSTRAINT "translation_history_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "translation_history_user_id_idx" ON "translation_history"("user_id");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "translation_history_created_at_idx" ON "translation_history"("created_at");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "translation_history_source_text_target_language_idx" ON "translation_history"("source_text", "target_language");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "translation_history_translated_text_source_language_target__idx" ON "translation_history"("translated_text", "source_language", "target_language");
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "translation_history" ADD CONSTRAINT "translation_history_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
/*
|
|
||||||
Warnings:
|
|
||||||
|
|
||||||
- A unique constraint covering the columns `[folder_id,language1,language2,text1,text2]` on the table `pairs` will be added. If there are existing duplicate values, this will fail.
|
|
||||||
|
|
||||||
*/
|
|
||||||
-- DropIndex
|
|
||||||
DROP INDEX "pairs_folder_id_language1_language2_text1_key";
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "pairs_folder_id_language1_language2_text1_text2_key" ON "pairs"("folder_id", "language1", "language2", "text1", "text2");
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
-- AlterTable
|
|
||||||
ALTER TABLE "pairs" ALTER COLUMN "language1" SET DATA TYPE TEXT,
|
|
||||||
ALTER COLUMN "language2" SET DATA TYPE TEXT;
|
|
||||||
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "translation_history" ALTER COLUMN "source_language" SET DATA TYPE TEXT,
|
|
||||||
ALTER COLUMN "target_language" SET DATA TYPE TEXT;
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
/*
|
|
||||||
Warnings:
|
|
||||||
|
|
||||||
- You are about to drop the column `dictionary_phrase_id` on the `dictionary_lookups` table. All the data in the column will be lost.
|
|
||||||
- You are about to drop the column `dictionary_word_id` on the `dictionary_lookups` table. All the data in the column will be lost.
|
|
||||||
- You are about to drop the `dictionary_phrase_entries` table. If the table is not empty, all the data it contains will be lost.
|
|
||||||
- You are about to drop the `dictionary_phrases` table. If the table is not empty, all the data it contains will be lost.
|
|
||||||
- You are about to drop the `dictionary_word_entries` table. If the table is not empty, all the data it contains will be lost.
|
|
||||||
- You are about to drop the `dictionary_words` table. If the table is not empty, all the data it contains will be lost.
|
|
||||||
|
|
||||||
*/
|
|
||||||
-- DropForeignKey
|
|
||||||
ALTER TABLE "dictionary_lookups" DROP CONSTRAINT "dictionary_lookups_dictionary_phrase_id_fkey";
|
|
||||||
|
|
||||||
-- DropForeignKey
|
|
||||||
ALTER TABLE "dictionary_lookups" DROP CONSTRAINT "dictionary_lookups_dictionary_word_id_fkey";
|
|
||||||
|
|
||||||
-- DropForeignKey
|
|
||||||
ALTER TABLE "dictionary_phrase_entries" DROP CONSTRAINT "dictionary_phrase_entries_phrase_id_fkey";
|
|
||||||
|
|
||||||
-- DropForeignKey
|
|
||||||
ALTER TABLE "dictionary_word_entries" DROP CONSTRAINT "dictionary_word_entries_word_id_fkey";
|
|
||||||
|
|
||||||
-- DropIndex
|
|
||||||
DROP INDEX "dictionary_lookups_text_query_lang_definition_lang_idx";
|
|
||||||
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "dictionary_lookups" DROP COLUMN "dictionary_phrase_id",
|
|
||||||
DROP COLUMN "dictionary_word_id",
|
|
||||||
ADD COLUMN "dictionary_item_id" INTEGER,
|
|
||||||
ADD COLUMN "normalized_text" TEXT NOT NULL DEFAULT '';
|
|
||||||
|
|
||||||
-- DropTable
|
|
||||||
DROP TABLE "dictionary_phrase_entries";
|
|
||||||
|
|
||||||
-- DropTable
|
|
||||||
DROP TABLE "dictionary_phrases";
|
|
||||||
|
|
||||||
-- DropTable
|
|
||||||
DROP TABLE "dictionary_word_entries";
|
|
||||||
|
|
||||||
-- DropTable
|
|
||||||
DROP TABLE "dictionary_words";
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "dictionary_items" (
|
|
||||||
"id" SERIAL NOT NULL,
|
|
||||||
"frequency" INTEGER NOT NULL DEFAULT 1,
|
|
||||||
"standard_form" TEXT NOT NULL,
|
|
||||||
"query_lang" TEXT NOT NULL,
|
|
||||||
"definition_lang" TEXT NOT NULL,
|
|
||||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
|
||||||
|
|
||||||
CONSTRAINT "dictionary_items_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "dictionary_entries" (
|
|
||||||
"id" SERIAL NOT NULL,
|
|
||||||
"item_id" INTEGER NOT NULL,
|
|
||||||
"ipa" TEXT,
|
|
||||||
"definition" TEXT NOT NULL,
|
|
||||||
"part_of_speech" TEXT,
|
|
||||||
"example" TEXT NOT NULL,
|
|
||||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
|
||||||
|
|
||||||
CONSTRAINT "dictionary_entries_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "dictionary_items_standard_form_idx" ON "dictionary_items"("standard_form");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "dictionary_items_query_lang_definition_lang_idx" ON "dictionary_items"("query_lang", "definition_lang");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "dictionary_items_standard_form_query_lang_definition_lang_key" ON "dictionary_items"("standard_form", "query_lang", "definition_lang");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "dictionary_entries_item_id_idx" ON "dictionary_entries"("item_id");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "dictionary_entries_created_at_idx" ON "dictionary_entries"("created_at");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "dictionary_lookups_normalized_text_idx" ON "dictionary_lookups"("normalized_text");
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "dictionary_lookups" ADD CONSTRAINT "dictionary_lookups_dictionary_item_id_fkey" FOREIGN KEY ("dictionary_item_id") REFERENCES "dictionary_items"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "dictionary_entries" ADD CONSTRAINT "dictionary_entries_item_id_fkey" FOREIGN KEY ("item_id") REFERENCES "dictionary_items"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
/*
|
|
||||||
Warnings:
|
|
||||||
|
|
||||||
- A unique constraint covering the columns `[username]` on the table `user` will be added. If there are existing duplicate values, this will fail.
|
|
||||||
|
|
||||||
*/
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "user" ADD COLUMN "displayUsername" TEXT,
|
|
||||||
ADD COLUMN "username" TEXT;
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "user_username_key" ON "user"("username");
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
-- CreateEnum
|
|
||||||
CREATE TYPE "Visibility" AS ENUM ('PRIVATE', 'PUBLIC');
|
|
||||||
|
|
||||||
-- AlterTable
|
|
||||||
ALTER TABLE "folders" ADD COLUMN "visibility" "Visibility" NOT NULL DEFAULT 'PRIVATE';
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "folder_favorites" (
|
|
||||||
"id" SERIAL NOT NULL,
|
|
||||||
"user_id" TEXT NOT NULL,
|
|
||||||
"folder_id" INTEGER NOT NULL,
|
|
||||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
|
|
||||||
CONSTRAINT "folder_favorites_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "folder_favorites_folder_id_idx" ON "folder_favorites"("folder_id");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "folder_favorites_user_id_folder_id_key" ON "folder_favorites"("user_id", "folder_id");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "folder_favorites_user_id_idx" ON "folder_favorites"("user_id");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "folders_visibility_idx" ON "folders"("visibility");
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "folder_favorites" ADD CONSTRAINT "folder_favorites_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "folder_favorites" ADD CONSTRAINT "folder_favorites_folder_id_fkey" FOREIGN KEY ("folder_id") REFERENCES "folders"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
262
prisma/migrations/20260310014042_init/migration.sql
Normal file
262
prisma/migrations/20260310014042_init/migration.sql
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "Visibility" AS ENUM ('PRIVATE', 'PUBLIC');
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "user" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"emailVerified" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"image" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"displayUsername" TEXT,
|
||||||
|
"username" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "user_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "session" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"token" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"ipAddress" TEXT,
|
||||||
|
"userAgent" TEXT,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "session_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "account" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"accountId" TEXT NOT NULL,
|
||||||
|
"providerId" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"accessToken" TEXT,
|
||||||
|
"refreshToken" TEXT,
|
||||||
|
"idToken" TEXT,
|
||||||
|
"accessTokenExpiresAt" TIMESTAMP(3),
|
||||||
|
"refreshTokenExpiresAt" TIMESTAMP(3),
|
||||||
|
"scope" TEXT,
|
||||||
|
"password" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "account_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "verification" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"identifier" TEXT NOT NULL,
|
||||||
|
"value" TEXT NOT NULL,
|
||||||
|
"expiresAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "verification_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "pairs" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"language1" TEXT NOT NULL,
|
||||||
|
"language2" TEXT NOT NULL,
|
||||||
|
"text1" TEXT NOT NULL,
|
||||||
|
"text2" TEXT NOT NULL,
|
||||||
|
"ipa1" TEXT,
|
||||||
|
"ipa2" TEXT,
|
||||||
|
"folder_id" INTEGER NOT NULL,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "pairs_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "folders" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"user_id" TEXT NOT NULL,
|
||||||
|
"visibility" "Visibility" NOT NULL DEFAULT 'PRIVATE',
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "folders_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "folder_favorites" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"user_id" TEXT NOT NULL,
|
||||||
|
"folder_id" INTEGER NOT NULL,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "folder_favorites_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "dictionary_lookups" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"user_id" TEXT,
|
||||||
|
"text" TEXT NOT NULL,
|
||||||
|
"query_lang" TEXT NOT NULL,
|
||||||
|
"definition_lang" TEXT NOT NULL,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"dictionary_item_id" INTEGER,
|
||||||
|
"normalized_text" TEXT NOT NULL DEFAULT '',
|
||||||
|
|
||||||
|
CONSTRAINT "dictionary_lookups_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "dictionary_items" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"frequency" INTEGER NOT NULL DEFAULT 1,
|
||||||
|
"standard_form" TEXT NOT NULL,
|
||||||
|
"query_lang" TEXT NOT NULL,
|
||||||
|
"definition_lang" TEXT NOT NULL,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "dictionary_items_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "dictionary_entries" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"item_id" INTEGER NOT NULL,
|
||||||
|
"ipa" TEXT,
|
||||||
|
"definition" TEXT NOT NULL,
|
||||||
|
"part_of_speech" TEXT,
|
||||||
|
"example" TEXT NOT NULL,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "dictionary_entries_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "translation_history" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"user_id" TEXT,
|
||||||
|
"source_text" TEXT NOT NULL,
|
||||||
|
"source_language" TEXT NOT NULL,
|
||||||
|
"target_language" TEXT NOT NULL,
|
||||||
|
"translated_text" TEXT NOT NULL,
|
||||||
|
"source_ipa" TEXT,
|
||||||
|
"target_ipa" TEXT,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "translation_history_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "user_email_key" ON "user"("email");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "user_username_key" ON "user"("username");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "session_token_key" ON "session"("token");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "session_userId_idx" ON "session"("userId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "account_userId_idx" ON "account"("userId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "verification_identifier_idx" ON "verification"("identifier");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "pairs_folder_id_idx" ON "pairs"("folder_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "pairs_folder_id_language1_language2_text1_text2_key" ON "pairs"("folder_id", "language1", "language2", "text1", "text2");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "folders_user_id_idx" ON "folders"("user_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "folders_visibility_idx" ON "folders"("visibility");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "folder_favorites_user_id_idx" ON "folder_favorites"("user_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "folder_favorites_folder_id_idx" ON "folder_favorites"("folder_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "folder_favorites_user_id_folder_id_key" ON "folder_favorites"("user_id", "folder_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "dictionary_lookups_user_id_idx" ON "dictionary_lookups"("user_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "dictionary_lookups_created_at_idx" ON "dictionary_lookups"("created_at");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "dictionary_lookups_normalized_text_idx" ON "dictionary_lookups"("normalized_text");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "dictionary_items_standard_form_idx" ON "dictionary_items"("standard_form");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "dictionary_items_query_lang_definition_lang_idx" ON "dictionary_items"("query_lang", "definition_lang");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "dictionary_items_standard_form_query_lang_definition_lang_key" ON "dictionary_items"("standard_form", "query_lang", "definition_lang");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "dictionary_entries_item_id_idx" ON "dictionary_entries"("item_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "dictionary_entries_created_at_idx" ON "dictionary_entries"("created_at");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "translation_history_user_id_idx" ON "translation_history"("user_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "translation_history_created_at_idx" ON "translation_history"("created_at");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "translation_history_source_text_target_language_idx" ON "translation_history"("source_text", "target_language");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "translation_history_translated_text_source_language_target__idx" ON "translation_history"("translated_text", "source_language", "target_language");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "session" ADD CONSTRAINT "session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "account" ADD CONSTRAINT "account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "pairs" ADD CONSTRAINT "pairs_folder_id_fkey" FOREIGN KEY ("folder_id") REFERENCES "folders"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "folders" ADD CONSTRAINT "folders_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "folder_favorites" ADD CONSTRAINT "folder_favorites_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "folder_favorites" ADD CONSTRAINT "folder_favorites_folder_id_fkey" FOREIGN KEY ("folder_id") REFERENCES "folders"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "dictionary_lookups" ADD CONSTRAINT "dictionary_lookups_dictionary_item_id_fkey" FOREIGN KEY ("dictionary_item_id") REFERENCES "dictionary_items"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "dictionary_lookups" ADD CONSTRAINT "dictionary_lookups_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "dictionary_entries" ADD CONSTRAINT "dictionary_entries_item_id_fkey" FOREIGN KEY ("item_id") REFERENCES "dictionary_items"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "translation_history" ADD CONSTRAINT "translation_history_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
@@ -16,7 +16,7 @@ model User {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
displayUsername String?
|
displayUsername String?
|
||||||
username String? @unique
|
username String @unique
|
||||||
accounts Account[]
|
accounts Account[]
|
||||||
dictionaryLookUps DictionaryLookUp[]
|
dictionaryLookUps DictionaryLookUp[]
|
||||||
folders Folder[]
|
folders Folder[]
|
||||||
|
|||||||
102
src/app/(auth)/forgot-password/page.tsx
Normal file
102
src/app/(auth)/forgot-password/page.tsx
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { Card, CardBody } from "@/design-system/base/card";
|
||||||
|
import { Input } from "@/design-system/base/input";
|
||||||
|
import { PrimaryButton } from "@/design-system/base/button";
|
||||||
|
import { VStack } from "@/design-system/layout/stack";
|
||||||
|
|
||||||
|
export default function ForgotPasswordPage() {
|
||||||
|
const t = useTranslations("auth");
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [sent, setSent] = useState(false);
|
||||||
|
|
||||||
|
const handleResetRequest = async () => {
|
||||||
|
if (!email) {
|
||||||
|
toast.error(t("emailRequired"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
const { error } = await authClient.requestPasswordReset({
|
||||||
|
email,
|
||||||
|
redirectTo: "/reset-password",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
toast.error(error.message ?? t("resetPasswordFailed"));
|
||||||
|
} else {
|
||||||
|
setSent(true);
|
||||||
|
toast.success(t("resetPasswordEmailSent"));
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (sent) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center min-h-screen">
|
||||||
|
<Card className="w-96">
|
||||||
|
<CardBody>
|
||||||
|
<VStack gap={4} align="center" justify="center">
|
||||||
|
<h1 className="text-2xl font-bold text-center w-full">
|
||||||
|
{t("checkYourEmail")}
|
||||||
|
</h1>
|
||||||
|
<p className="text-center text-gray-600">
|
||||||
|
{t("resetPasswordEmailSentHint")}
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
href="/login"
|
||||||
|
className="text-primary-500 hover:underline"
|
||||||
|
>
|
||||||
|
{t("backToLogin")}
|
||||||
|
</Link>
|
||||||
|
</VStack>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center min-h-screen">
|
||||||
|
<Card className="w-96">
|
||||||
|
<CardBody>
|
||||||
|
<VStack gap={4} align="center" justify="center">
|
||||||
|
<h1 className="text-3xl font-bold text-center w-full">
|
||||||
|
{t("forgotPassword")}
|
||||||
|
</h1>
|
||||||
|
<p className="text-center text-gray-600 text-sm">
|
||||||
|
{t("forgotPasswordHint")}
|
||||||
|
</p>
|
||||||
|
<VStack gap={0} align="center" justify="center" className="w-full">
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
placeholder={t("emailPlaceholder")}
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
/>
|
||||||
|
</VStack>
|
||||||
|
<PrimaryButton
|
||||||
|
onClick={handleResetRequest}
|
||||||
|
loading={loading}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
{t("sendResetEmail")}
|
||||||
|
</PrimaryButton>
|
||||||
|
<Link
|
||||||
|
href="/login"
|
||||||
|
className="text-center text-primary-500 hover:underline"
|
||||||
|
>
|
||||||
|
{t("backToLogin")}
|
||||||
|
</Link>
|
||||||
|
</VStack>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,17 +1,18 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useEffect } from "react";
|
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
import { Card, CardBody } from "@/design-system/base/card";
|
import { Card, CardBody } from "@/design-system/base/card";
|
||||||
import { Input } from "@/design-system/base/input";
|
import { Input } from "@/design-system/base/input";
|
||||||
import { PrimaryButton } from "@/design-system/base/button";
|
import { PrimaryButton } from "@/design-system/base/button";
|
||||||
import { VStack } from "@/design-system/layout/stack";
|
import { VStack } from "@/design-system/layout/stack";
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
|
const t = useTranslations("auth");
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -19,37 +20,43 @@ export default function LoginPage() {
|
|||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const redirectTo = searchParams.get("redirect");
|
const redirectTo = searchParams.get("redirect");
|
||||||
|
|
||||||
const session = authClient.useSession().data;
|
const { data: session, isPending } = authClient.useSession();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (session) {
|
if (!isPending && session?.user?.username && !redirectTo) {
|
||||||
router.push(redirectTo ?? "/profile");
|
router.push("/folders");
|
||||||
}
|
}
|
||||||
}, [session, router, redirectTo]);
|
}, [session, isPending, router, redirectTo]);
|
||||||
|
|
||||||
const handleLogin = async () => {
|
const handleLogin = async () => {
|
||||||
if (!username || !password) {
|
if (!username || !password) {
|
||||||
toast.error("请输入用户名和密码");
|
toast.error(t("enterCredentials"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
if (username.includes("@")) {
|
if (username.includes("@")) {
|
||||||
await authClient.signIn.email({
|
const { error } = await authClient.signIn.email({
|
||||||
email: username,
|
email: username,
|
||||||
password: username
|
password: password,
|
||||||
});
|
});
|
||||||
|
if (error) {
|
||||||
|
toast.error(error.message ?? t("loginFailed"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await authClient.signIn.username({
|
const { error } = await authClient.signIn.username({
|
||||||
username: username,
|
username: username,
|
||||||
password: password,
|
password: password,
|
||||||
});
|
});
|
||||||
|
if (error) {
|
||||||
|
toast.error(error.message ?? t("loginFailed"));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
router.push(redirectTo ?? "/profile");
|
}
|
||||||
} catch (error) {
|
router.push(redirectTo ?? "/folders");
|
||||||
toast.error("登录失败");
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -57,39 +64,46 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center items-center min-h-screen">
|
<div className="flex justify-center items-center min-h-screen">
|
||||||
<Card className="w-80">
|
<Card className="w-96">
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<VStack gap={4} align="center" justify="center">
|
<VStack gap={4} align="center" justify="center">
|
||||||
<h1 className="text-3xl font-bold text-center w-full">登录</h1>
|
<h1 className="text-3xl font-bold text-center w-full">{t("title")}</h1>
|
||||||
|
|
||||||
<VStack gap={0} align="center" justify="center" className="w-full">
|
<VStack gap={0} align="center" justify="center" className="w-full">
|
||||||
<Input
|
<Input
|
||||||
placeholder="用户名或邮箱地址"
|
placeholder={t("usernameOrEmailPlaceholder")}
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="密码"
|
placeholder={t("passwordPlaceholder")}
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href="/forgot-password"
|
||||||
|
className="text-sm text-gray-500 hover:text-primary-500 self-end"
|
||||||
|
>
|
||||||
|
{t("forgotPassword")}
|
||||||
|
</Link>
|
||||||
|
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
onClick={handleLogin}
|
onClick={handleLogin}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
fullWidth
|
fullWidth
|
||||||
>
|
>
|
||||||
确认
|
{t("confirm")}
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href={"/signup" + (redirectTo ? `?redirect=${redirectTo}` : "")}
|
href={"/signup" + (redirectTo ? `?redirect=${redirectTo}` : "")}
|
||||||
className="text-center text-primary-500 hover:underline"
|
className="text-center text-primary-500 hover:underline"
|
||||||
>
|
>
|
||||||
没有账号?去注册
|
{t("noAccountLink")}
|
||||||
</Link>
|
</Link>
|
||||||
</VStack>
|
</VStack>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import { headers } from "next/headers";
|
|||||||
export default async function ProfilePage() {
|
export default async function ProfilePage() {
|
||||||
const session = await auth.api.getSession({ headers: await headers() });
|
const session = await auth.api.getSession({ headers: await headers() });
|
||||||
|
|
||||||
if (!session) {
|
if (!session?.user?.id) {
|
||||||
redirect("/login?redirect=/profile");
|
redirect("/login?redirect=/profile");
|
||||||
}
|
}
|
||||||
|
|
||||||
redirect(`/users/${session.user.username}`);
|
redirect(session.user.username ? `/users/${session.user.username}` : "/folders");
|
||||||
}
|
}
|
||||||
|
|||||||
154
src/app/(auth)/reset-password/page.tsx
Normal file
154
src/app/(auth)/reset-password/page.tsx
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { Card, CardBody } from "@/design-system/base/card";
|
||||||
|
import { Input } from "@/design-system/base/input";
|
||||||
|
import { PrimaryButton } from "@/design-system/base/button";
|
||||||
|
import { VStack } from "@/design-system/layout/stack";
|
||||||
|
|
||||||
|
export default function ResetPasswordPage() {
|
||||||
|
const t = useTranslations("auth");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [success, setSuccess] = useState(false);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const token = searchParams.get("token");
|
||||||
|
|
||||||
|
const handleResetPassword = async () => {
|
||||||
|
if (!password || !confirmPassword) {
|
||||||
|
toast.error(t("fillAllFields"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
toast.error(t("passwordsNotMatch"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 8) {
|
||||||
|
toast.error(t("passwordTooShort"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
toast.error(t("invalidToken"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
const { error } = await authClient.resetPassword({
|
||||||
|
newPassword: password,
|
||||||
|
token,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
toast.error(error.message ?? t("resetPasswordFailed"));
|
||||||
|
} else {
|
||||||
|
setSuccess(true);
|
||||||
|
toast.success(t("resetPasswordSuccess"));
|
||||||
|
setTimeout(() => {
|
||||||
|
router.push("/login");
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center min-h-screen">
|
||||||
|
<Card className="w-96">
|
||||||
|
<CardBody>
|
||||||
|
<VStack gap={4} align="center" justify="center">
|
||||||
|
<h1 className="text-2xl font-bold text-center w-full">
|
||||||
|
{t("resetPasswordSuccessTitle")}
|
||||||
|
</h1>
|
||||||
|
<p className="text-center text-gray-600">
|
||||||
|
{t("resetPasswordSuccessHint")}
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
href="/login"
|
||||||
|
className="text-primary-500 hover:underline"
|
||||||
|
>
|
||||||
|
{t("backToLogin")}
|
||||||
|
</Link>
|
||||||
|
</VStack>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center min-h-screen">
|
||||||
|
<Card className="w-96">
|
||||||
|
<CardBody>
|
||||||
|
<VStack gap={4} align="center" justify="center">
|
||||||
|
<h1 className="text-2xl font-bold text-center w-full">
|
||||||
|
{t("invalidToken")}
|
||||||
|
</h1>
|
||||||
|
<p className="text-center text-gray-600">
|
||||||
|
{t("invalidTokenHint")}
|
||||||
|
</p>
|
||||||
|
<Link
|
||||||
|
href="/forgot-password"
|
||||||
|
className="text-primary-500 hover:underline"
|
||||||
|
>
|
||||||
|
{t("requestNewToken")}
|
||||||
|
</Link>
|
||||||
|
</VStack>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center items-center min-h-screen">
|
||||||
|
<Card className="w-96">
|
||||||
|
<CardBody>
|
||||||
|
<VStack gap={4} align="center" justify="center">
|
||||||
|
<h1 className="text-3xl font-bold text-center w-full">
|
||||||
|
{t("resetPassword")}
|
||||||
|
</h1>
|
||||||
|
<VStack gap={0} align="center" justify="center" className="w-full">
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder={t("newPassword")}
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder={t("confirmPassword")}
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
</VStack>
|
||||||
|
<PrimaryButton
|
||||||
|
onClick={handleResetPassword}
|
||||||
|
loading={loading}
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
{t("resetPassword")}
|
||||||
|
</PrimaryButton>
|
||||||
|
<Link
|
||||||
|
href="/login"
|
||||||
|
className="text-center text-primary-500 hover:underline"
|
||||||
|
>
|
||||||
|
{t("backToLogin")}
|
||||||
|
</Link>
|
||||||
|
</VStack>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -6,12 +6,14 @@ import Link from "next/link";
|
|||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
import { Card, CardBody } from "@/design-system/base/card";
|
import { Card, CardBody } from "@/design-system/base/card";
|
||||||
import { Input } from "@/design-system/base/input";
|
import { Input } from "@/design-system/base/input";
|
||||||
import { PrimaryButton } from "@/design-system/base/button";
|
import { PrimaryButton } from "@/design-system/base/button";
|
||||||
import { VStack } from "@/design-system/layout/stack";
|
import { VStack } from "@/design-system/layout/stack";
|
||||||
|
|
||||||
export default function SignUpPage() {
|
export default function SignUpPage() {
|
||||||
|
const t = useTranslations("auth");
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
@@ -20,32 +22,34 @@ export default function SignUpPage() {
|
|||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const redirectTo = searchParams.get("redirect");
|
const redirectTo = searchParams.get("redirect");
|
||||||
|
|
||||||
const session = authClient.useSession().data;
|
const { data: session, isPending } = authClient.useSession();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (session) {
|
if (!isPending && session?.user?.username && !redirectTo) {
|
||||||
router.push(redirectTo ?? "/profile");
|
router.push("/folders");
|
||||||
}
|
}
|
||||||
}, [session, router, redirectTo]);
|
}, [session, isPending, router, redirectTo]);
|
||||||
|
|
||||||
const handleSignUp = async () => {
|
const handleSignUp = async () => {
|
||||||
if (!username || !email || !password) {
|
if (!username || !email || !password) {
|
||||||
toast.error("请填写所有字段");
|
toast.error(t("fillAllFields"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
await authClient.signUp.email({
|
const { error } = await authClient.signUp.email({
|
||||||
email: email,
|
email: email,
|
||||||
name: username,
|
name: username,
|
||||||
username: username,
|
username: username,
|
||||||
password: password,
|
password: password,
|
||||||
});
|
});
|
||||||
router.push(redirectTo ?? "/profile");
|
if (error) {
|
||||||
} catch (error) {
|
toast.error(error.message ?? t("signUpFailed"));
|
||||||
toast.error("注册失败");
|
return;
|
||||||
|
}
|
||||||
|
router.push(redirectTo ?? "/folders");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -53,28 +57,28 @@ export default function SignUpPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center items-center min-h-screen">
|
<div className="flex justify-center items-center min-h-screen">
|
||||||
<Card className="w-80">
|
<Card className="w-96">
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<VStack gap={4} align="center" justify="center">
|
<VStack gap={4} align="center" justify="center">
|
||||||
<h1 className="text-3xl font-bold text-center w-full">注册</h1>
|
<h1 className="text-3xl font-bold text-center w-full">{t("signUpTitle")}</h1>
|
||||||
|
|
||||||
<VStack gap={0} align="center" justify="center" className="w-full">
|
<VStack gap={0} align="center" justify="center" className="w-full">
|
||||||
<Input
|
<Input
|
||||||
placeholder="用户名"
|
placeholder={t("usernamePlaceholder")}
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
type="email"
|
type="email"
|
||||||
placeholder="邮箱地址"
|
placeholder={t("emailPlaceholder")}
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="密码"
|
placeholder={t("passwordPlaceholder")}
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
/>
|
/>
|
||||||
@@ -85,14 +89,14 @@ export default function SignUpPage() {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
fullWidth
|
fullWidth
|
||||||
>
|
>
|
||||||
确认
|
{t("confirm")}
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href={"/login" + (redirectTo ? `?redirect=${redirectTo}` : "")}
|
href={"/login" + (redirectTo ? `?redirect=${redirectTo}` : "")}
|
||||||
className="text-center text-primary-500 hover:underline"
|
className="text-center text-primary-500 hover:underline"
|
||||||
>
|
>
|
||||||
已有账号?去登录
|
{t("hasAccountLink")}
|
||||||
</Link>
|
</Link>
|
||||||
</VStack>
|
</VStack>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export default async function UserPage({ params }: UserPageProps) {
|
|||||||
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
|
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div></div>
|
<div></div>
|
||||||
{isOwnProfile && <LinkButton href="/logout">登出</LinkButton>}
|
{isOwnProfile && <LinkButton href="/logout">{t("logout")}</LinkButton>}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-6">
|
<div className="flex items-center space-x-6">
|
||||||
{/* Avatar */}
|
{/* Avatar */}
|
||||||
|
|||||||
@@ -54,8 +54,8 @@ export default function Alphabet() {
|
|||||||
{t("chooseCharacters")}
|
{t("chooseCharacters")}
|
||||||
</h1>
|
</h1>
|
||||||
{/* 副标题说明 */}
|
{/* 副标题说明 */}
|
||||||
<p className="text-gray-600 mb-8 text-lg">
|
<p className="text-lg text-gray-600 text-center">
|
||||||
选择一种语言的字母表开始学习
|
{t("chooseAlphabetHint")}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* 语言选择按钮网格 */}
|
{/* 语言选择按钮网格 */}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { Plus, RefreshCw } from "lucide-react";
|
|||||||
import { DictionaryEntry } from "./DictionaryEntry";
|
import { DictionaryEntry } from "./DictionaryEntry";
|
||||||
import { LanguageSelector } from "./LanguageSelector";
|
import { LanguageSelector } from "./LanguageSelector";
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { actionGetFoldersByUserId, actionCreatePair } from "@/modules/folder/folder-aciton";
|
import { actionGetFoldersByUserId, actionCreatePair } from "@/modules/folder/folder-action";
|
||||||
import { TSharedFolder } from "@/shared/folder-type";
|
import { TSharedFolder } from "@/shared/folder-type";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
@@ -90,11 +90,11 @@ export function DictionaryClient({ initialFolders }: DictionaryClientProps) {
|
|||||||
const folderSelect = document.getElementById("folder-select") as HTMLSelectElement;
|
const folderSelect = document.getElementById("folder-select") as HTMLSelectElement;
|
||||||
const folderId = folderSelect?.value ? Number(folderSelect.value) : folders[0]?.id;
|
const folderId = folderSelect?.value ? Number(folderSelect.value) : folders[0]?.id;
|
||||||
|
|
||||||
if (!searchResult) return;
|
if (!searchResult?.entries?.length) return;
|
||||||
|
|
||||||
const definition = searchResult.entries.reduce((p, e) => {
|
const definition = searchResult.entries
|
||||||
return { ...p, definition: p.definition + ' | ' + e.definition };
|
.map((e) => e.definition)
|
||||||
}).definition;
|
.join(" | ");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await actionCreatePair({
|
await actionCreatePair({
|
||||||
@@ -102,7 +102,7 @@ export function DictionaryClient({ initialFolders }: DictionaryClientProps) {
|
|||||||
text2: definition,
|
text2: definition,
|
||||||
language1: queryLang,
|
language1: queryLang,
|
||||||
language2: definitionLang,
|
language2: definitionLang,
|
||||||
ipa1: searchResult.entries[0].ipa,
|
ipa1: searchResult.entries[0]?.ipa,
|
||||||
folderId: folderId,
|
folderId: folderId,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -133,10 +133,11 @@ export function DictionaryClient({ initialFolders }: DictionaryClientProps) {
|
|||||||
placeholder={t("searchPlaceholder")}
|
placeholder={t("searchPlaceholder")}
|
||||||
variant="search"
|
variant="search"
|
||||||
required
|
required
|
||||||
|
containerClassName="flex-1"
|
||||||
/>
|
/>
|
||||||
<LightButton
|
<LightButton
|
||||||
type="submit"
|
type="submit"
|
||||||
className="px-6 py-3 whitespace-nowrap text-center sm:min-w-30"
|
className="h-10 px-6 rounded-full whitespace-nowrap"
|
||||||
loading={isSearching}
|
loading={isSearching}
|
||||||
>
|
>
|
||||||
{t("search")}
|
{t("search")}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { TSharedEntry } from "@/shared/dictionary-type";
|
import { TSharedEntry } from "@/shared/dictionary-type";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
interface DictionaryEntryProps {
|
interface DictionaryEntryProps {
|
||||||
entry: TSharedEntry;
|
entry: TSharedEntry;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DictionaryEntry({ entry }: DictionaryEntryProps) {
|
export function DictionaryEntry({ entry }: DictionaryEntryProps) {
|
||||||
|
const t = useTranslations("dictionary");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{/* 音标和词性 */}
|
|
||||||
<div className="flex items-center gap-3 mb-3">
|
<div className="flex items-center gap-3 mb-3">
|
||||||
{entry.ipa && (
|
{entry.ipa && (
|
||||||
<span className="text-gray-600 text-lg">
|
<span className="text-gray-600 text-lg">
|
||||||
@@ -21,19 +23,17 @@ export function DictionaryEntry({ entry }: DictionaryEntryProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 释义 */}
|
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<h3 className="text-sm font-semibold text-gray-700 mb-1">
|
<h3 className="text-sm font-semibold text-gray-700 mb-1">
|
||||||
释义
|
{t("definition")}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-800">{entry.definition}</p>
|
<p className="text-gray-800">{entry.definition}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 例句 */}
|
|
||||||
{entry.example && (
|
{entry.example && (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-semibold text-gray-700 mb-1">
|
<h3 className="text-sm font-semibold text-gray-700 mb-1">
|
||||||
例句
|
{t("example")}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-700 pl-4 border-l-4 border-[#35786f]">
|
<p className="text-gray-700 pl-4 border-l-4 border-[#35786f]">
|
||||||
{entry.example}
|
{entry.example}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { DictionaryClient } from "./DictionaryClient";
|
import { DictionaryClient } from "./DictionaryClient";
|
||||||
import { auth } from "@/auth";
|
import { auth } from "@/auth";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { actionGetFoldersByUserId } from "@/modules/folder/folder-aciton";
|
import { actionGetFoldersByUserId } from "@/modules/folder/folder-action";
|
||||||
import { TSharedFolder } from "@/shared/folder-type";
|
import { TSharedFolder } from "@/shared/folder-type";
|
||||||
|
|
||||||
export default async function DictionaryPage() {
|
export default async function DictionaryPage() {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
actionSearchPublicFolders,
|
actionSearchPublicFolders,
|
||||||
actionToggleFavorite,
|
actionToggleFavorite,
|
||||||
actionCheckFavorite,
|
actionCheckFavorite,
|
||||||
} from "@/modules/folder/folder-aciton";
|
} from "@/modules/folder/folder-action";
|
||||||
import { TPublicFolder } from "@/shared/folder-type";
|
import { TPublicFolder } from "@/shared/folder-type";
|
||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
|
||||||
|
|||||||
146
src/app/(features)/explore/[id]/ExploreDetailClient.tsx
Normal file
146
src/app/(features)/explore/[id]/ExploreDetailClient.tsx
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Folder as Fd, Heart, ExternalLink, ArrowLeft } from "lucide-react";
|
||||||
|
import { CircleButton } from "@/design-system/base/button";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import Link from "next/link";
|
||||||
|
import {
|
||||||
|
actionToggleFavorite,
|
||||||
|
actionCheckFavorite,
|
||||||
|
} from "@/modules/folder/folder-action";
|
||||||
|
import { ActionOutputPublicFolder } from "@/modules/folder/folder-action-dto";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
|
||||||
|
interface ExploreDetailClientProps {
|
||||||
|
folder: ActionOutputPublicFolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ExploreDetailClient({ folder }: ExploreDetailClientProps) {
|
||||||
|
const router = useRouter();
|
||||||
|
const t = useTranslations("exploreDetail");
|
||||||
|
const [isFavorited, setIsFavorited] = useState(false);
|
||||||
|
const [favoriteCount, setFavoriteCount] = useState(folder.favoriteCount);
|
||||||
|
|
||||||
|
const { data: session } = authClient.useSession();
|
||||||
|
const currentUserId = session?.user?.id;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentUserId) {
|
||||||
|
actionCheckFavorite(folder.id).then((result) => {
|
||||||
|
if (result.success && result.data) {
|
||||||
|
setIsFavorited(result.data.isFavorited);
|
||||||
|
setFavoriteCount(result.data.favoriteCount);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [folder.id, currentUserId]);
|
||||||
|
|
||||||
|
const handleToggleFavorite = async () => {
|
||||||
|
if (!currentUserId) {
|
||||||
|
toast.error(t("pleaseLogin"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = await actionToggleFavorite(folder.id);
|
||||||
|
if (result.success && result.data) {
|
||||||
|
setIsFavorited(result.data.isFavorited);
|
||||||
|
setFavoriteCount(result.data.favoriteCount);
|
||||||
|
toast.success(
|
||||||
|
result.data.isFavorited ? t("favorited") : t("unfavorited")
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
toast.error(result.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (date: Date) => {
|
||||||
|
return new Intl.DateTimeFormat("zh-CN", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
}).format(new Date(date));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-50">
|
||||||
|
<div className="max-w-3xl mx-auto px-4 py-6 sm:py-8">
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<CircleButton onClick={() => router.push("/explore")}>
|
||||||
|
<ArrowLeft size={18} />
|
||||||
|
</CircleButton>
|
||||||
|
<h1 className="text-lg sm:text-xl font-semibold text-gray-900">
|
||||||
|
{t("title")}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white border border-gray-200 rounded-xl p-5 sm:p-8 shadow-sm">
|
||||||
|
<div className="flex items-start justify-between mb-6">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="w-14 h-14 sm:w-16 sm:h-16 rounded-xl bg-primary-50 flex items-center justify-center text-primary-500">
|
||||||
|
<Fd size={28} className="sm:w-8 sm:h-8" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl sm:text-2xl font-bold text-gray-900">
|
||||||
|
{folder.name}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-gray-500 mt-1">
|
||||||
|
{t("createdBy", {
|
||||||
|
name: folder.userName ?? folder.userUsername ?? t("unknownUser"),
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CircleButton
|
||||||
|
onClick={handleToggleFavorite}
|
||||||
|
title={isFavorited ? t("unfavorite") : t("favorite")}
|
||||||
|
className="shrink-0"
|
||||||
|
>
|
||||||
|
<Heart
|
||||||
|
size={20}
|
||||||
|
className={isFavorited ? "fill-red-500 text-red-500" : ""}
|
||||||
|
/>
|
||||||
|
</CircleButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-4 mb-6 py-4 border-y border-gray-100">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-2xl sm:text-3xl font-bold text-primary-600">
|
||||||
|
{folder.totalPairs}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs sm:text-sm text-gray-500 mt-1">
|
||||||
|
{t("totalPairs")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center border-x border-gray-100">
|
||||||
|
<div className="text-2xl sm:text-3xl font-bold text-red-500 flex items-center justify-center gap-1">
|
||||||
|
<Heart size={18} className={isFavorited ? "fill-red-500" : ""} />
|
||||||
|
{favoriteCount}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs sm:text-sm text-gray-500 mt-1">
|
||||||
|
{t("favorites")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-lg sm:text-xl font-semibold text-gray-700">
|
||||||
|
{formatDate(folder.createdAt)}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs sm:text-sm text-gray-500 mt-1">
|
||||||
|
{t("createdAt")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
href={`/folders/${folder.id}`}
|
||||||
|
className="flex items-center justify-center gap-2 w-full py-3 px-4 bg-primary-500 hover:bg-primary-600 text-white rounded-lg font-medium transition-colors"
|
||||||
|
>
|
||||||
|
<ExternalLink size={18} />
|
||||||
|
{t("viewContent")}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { InFolder } from "@/app/folders/[folder_id]/InFolder";
|
import { ExploreDetailClient } from "./ExploreDetailClient";
|
||||||
import { actionGetFolderVisibility } from "@/modules/folder/folder-aciton";
|
import { actionGetPublicFolderById } from "@/modules/folder/folder-action";
|
||||||
|
|
||||||
export default async function ExploreFolderPage({
|
export default async function ExploreFolderPage({
|
||||||
params,
|
params,
|
||||||
@@ -13,17 +13,11 @@ export default async function ExploreFolderPage({
|
|||||||
redirect("/explore");
|
redirect("/explore");
|
||||||
}
|
}
|
||||||
|
|
||||||
const folderInfo = (await actionGetFolderVisibility(Number(id))).data;
|
const result = await actionGetPublicFolderById(Number(id));
|
||||||
|
|
||||||
if (!folderInfo) {
|
if (!result.success || !result.data) {
|
||||||
redirect("/explore");
|
redirect("/explore");
|
||||||
}
|
}
|
||||||
|
|
||||||
const isPublic = folderInfo.visibility === "PUBLIC";
|
return <ExploreDetailClient folder={result.data} />;
|
||||||
|
|
||||||
if (!isPublic) {
|
|
||||||
redirect("/explore");
|
|
||||||
}
|
|
||||||
|
|
||||||
return <InFolder folderId={Number(id)} isReadOnly={true} />;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ExploreClient } from "./ExploreClient";
|
import { ExploreClient } from "./ExploreClient";
|
||||||
import { actionGetPublicFolders } from "@/modules/folder/folder-aciton";
|
import { actionGetPublicFolders } from "@/modules/folder/folder-action";
|
||||||
|
|
||||||
export default async function ExplorePage() {
|
export default async function ExplorePage() {
|
||||||
const publicFoldersResult = await actionGetPublicFolders();
|
const publicFoldersResult = await actionGetPublicFolders();
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { toast } from "sonner";
|
|||||||
import { PageLayout } from "@/components/ui/PageLayout";
|
import { PageLayout } from "@/components/ui/PageLayout";
|
||||||
import { PageHeader } from "@/components/ui/PageHeader";
|
import { PageHeader } from "@/components/ui/PageHeader";
|
||||||
import { CardList } from "@/components/ui/CardList";
|
import { CardList } from "@/components/ui/CardList";
|
||||||
import { actionGetUserFavorites, actionToggleFavorite } from "@/modules/folder/folder-aciton";
|
import { actionGetUserFavorites, actionToggleFavorite } from "@/modules/folder/folder-action";
|
||||||
|
|
||||||
type UserFavorite = {
|
type UserFavorite = {
|
||||||
id: number;
|
id: number;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { FolderSelector } from "./FolderSelector";
|
|||||||
import { Memorize } from "./Memorize";
|
import { Memorize } from "./Memorize";
|
||||||
import { auth } from "@/auth";
|
import { auth } from "@/auth";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { actionGetFoldersWithTotalPairsByUserId, actionGetPairsByFolderId } from "@/modules/folder/folder-aciton";
|
import { actionGetFoldersWithTotalPairsByUserId, actionGetPairsByFolderId } from "@/modules/folder/folder-action";
|
||||||
|
|
||||||
export default async function MemorizePage({
|
export default async function MemorizePage({
|
||||||
searchParams,
|
searchParams,
|
||||||
|
|||||||
@@ -127,21 +127,21 @@ export default function SrtPlayerPage() {
|
|||||||
<div className="border-gray-200 border-2 flex items p-2 justify-between items-center rounded gap-8">
|
<div className="border-gray-200 border-2 flex items p-2 justify-between items-center rounded gap-8">
|
||||||
<div className="flex items-center flex-col">
|
<div className="flex items-center flex-col">
|
||||||
<Video size={16} />
|
<Video size={16} />
|
||||||
<span className="text-sm">视频文件</span>
|
<span className="text-sm">{srtT("videoFile")}</span>
|
||||||
</div>
|
</div>
|
||||||
<LightButton onClick={handleVideoUpload} disabled={!!videoUrl}>
|
<LightButton onClick={handleVideoUpload} disabled={!!videoUrl}>
|
||||||
{videoUrl ? '已上传' : '上传视频'}
|
{videoUrl ? srtT("uploaded") : srtT("uploadVideoButton")}
|
||||||
</LightButton>
|
</LightButton>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-gray-200 border-2 flex items p-2 justify-between items-center rounded gap-8">
|
<div className="border-gray-200 border-2 flex items p-2 justify-between items-center rounded gap-8">
|
||||||
<div className="flex items-center flex-col">
|
<div className="flex items-center flex-col">
|
||||||
<FileText size={16} />
|
<FileText size={16} />
|
||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
{subtitleData.length > 0 ? `字幕已上传 (${subtitleData.length} 条)` : "字幕未上传"}
|
{subtitleData.length > 0 ? srtT("subtitleUploaded", { count: subtitleData.length }) : srtT("subtitleNotUploaded")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<LightButton onClick={handleSubtitleUpload} disabled={!!subtitleUrl}>
|
<LightButton onClick={handleSubtitleUpload} disabled={!!subtitleUrl}>
|
||||||
{subtitleUrl ? '已上传' : '上传字幕'}
|
{subtitleUrl ? srtT("uploaded") : srtT("uploadSubtitleButton")}
|
||||||
</LightButton>
|
</LightButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -62,13 +62,12 @@ export function getNearestIndex(
|
|||||||
): number | null {
|
): number | null {
|
||||||
for (let i = 0; i < subtitles.length; i++) {
|
for (let i = 0; i < subtitles.length; i++) {
|
||||||
const subtitle = subtitles[i];
|
const subtitle = subtitles[i];
|
||||||
const isBefore = currentTime - subtitle.start >= 0;
|
const isWithin = currentTime >= subtitle.start && currentTime <= subtitle.end;
|
||||||
const isAfter = currentTime - subtitle.end >= 0;
|
|
||||||
|
|
||||||
if (!isBefore || !isAfter) return i - 1;
|
if (isWithin) return i;
|
||||||
if (isBefore && !isAfter) return i;
|
if (currentTime < subtitle.start) return i > 0 ? i - 1 : null;
|
||||||
}
|
}
|
||||||
return null;
|
return subtitles.length > 0 ? subtitles.length - 1 : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCurrentSubtitle(
|
export function getCurrentSubtitle(
|
||||||
|
|||||||
@@ -60,11 +60,12 @@ export function SaveList({ show = false, handleUse }: SaveListProps) {
|
|||||||
const [data, setData] = useState(getFromLocalStorage());
|
const [data, setData] = useState(getFromLocalStorage());
|
||||||
const handleDel = (item: z.infer<typeof TextSpeakerItemSchema>) => {
|
const handleDel = (item: z.infer<typeof TextSpeakerItemSchema>) => {
|
||||||
const current_data = getFromLocalStorage();
|
const current_data = getFromLocalStorage();
|
||||||
|
if (!current_data) return;
|
||||||
|
|
||||||
current_data.splice(
|
const index = current_data.findIndex((v) => v.text === item.text);
|
||||||
current_data.findIndex((v) => v.text === item.text),
|
if (index === -1) return;
|
||||||
1,
|
|
||||||
);
|
current_data.splice(index, 1);
|
||||||
setIntoLocalStorage(current_data);
|
setIntoLocalStorage(current_data);
|
||||||
refresh();
|
refresh();
|
||||||
};
|
};
|
||||||
@@ -78,33 +79,25 @@ export function SaveList({ show = false, handleUse }: SaveListProps) {
|
|||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (show)
|
if (show && data)
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="my-4 p-2 mx-4 md:mx-32 border border-gray-200 rounded-lg"
|
className="my-4 p-2 mx-4 md:mx-32 border border-gray-200 rounded-lg"
|
||||||
style={{ fontFamily: "Times New Roman, serif" }}
|
|
||||||
>
|
>
|
||||||
<div className="flex flex-row justify-center gap-8 items-center">
|
<div className="flex justify-between items-center mb-2">
|
||||||
<IconClick
|
<p className="text-sm text-gray-600">{t("saved")}</p>
|
||||||
src={IMAGES.refresh}
|
<button
|
||||||
alt="refresh"
|
|
||||||
onClick={refresh}
|
|
||||||
size="lg"
|
|
||||||
className=""
|
|
||||||
></IconClick>
|
|
||||||
<IconClick
|
|
||||||
src={IMAGES.delete}
|
|
||||||
alt="delete"
|
|
||||||
onClick={handleDeleteAll}
|
onClick={handleDeleteAll}
|
||||||
size="lg"
|
className="text-xs text-gray-500 hover:text-gray-800"
|
||||||
className=""
|
>
|
||||||
></IconClick>
|
{t("clearAll")}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<ul>
|
<ul className="divide-y divide-gray-100">
|
||||||
{data.map((v) => (
|
{data.map((item, i) => (
|
||||||
<TextCard
|
<TextCard
|
||||||
item={v}
|
key={i}
|
||||||
key={crypto.randomUUID()}
|
item={item}
|
||||||
handleUse={handleUse}
|
handleUse={handleUse}
|
||||||
handleDel={handleDel}
|
handleDel={handleDel}
|
||||||
></TextCard>
|
></TextCard>
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ export default function TextSpeakerPage() {
|
|||||||
const handleEnded = () => {
|
const handleEnded = () => {
|
||||||
if (autopause) {
|
if (autopause) {
|
||||||
setPause(true);
|
setPause(true);
|
||||||
} else {
|
} else if (objurlRef.current) {
|
||||||
load(objurlRef.current!);
|
load(objurlRef.current);
|
||||||
play();
|
play();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -187,7 +187,7 @@ export default function TextSpeakerPage() {
|
|||||||
theIPA = tmp_ipa;
|
theIPA = tmp_ipa;
|
||||||
}
|
}
|
||||||
|
|
||||||
const save = getFromLocalStorage();
|
const save = getFromLocalStorage() ?? [];
|
||||||
const oldIndex = save.findIndex((v) => v.text === textRef.current);
|
const oldIndex = save.findIndex((v) => v.text === textRef.current);
|
||||||
if (oldIndex !== -1) {
|
if (oldIndex !== -1) {
|
||||||
const oldItem = save[oldIndex];
|
const oldItem = save[oldIndex];
|
||||||
@@ -293,7 +293,7 @@ export default function TextSpeakerPage() {
|
|||||||
size="lg"
|
size="lg"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setAutopause(!autopause);
|
setAutopause(!autopause);
|
||||||
if (objurlRef) {
|
if (objurlRef.current) {
|
||||||
stop();
|
stop();
|
||||||
}
|
}
|
||||||
setPause(true);
|
setPause(true);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
actionGetFoldersWithTotalPairsByUserId,
|
actionGetFoldersWithTotalPairsByUserId,
|
||||||
actionRenameFolderById,
|
actionRenameFolderById,
|
||||||
actionSetFolderVisibility,
|
actionSetFolderVisibility,
|
||||||
} from "@/modules/folder/folder-aciton";
|
} from "@/modules/folder/folder-action";
|
||||||
import { TSharedFolderWithTotalPairs } from "@/shared/folder-type";
|
import { TSharedFolderWithTotalPairs } from "@/shared/folder-type";
|
||||||
|
|
||||||
interface FolderCardProps {
|
interface FolderCardProps {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { useTranslations } from "next-intl";
|
|||||||
import { PageLayout } from "@/components/ui/PageLayout";
|
import { PageLayout } from "@/components/ui/PageLayout";
|
||||||
import { PrimaryButton, CircleButton, LinkButton } from "@/design-system/base/button";
|
import { PrimaryButton, CircleButton, LinkButton } from "@/design-system/base/button";
|
||||||
import { CardList } from "@/components/ui/CardList";
|
import { CardList } from "@/components/ui/CardList";
|
||||||
import { actionCreatePair, actionDeletePairById, actionGetPairsByFolderId } from "@/modules/folder/folder-aciton";
|
import { actionCreatePair, actionDeletePairById, actionGetPairsByFolderId } from "@/modules/folder/folder-action";
|
||||||
import { TSharedPair } from "@/shared/folder-type";
|
import { TSharedPair } from "@/shared/folder-type";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
@@ -26,10 +26,14 @@ export function InFolder({ folderId, isReadOnly }: { folderId: number; isReadOnl
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
await actionGetPairsByFolderId(folderId)
|
await actionGetPairsByFolderId(folderId)
|
||||||
.then(result => {
|
.then(result => {
|
||||||
if (!result.success || !result.data) throw result.message;
|
if (!result.success || !result.data) {
|
||||||
|
throw new Error(result.message || "Failed to load text pairs");
|
||||||
|
}
|
||||||
return result.data;
|
return result.data;
|
||||||
}).then(setTextPairs)
|
}).then(setTextPairs)
|
||||||
.catch(toast.error)
|
.catch((error) => {
|
||||||
|
toast.error(error instanceof Error ? error.message : "Unknown error");
|
||||||
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
@@ -40,10 +44,14 @@ export function InFolder({ folderId, isReadOnly }: { folderId: number; isReadOnl
|
|||||||
const refreshTextPairs = async () => {
|
const refreshTextPairs = async () => {
|
||||||
await actionGetPairsByFolderId(folderId)
|
await actionGetPairsByFolderId(folderId)
|
||||||
.then(result => {
|
.then(result => {
|
||||||
if (!result.success || !result.data) throw result.message;
|
if (!result.success || !result.data) {
|
||||||
|
throw new Error(result.message || "Failed to refresh text pairs");
|
||||||
|
}
|
||||||
return result.data;
|
return result.data;
|
||||||
}).then(setTextPairs)
|
}).then(setTextPairs)
|
||||||
.catch(toast.error);
|
.catch((error) => {
|
||||||
|
toast.error(error instanceof Error ? error.message : "Unknown error");
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -119,9 +127,11 @@ export function InFolder({ folderId, isReadOnly }: { folderId: number; isReadOnl
|
|||||||
onDel={() => {
|
onDel={() => {
|
||||||
actionDeletePairById(textPair.id)
|
actionDeletePairById(textPair.id)
|
||||||
.then(result => {
|
.then(result => {
|
||||||
if (!result.success) throw result.message;
|
if (!result.success) throw new Error(result.message || "Delete failed");
|
||||||
}).then(refreshTextPairs)
|
}).then(refreshTextPairs)
|
||||||
.catch(toast.error);
|
.catch((error) => {
|
||||||
|
toast.error(error instanceof Error ? error.message : "Unknown error");
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
refreshTextPairs={refreshTextPairs}
|
refreshTextPairs={refreshTextPairs}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { CircleButton } from "@/design-system/base/button";
|
|||||||
import { UpdateTextPairModal } from "./UpdateTextPairModal";
|
import { UpdateTextPairModal } from "./UpdateTextPairModal";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { TSharedPair } from "@/shared/folder-type";
|
import { TSharedPair } from "@/shared/folder-type";
|
||||||
import { actionUpdatePairById } from "@/modules/folder/folder-aciton";
|
import { actionUpdatePairById } from "@/modules/folder/folder-action";
|
||||||
import { ActionInputUpdatePairById } from "@/modules/folder/folder-action-dto";
|
import { ActionInputUpdatePairById } from "@/modules/folder/folder-action-dto";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { getTranslations } from "next-intl/server";
|
|||||||
import { InFolder } from "./InFolder";
|
import { InFolder } from "./InFolder";
|
||||||
import { auth } from "@/auth";
|
import { auth } from "@/auth";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { actionGetFolderVisibility } from "@/modules/folder/folder-aciton";
|
import { actionGetFolderVisibility } from "@/modules/folder/folder-action";
|
||||||
|
|
||||||
export default async function FoldersPage({
|
export default async function FoldersPage({
|
||||||
params,
|
params,
|
||||||
|
|||||||
46
src/auth.ts
46
src/auth.ts
@@ -1,21 +1,57 @@
|
|||||||
import { betterAuth } from "better-auth";
|
import { betterAuth } from "better-auth";
|
||||||
import { prismaAdapter } from "better-auth/adapters/prisma";
|
import { prismaAdapter } from "better-auth/adapters/prisma";
|
||||||
import { nextCookies } from "better-auth/next-js";
|
import { nextCookies } from "better-auth/next-js";
|
||||||
import { prisma } from "./lib/db";
|
|
||||||
import { username } from "better-auth/plugins";
|
import { username } from "better-auth/plugins";
|
||||||
|
import { createAuthMiddleware, APIError } from "better-auth/api";
|
||||||
|
import { prisma } from "./lib/db";
|
||||||
|
import {
|
||||||
|
sendEmail,
|
||||||
|
generateVerificationEmailHtml,
|
||||||
|
generateResetPasswordEmailHtml,
|
||||||
|
} from "./lib/email";
|
||||||
|
|
||||||
export const auth = betterAuth({
|
export const auth = betterAuth({
|
||||||
database: prismaAdapter(prisma, {
|
database: prismaAdapter(prisma, {
|
||||||
provider: "postgresql"
|
provider: "postgresql",
|
||||||
}),
|
}),
|
||||||
emailAndPassword: {
|
emailAndPassword: {
|
||||||
enabled: true
|
enabled: true,
|
||||||
|
requireEmailVerification: true,
|
||||||
|
sendResetPassword: async ({ user, url }) => {
|
||||||
|
void sendEmail({
|
||||||
|
to: user.email,
|
||||||
|
subject: "重置您的密码 - Learn Languages",
|
||||||
|
html: generateResetPasswordEmailHtml(url, user.name || "用户"),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
emailVerification: {
|
||||||
|
sendOnSignUp: true,
|
||||||
|
sendVerificationEmail: async ({ user, url }) => {
|
||||||
|
void sendEmail({
|
||||||
|
to: user.email,
|
||||||
|
subject: "验证您的邮箱 - Learn Languages",
|
||||||
|
html: generateVerificationEmailHtml(url, user.name || "用户"),
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
socialProviders: {
|
socialProviders: {
|
||||||
github: {
|
github: {
|
||||||
clientId: process.env.GITHUB_CLIENT_ID as string,
|
clientId: process.env.GITHUB_CLIENT_ID as string,
|
||||||
clientSecret: process.env.GITHUB_CLIENT_SECRET as string
|
clientSecret: process.env.GITHUB_CLIENT_SECRET as string,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [nextCookies(), username()]
|
plugins: [nextCookies(), username()],
|
||||||
|
hooks: {
|
||||||
|
before: createAuthMiddleware(async (ctx) => {
|
||||||
|
if (ctx.path !== "/sign-up/email" && ctx.path !== "/update-user") return;
|
||||||
|
|
||||||
|
const body = ctx.body as { username?: string };
|
||||||
|
if (!body.username || body.username.trim() === "") {
|
||||||
|
throw new APIError("BAD_REQUEST", {
|
||||||
|
message: "Username is required",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
# 词典查询模块化架构
|
# 词典查询架构
|
||||||
|
|
||||||
本目录包含词典查询系统的**多阶段 LLM 调用**实现,将查询过程拆分为 4 个独立的 LLM 调用,每个阶段之间有代码层面的数据验证,只要有一环失败,直接返回错误。
|
2 次 LLM 调用的词典查询系统。
|
||||||
|
|
||||||
## 目录结构
|
## 目录结构
|
||||||
|
|
||||||
```
|
```
|
||||||
dictionary/
|
dictionary/
|
||||||
├── index.ts # 主导出文件
|
├── orchestrator.ts # 编排器
|
||||||
├── orchestrator.ts # 主编排器,串联所有阶段
|
├── stage1-preprocess.ts # 阶段1:预处理(输入分析+语义映射+标准形式)
|
||||||
├── types.ts # 类型定义
|
├── stage4-entriesGeneration.ts # 阶段2:词条生成
|
||||||
├── stage1-inputAnalysis.ts # 阶段1:输入解析与语言识别
|
└── types.ts # 类型定义
|
||||||
├── stage2-semanticMapping.ts # 阶段2:跨语言语义映射决策
|
|
||||||
├── stage3-standardForm.ts # 阶段3:standardForm 生成与规范化
|
|
||||||
└── stage4-entriesGeneration.ts # 阶段4:释义与词条生成
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 工作流程
|
## 工作流程
|
||||||
@@ -20,187 +17,22 @@ dictionary/
|
|||||||
```
|
```
|
||||||
用户输入
|
用户输入
|
||||||
↓
|
↓
|
||||||
[阶段1] 输入分析 → 代码验证 → 失败则返回错误
|
[阶段1] 预处理(1次LLM)→ isValid, standardForm, inputType
|
||||||
↓
|
↓
|
||||||
[阶段2] 语义映射 → 代码验证 → 失败则保守处理(不映射)
|
[阶段2] 词条生成(1次LLM)→ entries
|
||||||
↓
|
|
||||||
[阶段3] 标准形式 → 代码验证 → 失败则返回错误
|
|
||||||
↓
|
|
||||||
[阶段4] 词条生成 → 代码验证 → 失败则返回错误
|
|
||||||
↓
|
↓
|
||||||
最终结果
|
最终结果
|
||||||
```
|
```
|
||||||
|
|
||||||
## 各阶段详细说明
|
## 性能
|
||||||
|
|
||||||
### 阶段 1:输入分析
|
- 原 4 次 LLM 调用 → 现 2 次
|
||||||
|
- 预期耗时:8-13s(原 33s)
|
||||||
|
|
||||||
**文件**: `stage1-inputAnalysis.ts`
|
## 使用
|
||||||
|
|
||||||
**目的**:
|
|
||||||
- 判断输入是否有效
|
|
||||||
- 判断是「单词」还是「短语」
|
|
||||||
- 识别输入语言
|
|
||||||
|
|
||||||
**返回**: `InputAnalysisResult`
|
|
||||||
|
|
||||||
**代码验证**:
|
|
||||||
- `isValid` 必须是 boolean
|
|
||||||
- 输入为空或无效时立即返回错误
|
|
||||||
|
|
||||||
### 阶段 2:语义映射
|
|
||||||
|
|
||||||
**文件**: `stage2-semanticMapping.ts`
|
|
||||||
|
|
||||||
**目的**:
|
|
||||||
- 决定是否启用"语义级查询"
|
|
||||||
- **严格条件**:只有输入符合"明确、基础、可词典化的语义概念"且语言不一致时才映射
|
|
||||||
- 不符合条件则**直接失败**(快速失败)
|
|
||||||
|
|
||||||
**返回**: `SemanticMappingResult`
|
|
||||||
|
|
||||||
**代码验证**:
|
|
||||||
- `shouldMap` 必须是 boolean
|
|
||||||
- 如果 `shouldMap=true`,必须有 `mappedQuery`
|
|
||||||
- 如果不应该映射,**抛出异常**(不符合条件直接失败)
|
|
||||||
- **失败则直接返回错误响应**,不继续后续阶段
|
|
||||||
|
|
||||||
**映射条件**(必须同时满足):
|
|
||||||
a) 输入语言 ≠ 查询语言
|
|
||||||
b) 输入是明确、基础、可词典化的语义概念(如常见动词、名词、形容词)
|
|
||||||
|
|
||||||
**不符合条件的例子**:
|
|
||||||
- 复杂句子:"我喜欢吃苹果"
|
|
||||||
- 专业术语
|
|
||||||
- 无法确定语义的词汇
|
|
||||||
|
|
||||||
### 阶段 3:标准形式生成
|
|
||||||
|
|
||||||
**文件**: `stage3-standardForm.ts`
|
|
||||||
|
|
||||||
**目的**:
|
|
||||||
- 确定最终词条的"标准形"(整个系统的锚点)
|
|
||||||
- 修正拼写错误
|
|
||||||
- 还原为词典形式(动词原形、辞书形等)
|
|
||||||
- **如果进行了语义映射**:基于映射结果生成标准形式,同时参考原始输入的语义上下文
|
|
||||||
|
|
||||||
**参数**:
|
|
||||||
- `inputText`: 用于生成标准形式的文本(可能是映射后的结果)
|
|
||||||
- `queryLang`: 查询语言
|
|
||||||
- `originalInput`: (可选)原始用户输入,用于语义参考
|
|
||||||
|
|
||||||
**返回**: `StandardFormResult`
|
|
||||||
|
|
||||||
**代码验证**:
|
|
||||||
- `standardForm` 不能为空
|
|
||||||
- `confidence` 必须是 "high" | "medium" | "low"
|
|
||||||
- 失败时使用原输入作为标准形式
|
|
||||||
|
|
||||||
**特殊逻辑**:
|
|
||||||
- 当进行了语义映射时(即提供了 `originalInput`),阶段 3 会:
|
|
||||||
1. 基于 `inputText`(映射结果)生成标准形式
|
|
||||||
2. 参考 `originalInput` 的语义上下文,确保标准形式符合用户的真实查询意图
|
|
||||||
3. 例如:原始输入 "吃"(中文)→ 映射为 "to eat"(英语)→ 标准形式 "eat"
|
|
||||||
|
|
||||||
### 阶段 4:词条生成
|
|
||||||
|
|
||||||
**文件**: `stage4-entriesGeneration.ts`
|
|
||||||
|
|
||||||
**目的**:
|
|
||||||
- 生成真正的词典内容
|
|
||||||
- 根据类型生成单词或短语条目
|
|
||||||
|
|
||||||
**返回**: `EntriesGenerationResult`
|
|
||||||
|
|
||||||
**代码验证**:
|
|
||||||
- `entries` 必须是非空数组
|
|
||||||
- 每个条目必须有 `definition` 和 `example`
|
|
||||||
- 单词条目必须有 `partOfSpeech`
|
|
||||||
- **失败则抛出异常**(核心阶段)
|
|
||||||
|
|
||||||
## 使用方式
|
|
||||||
|
|
||||||
### 基本使用
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { lookUp } from "@/lib/server/bigmodel/dictionaryActions";
|
import { executeDictionaryLookup } from "@/lib/bigmodel/dictionary/orchestrator";
|
||||||
|
|
||||||
const result = await lookUp({
|
const result = await executeDictionaryLookup("hello", "English", "中文");
|
||||||
text: "hello",
|
|
||||||
queryLang: "English",
|
|
||||||
definitionLang: "中文"
|
|
||||||
});
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 高级使用(直接调用编排器)
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { executeDictionaryLookup } from "@/lib/server/bigmodel/dictionary";
|
|
||||||
|
|
||||||
const result = await executeDictionaryLookup(
|
|
||||||
"hello",
|
|
||||||
"English",
|
|
||||||
"中文"
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 单独测试某个阶段
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { analyzeInput } from "@/lib/server/bigmodel/dictionary";
|
|
||||||
|
|
||||||
const analysis = await analyzeInput("hello");
|
|
||||||
console.log(analysis);
|
|
||||||
```
|
|
||||||
|
|
||||||
## 设计优势
|
|
||||||
|
|
||||||
### 1. 代码层面的数据验证
|
|
||||||
每个阶段完成后都有严格的类型检查和数据验证,确保数据质量。
|
|
||||||
|
|
||||||
### 2. 快速失败
|
|
||||||
只要有一个阶段失败,立即返回错误,不浪费后续的 LLM 调用。
|
|
||||||
|
|
||||||
### 3. 可观测性
|
|
||||||
每个阶段都有 console.log 输出,方便调试和追踪问题。
|
|
||||||
|
|
||||||
### 4. 模块化
|
|
||||||
每个阶段独立文件,可以单独测试、修改或替换。
|
|
||||||
|
|
||||||
### 5. 容错性
|
|
||||||
非核心阶段(阶段2、3)失败时有降级策略,不会导致整个查询失败。
|
|
||||||
|
|
||||||
## 日志示例
|
|
||||||
|
|
||||||
```
|
|
||||||
[阶段1] 开始输入分析...
|
|
||||||
[阶段1] 输入分析完成: { isValid: true, inputType: 'word', inputLanguage: 'English' }
|
|
||||||
[阶段2] 开始语义映射...
|
|
||||||
[阶段2] 语义映射完成: { shouldMap: false }
|
|
||||||
[阶段3] 开始生成标准形式...
|
|
||||||
[阶段3] 标准形式生成完成: { standardForm: 'hello', confidence: 'high' }
|
|
||||||
[阶段4] 开始生成词条...
|
|
||||||
[阶段4] 词条生成完成: { entries: [...] }
|
|
||||||
[完成] 词典查询成功
|
|
||||||
```
|
|
||||||
|
|
||||||
## 扩展建议
|
|
||||||
|
|
||||||
### 添加缓存
|
|
||||||
对阶段1、3的结果进行缓存,避免重复调用 LLM。
|
|
||||||
|
|
||||||
### 添加指标
|
|
||||||
记录每个阶段的耗时和成功率,用于性能优化。
|
|
||||||
|
|
||||||
### 并行化
|
|
||||||
某些阶段可以并行执行(如果有依赖关系允许的话)。
|
|
||||||
|
|
||||||
### A/B 测试
|
|
||||||
为某个阶段创建不同版本的实现,进行效果对比。
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
- 每个阶段都是独立的 LLM 调用,会增加总耗时
|
|
||||||
- 需要控制 token 使用量,避免成本过高
|
|
||||||
- 错误处理要完善,避免某个阶段卡住整个流程
|
|
||||||
- 日志记录要清晰,方便问题排查
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { ServiceOutputLookUp } from "@/modules/dictionary/dictionary-service-dto";
|
import { ServiceOutputLookUp } from "@/modules/dictionary/dictionary-service-dto";
|
||||||
import { analyzeInput } from "./stage1-inputAnalysis";
|
import { preprocessInput } from "./stage1-preprocess";
|
||||||
import { determineSemanticMapping } from "./stage2-semanticMapping";
|
|
||||||
import { generateStandardForm } from "./stage3-standardForm";
|
|
||||||
import { generateEntries } from "./stage4-entriesGeneration";
|
import { generateEntries } from "./stage4-entriesGeneration";
|
||||||
import { LookUpError } from "@/lib/errors";
|
import { LookUpError } from "@/lib/errors";
|
||||||
import { createLogger } from "@/lib/logger";
|
import { createLogger } from "@/lib/logger";
|
||||||
@@ -14,64 +12,28 @@ export async function executeDictionaryLookup(
|
|||||||
definitionLang: string
|
definitionLang: string
|
||||||
): Promise<ServiceOutputLookUp> {
|
): Promise<ServiceOutputLookUp> {
|
||||||
try {
|
try {
|
||||||
log.debug("[Stage 1] Starting input analysis");
|
log.debug("[Stage 1] Preprocessing input");
|
||||||
const analysis = await analyzeInput(text);
|
const preprocessed = await preprocessInput(text, queryLang);
|
||||||
|
|
||||||
if (!analysis.isValid) {
|
if (!preprocessed.isValid) {
|
||||||
log.debug("[Stage 1] Invalid input", { reason: analysis.reason });
|
log.debug("[Stage 1] Invalid input", { reason: preprocessed.reason });
|
||||||
throw new LookUpError(analysis.reason || "无效输入");
|
throw new LookUpError(preprocessed.reason || "无效输入");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (analysis.isEmpty) {
|
log.debug("[Stage 1] Preprocess complete", { preprocessed });
|
||||||
log.debug("[Stage 1] Empty input");
|
|
||||||
throw new LookUpError("输入为空");
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("[Stage 1] Analysis complete", { analysis });
|
log.debug("[Stage 2] Generating entries");
|
||||||
|
|
||||||
log.debug("[Stage 2] Starting semantic mapping");
|
|
||||||
const semanticMapping = await determineSemanticMapping(
|
|
||||||
text,
|
|
||||||
queryLang,
|
|
||||||
analysis.inputLanguage ?? text
|
|
||||||
);
|
|
||||||
|
|
||||||
log.debug("[Stage 2] Semantic mapping complete", { semanticMapping });
|
|
||||||
|
|
||||||
log.debug("[Stage 3] Generating standard form");
|
|
||||||
|
|
||||||
// 如果进行了语义映射,标准形式要基于映射后的结果
|
|
||||||
// 同时传递原始输入作为语义参考
|
|
||||||
const shouldUseMapping = semanticMapping.shouldMap && semanticMapping.mappedQuery;
|
|
||||||
const inputForStandardForm = shouldUseMapping ? semanticMapping.mappedQuery! : text;
|
|
||||||
|
|
||||||
const standardFormResult = await generateStandardForm(
|
|
||||||
inputForStandardForm,
|
|
||||||
queryLang,
|
|
||||||
shouldUseMapping ? text : undefined // 如果进行了映射,传递原始输入作为语义参考
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!standardFormResult.standardForm) {
|
|
||||||
log.error("[Stage 3] Standard form is empty");
|
|
||||||
throw "无法生成标准形式";
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("[Stage 3] Standard form complete", { standardFormResult });
|
|
||||||
|
|
||||||
log.debug("[Stage 4] Generating entries");
|
|
||||||
const entriesResult = await generateEntries(
|
const entriesResult = await generateEntries(
|
||||||
standardFormResult.standardForm,
|
preprocessed.standardForm,
|
||||||
queryLang,
|
queryLang,
|
||||||
definitionLang,
|
definitionLang,
|
||||||
analysis.inputType === "unknown"
|
preprocessed.inputType
|
||||||
? (standardFormResult.standardForm.includes(" ") ? "phrase" : "word")
|
|
||||||
: analysis.inputType
|
|
||||||
);
|
);
|
||||||
|
|
||||||
log.debug("[Stage 4] Entries complete", { entriesResult });
|
log.debug("[Stage 2] Entries complete", { entriesResult });
|
||||||
|
|
||||||
const finalResult: ServiceOutputLookUp = {
|
const finalResult: ServiceOutputLookUp = {
|
||||||
standardForm: standardFormResult.standardForm,
|
standardForm: preprocessed.standardForm,
|
||||||
entries: entriesResult.entries,
|
entries: entriesResult.entries,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -79,8 +41,7 @@ export async function executeDictionaryLookup(
|
|||||||
return finalResult;
|
return finalResult;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("Dictionary lookup failed", { error });
|
log.error("Dictionary lookup failed", { error: error instanceof Error ? error.message : String(error) });
|
||||||
|
|
||||||
const errorMessage = error instanceof Error ? error.message : "未知错误";
|
const errorMessage = error instanceof Error ? error.message : "未知错误";
|
||||||
throw new LookUpError(errorMessage);
|
throw new LookUpError(errorMessage);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
import { getAnswer } from "../zhipu";
|
|
||||||
import { parseAIGeneratedJSON } from "@/utils/json";
|
|
||||||
import { InputAnalysisResult } from "./types";
|
|
||||||
import { createLogger } from "@/lib/logger";
|
|
||||||
|
|
||||||
const log = createLogger("dictionary-stage1");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 阶段 1:输入解析与语言识别
|
|
||||||
*
|
|
||||||
* 独立的 LLM 调用,分析输入文本
|
|
||||||
*/
|
|
||||||
|
|
||||||
export async function analyzeInput(text: string): Promise<InputAnalysisResult> {
|
|
||||||
const prompt = `
|
|
||||||
你是一个输入分析器。分析用户输入并返回 JSON 结果。
|
|
||||||
|
|
||||||
用户输入位于 <text> 标签内:
|
|
||||||
<text>${text}</text>
|
|
||||||
|
|
||||||
你的任务是:
|
|
||||||
1. 判断输入是否为空或明显非法
|
|
||||||
2. 判断输入是「单词」还是「短语」
|
|
||||||
3. 识别输入所属语言
|
|
||||||
|
|
||||||
返回 JSON 格式:
|
|
||||||
{
|
|
||||||
"isValid": true/false,
|
|
||||||
"isEmpty": true/false,
|
|
||||||
"isNaturalLanguage": true/false,
|
|
||||||
"inputLanguage": "检测到的语言名称(如 English、中文、日本語等)",
|
|
||||||
"inputType": "word/phrase/unknown",
|
|
||||||
"reason": "错误原因,成功时为空字符串\"\""
|
|
||||||
}
|
|
||||||
|
|
||||||
若输入为空、非自然语言或无法识别语言,设置 isValid 为 false,并在 reason 中说明原因。
|
|
||||||
若输入有效,设置 isValid 为 true,reason 为空字符串 ""。
|
|
||||||
只返回 JSON,不要任何其他文字。
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await getAnswer([
|
|
||||||
{
|
|
||||||
role: "system",
|
|
||||||
content: "你是一个输入分析器,只返回 JSON 格式的分析结果。",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: "user",
|
|
||||||
content: prompt,
|
|
||||||
},
|
|
||||||
]).then(parseAIGeneratedJSON<InputAnalysisResult>);
|
|
||||||
|
|
||||||
// 代码层面的数据验证
|
|
||||||
if (typeof result.isValid !== "boolean") {
|
|
||||||
throw new Error("阶段1:isValid 字段类型错误");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确保 reason 字段存在
|
|
||||||
if (typeof result.reason !== "string") {
|
|
||||||
result.reason = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
log.error("Stage 1 failed", { error });
|
|
||||||
// 失败时抛出错误,包含 reason
|
|
||||||
throw new Error("输入分析失败:无法识别输入类型或语言");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
87
src/lib/bigmodel/dictionary/stage1-preprocess.ts
Normal file
87
src/lib/bigmodel/dictionary/stage1-preprocess.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { getAnswer } from "../llm";
|
||||||
|
import { parseAIGeneratedJSON } from "@/utils/json";
|
||||||
|
import { PreprocessResult } from "./types";
|
||||||
|
import { createLogger } from "@/lib/logger";
|
||||||
|
|
||||||
|
const log = createLogger("dictionary-preprocess");
|
||||||
|
|
||||||
|
export async function preprocessInput(
|
||||||
|
text: string,
|
||||||
|
queryLang: string
|
||||||
|
): Promise<PreprocessResult> {
|
||||||
|
const prompt = `
|
||||||
|
你是一个词典预处理系统。分析输入并生成标准形式。
|
||||||
|
|
||||||
|
用户输入:<input>${text}</input>
|
||||||
|
查询语言:<queryLang>${queryLang}</queryLang>
|
||||||
|
|
||||||
|
任务:
|
||||||
|
1. 判断输入是否有效(非空、是自然语言)
|
||||||
|
2. 识别输入类型(单词/短语)
|
||||||
|
3. 将输入转换为查询语言的对应词(语义映射)
|
||||||
|
4. 生成标准形式(必须是查询语言)
|
||||||
|
|
||||||
|
重要规则:
|
||||||
|
- standardForm 必须是查询语言的词汇
|
||||||
|
- 例如:查询语言=维吾尔语,输入="japanese" → standardForm="ياپونىيە"
|
||||||
|
- 例如:查询语言=中文,输入="japanese" → standardForm="日语"
|
||||||
|
- 例如:查询语言=English,输入="日语" → standardForm="Japanese"
|
||||||
|
- 如果输入本身就是查询语言,则保持不变
|
||||||
|
- 只做词典形式还原,不纠正拼写
|
||||||
|
|
||||||
|
返回 JSON:
|
||||||
|
{
|
||||||
|
"isValid": boolean,
|
||||||
|
"inputType": "word" | "phrase",
|
||||||
|
"standardForm": "查询语言对应的标准形式",
|
||||||
|
"confidence": "high" | "medium" | "low",
|
||||||
|
"reason": "错误原因,成功时为空字符串"
|
||||||
|
}
|
||||||
|
|
||||||
|
注意:
|
||||||
|
- isValid=false 时,在 reason 中说明原因
|
||||||
|
- 成功时 reason 为空字符串 ""
|
||||||
|
- 只返回 JSON,不要其他文字
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await getAnswer([
|
||||||
|
{
|
||||||
|
role: "system",
|
||||||
|
content: "你是词典预处理系统,只返回 JSON。",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: prompt,
|
||||||
|
},
|
||||||
|
]).then(parseAIGeneratedJSON<PreprocessResult>);
|
||||||
|
|
||||||
|
if (typeof result.isValid !== "boolean") {
|
||||||
|
throw new Error("预处理:isValid 字段类型错误");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.standardForm || result.standardForm.trim().length === 0) {
|
||||||
|
throw new Error(result.reason || "预处理:standardForm 为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!["word", "phrase"].includes(result.inputType)) {
|
||||||
|
result.inputType = result.standardForm.includes(" ") ? "phrase" : "word";
|
||||||
|
}
|
||||||
|
|
||||||
|
let confidence: "high" | "medium" | "low" = "low";
|
||||||
|
const cv = result.confidence?.toLowerCase();
|
||||||
|
if (cv === "高" || cv === "high") confidence = "high";
|
||||||
|
else if (cv === "中" || cv === "medium") confidence = "medium";
|
||||||
|
|
||||||
|
return {
|
||||||
|
isValid: result.isValid,
|
||||||
|
inputType: result.inputType as "word" | "phrase",
|
||||||
|
standardForm: result.standardForm,
|
||||||
|
confidence,
|
||||||
|
reason: typeof result.reason === "string" ? result.reason : "",
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
log.error("Preprocess failed", { error: error instanceof Error ? error.message : String(error) });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
import { getAnswer } from "../zhipu";
|
|
||||||
import { parseAIGeneratedJSON } from "@/utils/json";
|
|
||||||
import { SemanticMappingResult } from "./types";
|
|
||||||
import { createLogger } from "@/lib/logger";
|
|
||||||
|
|
||||||
const log = createLogger("dictionary-stage2");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 阶段 2:跨语言语义映射决策
|
|
||||||
*
|
|
||||||
* 独立的 LLM 调用,决定是否需要语义映射
|
|
||||||
* 如果输入不符合"明确、基础、可词典化的语义概念"且语言不一致,直接返回失败
|
|
||||||
*/
|
|
||||||
|
|
||||||
export async function determineSemanticMapping(
|
|
||||||
text: string,
|
|
||||||
queryLang: string,
|
|
||||||
inputLanguage: string
|
|
||||||
): Promise<SemanticMappingResult> {
|
|
||||||
// 如果输入语言就是查询语言,不需要映射
|
|
||||||
if (inputLanguage.toLowerCase() === queryLang.toLowerCase()) {
|
|
||||||
return {
|
|
||||||
shouldMap: false,
|
|
||||||
reason: "输入语言与查询语言一致",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const prompt = `
|
|
||||||
你是一个语义映射决策器。判断是否需要对输入进行跨语言语义映射。
|
|
||||||
|
|
||||||
查询语言:${queryLang}
|
|
||||||
输入语言:${inputLanguage}
|
|
||||||
用户输入:${text}
|
|
||||||
|
|
||||||
判断规则:
|
|
||||||
1. 若输入表达一个**明确、基础、可词典化的语义概念**(如常见动词、名词、形容词),则应该映射
|
|
||||||
2. 若输入不符合上述条件(如复杂句子、专业术语、无法确定语义的词汇),则不应该映射
|
|
||||||
|
|
||||||
映射条件必须同时满足:
|
|
||||||
a) 输入语言 ≠ 查询语言
|
|
||||||
b) 输入是明确、基础、可词典化的语义概念
|
|
||||||
|
|
||||||
例如:
|
|
||||||
- 查询语言=English,输入="吃"(中文)→ 应该映射 → coreSemantic="to eat"
|
|
||||||
- 查询语言=Italiano,输入="run"(English)→ 应该映射 → coreSemantic="correre"
|
|
||||||
- 查询语言=中文,输入="hello"(English)→ 应该映射 → coreSemantic="你好"
|
|
||||||
- 查询语言=English,输入="我喜欢吃苹果"(中文,复杂句子)→ 不应该映射 → canMap=false
|
|
||||||
|
|
||||||
返回 JSON 格式:
|
|
||||||
{
|
|
||||||
"shouldMap": true/false,
|
|
||||||
"canMap": true/false,
|
|
||||||
"coreSemantic": "提取的核心语义(用${queryLang}表达)",
|
|
||||||
"mappedQuery": "映射到${queryLang}的标准表达",
|
|
||||||
"reason": "错误原因,成功时为空字符串\"\""
|
|
||||||
}
|
|
||||||
|
|
||||||
- canMap=true 表示输入符合"明确、基础、可词典化的语义概念"
|
|
||||||
- shouldMap=true 表示需要进行映射
|
|
||||||
- 只有 canMap=true 且语言不一致时,shouldMap 才为 true
|
|
||||||
- 如果 shouldMap=false,在 reason 中说明原因
|
|
||||||
- 如果 shouldMap=true,reason 为空字符串 ""
|
|
||||||
|
|
||||||
只返回 JSON,不要任何其他文字。
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await getAnswer([
|
|
||||||
{
|
|
||||||
role: "system",
|
|
||||||
content: `你是一个语义映射决策器,只返回 JSON 格式的结果。`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: "user",
|
|
||||||
content: prompt,
|
|
||||||
},
|
|
||||||
]).then(parseAIGeneratedJSON<SemanticMappingResult>);
|
|
||||||
|
|
||||||
// 代码层面的数据验证
|
|
||||||
if (typeof result.shouldMap !== "boolean") {
|
|
||||||
throw new Error("阶段2:shouldMap 字段类型错误");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确保 reason 字段存在
|
|
||||||
if (typeof result.reason !== "string") {
|
|
||||||
result.reason = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果不应该映射,返回错误
|
|
||||||
if (!result.shouldMap) {
|
|
||||||
throw new Error(result.reason || "输入不符合可词典化的语义概念,无法进行跨语言查询");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!result.mappedQuery || result.mappedQuery.trim().length === 0) {
|
|
||||||
throw new Error("语义映射失败:映射结果为空");
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
shouldMap: result.shouldMap,
|
|
||||||
coreSemantic: result.coreSemantic,
|
|
||||||
mappedQuery: result.mappedQuery,
|
|
||||||
reason: result.reason,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
log.error("Stage 2 failed", { error });
|
|
||||||
// 失败时直接抛出错误,让编排器返回错误响应
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
import { getAnswer } from "../zhipu";
|
|
||||||
import { parseAIGeneratedJSON } from "@/utils/json";
|
|
||||||
import { StandardFormResult } from "./types";
|
|
||||||
import { createLogger } from "@/lib/logger";
|
|
||||||
|
|
||||||
const log = createLogger("dictionary-stage3");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 阶段 3:standardForm 生成与规范化
|
|
||||||
*
|
|
||||||
* 独立的 LLM 调用,生成标准形式
|
|
||||||
*/
|
|
||||||
|
|
||||||
export async function generateStandardForm(
|
|
||||||
inputText: string,
|
|
||||||
queryLang: string,
|
|
||||||
originalInput?: string
|
|
||||||
): Promise<StandardFormResult> {
|
|
||||||
const prompt = `
|
|
||||||
你是一个词典标准形式生成器。为输入生成该语言下的标准形式。
|
|
||||||
|
|
||||||
查询语言:${queryLang}
|
|
||||||
当前输入:${inputText}
|
|
||||||
${originalInput ? `原始输入(语义参考):${originalInput}` : ''}
|
|
||||||
|
|
||||||
${originalInput ? `
|
|
||||||
**重要说明**:
|
|
||||||
- 当前输入是经过语义映射后的结果(从原始语言映射到查询语言)
|
|
||||||
- 原始输入提供了语义上下文,帮助你理解用户的真实查询意图
|
|
||||||
- 你需要基于**当前输入**生成标准形式,但要参考**原始输入的语义**以确保准确性
|
|
||||||
|
|
||||||
例如:
|
|
||||||
- 原始输入:"吃"(中文),当前输入:"to eat"(英语)→ 标准形式应为 "eat"
|
|
||||||
- 原始输入:"走"(中文),当前输入:"to walk"(英语)→ 标准形式应为 "walk"
|
|
||||||
` : ''}
|
|
||||||
|
|
||||||
规则:
|
|
||||||
1. 尝试修正明显拼写错误
|
|
||||||
2. 还原为该语言中**最常见、最自然、最标准**的形式:
|
|
||||||
* 英语:动词原形、名词单数
|
|
||||||
* 日语:辞书形
|
|
||||||
* 意大利语:不定式或最常见规范形式
|
|
||||||
* 维吾尔语:标准拉丁化或阿拉伯字母形式
|
|
||||||
* 中文:标准简化字
|
|
||||||
3. ${originalInput ? '参考原始输入的语义,确保标准形式符合用户的真实查询意图':'若无法确定或输入本身已规范,则保持不变'}
|
|
||||||
|
|
||||||
返回 JSON 格式:
|
|
||||||
{
|
|
||||||
"standardForm": "标准形式",
|
|
||||||
"confidence": "high/medium/low",
|
|
||||||
"reason": "错误原因,成功时为空字符串\"\""
|
|
||||||
}
|
|
||||||
|
|
||||||
成功生成标准形式时,reason 为空字符串 ""。
|
|
||||||
失败时,在 reason 中说明失败原因。
|
|
||||||
只返回 JSON,不要任何其他文字。
|
|
||||||
`.trim();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await getAnswer([
|
|
||||||
{
|
|
||||||
role: "system",
|
|
||||||
content: "你是一个词典标准形式生成器,只返回 JSON 格式的结果。",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: "user",
|
|
||||||
content: prompt,
|
|
||||||
},
|
|
||||||
]).then(parseAIGeneratedJSON<StandardFormResult>);
|
|
||||||
|
|
||||||
// 代码层面的数据验证
|
|
||||||
if (!result.standardForm || result.standardForm.trim().length === 0) {
|
|
||||||
throw new Error(result.reason || "阶段3:standardForm 为空");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理 confidence 可能是中文或英文的情况
|
|
||||||
let confidence: "high" | "medium" | "low" = "low";
|
|
||||||
const confidenceValue = result.confidence?.toLowerCase();
|
|
||||||
if (confidenceValue === "高" || confidenceValue === "high") {
|
|
||||||
confidence = "high";
|
|
||||||
} else if (confidenceValue === "中" || confidenceValue === "medium") {
|
|
||||||
confidence = "medium";
|
|
||||||
} else if (confidenceValue === "低" || confidenceValue === "low") {
|
|
||||||
confidence = "low";
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确保 reason 字段存在
|
|
||||||
const reason = typeof result.reason === "string" ? result.reason : "";
|
|
||||||
|
|
||||||
return {
|
|
||||||
standardForm: result.standardForm,
|
|
||||||
confidence,
|
|
||||||
reason,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
log.error("Stage 3 failed", { error });
|
|
||||||
// 失败时抛出错误
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +1,9 @@
|
|||||||
import { getAnswer } from "../zhipu";
|
import { getAnswer } from "../llm";
|
||||||
import { parseAIGeneratedJSON } from "@/utils/json";
|
import { parseAIGeneratedJSON } from "@/utils/json";
|
||||||
import { EntriesGenerationResult } from "./types";
|
import { EntriesGenerationResult } from "./types";
|
||||||
import { createLogger } from "@/lib/logger";
|
import { createLogger } from "@/lib/logger";
|
||||||
|
|
||||||
const log = createLogger("dictionary-stage4");
|
const log = createLogger("dictionary-entries");
|
||||||
|
|
||||||
/**
|
|
||||||
* 阶段 4:释义与词条生成
|
|
||||||
*
|
|
||||||
* 独立的 LLM 调用,生成词典条目
|
|
||||||
*/
|
|
||||||
|
|
||||||
export async function generateEntries(
|
export async function generateEntries(
|
||||||
standardForm: string,
|
standardForm: string,
|
||||||
@@ -20,89 +14,42 @@ export async function generateEntries(
|
|||||||
const isWord = inputType === "word";
|
const isWord = inputType === "word";
|
||||||
|
|
||||||
const prompt = `
|
const prompt = `
|
||||||
你是一个词典条目生成器。为标准形式生成词典条目。
|
生成词典条目。词语:"${standardForm}"(${queryLang})。用${definitionLang}释义。
|
||||||
|
|
||||||
标准形式:${standardForm}
|
返回 JSON:
|
||||||
查询语言:${queryLang}
|
${isWord ? `{"entries":[{"ipa":"音标","partOfSpeech":"词性","definition":"释义","example":"例句"}]}` : `{"entries":[{"definition":"释义","example":"例句"}]}`}
|
||||||
释义语言:${definitionLang}
|
|
||||||
词条类型:${isWord ? "单词" : "短语"}
|
|
||||||
|
|
||||||
${isWord ? `
|
只返回 JSON。
|
||||||
单词条目要求:
|
|
||||||
- ipa:音标(如适用)
|
|
||||||
- partOfSpeech:词性
|
|
||||||
- definition:释义(使用 ${definitionLang})
|
|
||||||
- example:例句(使用 ${queryLang})
|
|
||||||
` : `
|
|
||||||
短语条目要求:
|
|
||||||
- definition:短语释义(使用 ${definitionLang})
|
|
||||||
- example:例句(使用 ${queryLang})
|
|
||||||
`}
|
|
||||||
|
|
||||||
生成 1-3 个条目,返回 JSON 格式:
|
|
||||||
{
|
|
||||||
"entries": [
|
|
||||||
${isWord ? `
|
|
||||||
{
|
|
||||||
"ipa": "音标",
|
|
||||||
"partOfSpeech": "词性",
|
|
||||||
"definition": "释义",
|
|
||||||
"example": "例句"
|
|
||||||
}` : `
|
|
||||||
{
|
|
||||||
"definition": "释义",
|
|
||||||
"example": "例句"
|
|
||||||
}`}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
只返回 JSON,不要任何其他文字。
|
|
||||||
`.trim();
|
`.trim();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await getAnswer([
|
const result = await getAnswer([
|
||||||
{
|
{ role: "system", content: "词典条目生成器,只返回 JSON。" },
|
||||||
role: "system",
|
{ role: "user", content: prompt },
|
||||||
content: `你是一个词典条目生成器,只返回 JSON 格式的结果。`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: "user",
|
|
||||||
content: prompt,
|
|
||||||
},
|
|
||||||
]).then(parseAIGeneratedJSON<EntriesGenerationResult>);
|
]).then(parseAIGeneratedJSON<EntriesGenerationResult>);
|
||||||
|
|
||||||
// 代码层面的数据验证
|
if (!result.entries?.length) {
|
||||||
if (!result.entries || !Array.isArray(result.entries) || result.entries.length === 0) {
|
throw new Error("词条生成失败:结果为空");
|
||||||
throw new Error("阶段4:entries 为空或不是数组");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理每个条目,清理 IPA 格式
|
|
||||||
for (const entry of result.entries) {
|
for (const entry of result.entries) {
|
||||||
// 清理 IPA:删除两端可能包含的方括号、斜杠等字符
|
|
||||||
if (entry.ipa) {
|
if (entry.ipa) {
|
||||||
entry.ipa = entry.ipa.trim();
|
entry.ipa = entry.ipa.trim().replace(/^[\[\/]/, '').replace(/[\]\/]$/, '');
|
||||||
// 删除开头的 [ / /
|
|
||||||
entry.ipa = entry.ipa.replace(/^[\[\/]/, '');
|
|
||||||
// 删除结尾的 ] / /
|
|
||||||
entry.ipa = entry.ipa.replace(/[\]\/]$/, '');
|
|
||||||
}
|
}
|
||||||
|
if (!entry.definition?.trim()) {
|
||||||
if (!entry.definition || entry.definition.trim().length === 0) {
|
throw new Error("词条缺少释义");
|
||||||
throw new Error("阶段4:条目缺少 definition");
|
|
||||||
}
|
}
|
||||||
|
if (!entry.example?.trim()) {
|
||||||
if (!entry.example || entry.example.trim().length === 0) {
|
throw new Error("词条缺少例句");
|
||||||
throw new Error("阶段4:条目缺少 example");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isWord && !entry.partOfSpeech) {
|
if (isWord && !entry.partOfSpeech) {
|
||||||
throw new Error("阶段4:单词条目缺少 partOfSpeech");
|
throw new Error("单词条目缺少词性");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("Stage 4 failed", { error });
|
log.error("Entries generation failed", { error: error instanceof Error ? error.message : String(error) });
|
||||||
throw error; // 阶段4失败应该返回错误,因为这个阶段是核心
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +1,21 @@
|
|||||||
/**
|
|
||||||
* 词典查询的类型定义
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface DictionaryContext {
|
export interface DictionaryContext {
|
||||||
queryLang: string;
|
queryLang: string;
|
||||||
definitionLang: string;
|
definitionLang: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 阶段1:输入分析结果
|
export interface PreprocessResult {
|
||||||
export interface InputAnalysisResult {
|
|
||||||
isValid: boolean;
|
isValid: boolean;
|
||||||
isEmpty: boolean;
|
inputType: "word" | "phrase";
|
||||||
isNaturalLanguage: boolean;
|
|
||||||
inputLanguage?: string;
|
|
||||||
inputType: "word" | "phrase" | "unknown";
|
|
||||||
reason: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 阶段2:语义映射结果
|
|
||||||
export interface SemanticMappingResult {
|
|
||||||
shouldMap: boolean;
|
|
||||||
coreSemantic?: string;
|
|
||||||
mappedQuery?: string;
|
|
||||||
reason: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 阶段3:标准形式结果
|
|
||||||
export interface StandardFormResult {
|
|
||||||
standardForm: string;
|
standardForm: string;
|
||||||
confidence: "high" | "medium" | "low";
|
confidence: "high" | "medium" | "low";
|
||||||
reason: string;
|
reason: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 阶段4:词条生成结果
|
|
||||||
export interface EntriesGenerationResult {
|
export interface EntriesGenerationResult {
|
||||||
entries: Array<{
|
entries: Array<{
|
||||||
ipa?: string;
|
ipa?: string;
|
||||||
definition: string;
|
definition: string;
|
||||||
partOfSpeech?: string;
|
partOfSpeech?: string;
|
||||||
example: string; // example 必需
|
example: string;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|||||||
37
src/lib/bigmodel/llm.ts
Normal file
37
src/lib/bigmodel/llm.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import OpenAI from "openai";
|
||||||
|
|
||||||
|
const openai = new OpenAI({
|
||||||
|
apiKey: process.env.ZHIPU_API_KEY,
|
||||||
|
baseURL: "https://open.bigmodel.cn/api/paas/v4",
|
||||||
|
});
|
||||||
|
|
||||||
|
type Messages = Array<
|
||||||
|
| { role: "system"; content: string }
|
||||||
|
| { role: "user"; content: string }
|
||||||
|
| { role: "assistant"; content: string }
|
||||||
|
>;
|
||||||
|
|
||||||
|
async function getAnswer(prompt: string): Promise<string>;
|
||||||
|
async function getAnswer(prompt: Messages): Promise<string>;
|
||||||
|
async function getAnswer(prompt: string | Messages): Promise<string> {
|
||||||
|
const messages: Messages = typeof prompt === "string"
|
||||||
|
? [{ role: "user", content: prompt }]
|
||||||
|
: prompt;
|
||||||
|
|
||||||
|
const response = await openai.chat.completions.create({
|
||||||
|
model: process.env.ZHIPU_MODEL_NAME || "glm-4",
|
||||||
|
messages: messages as OpenAI.Chat.Completions.ChatCompletionMessageParam[],
|
||||||
|
temperature: 0.2,
|
||||||
|
});
|
||||||
|
|
||||||
|
const content = response.choices[0]?.message?.content;
|
||||||
|
if (!content) {
|
||||||
|
throw new Error("AI API 返回空响应");
|
||||||
|
}
|
||||||
|
|
||||||
|
return content.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getAnswer };
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getAnswer } from "../zhipu";
|
import { getAnswer } from "../llm";
|
||||||
import { parseAIGeneratedJSON } from "@/utils/json";
|
import { parseAIGeneratedJSON } from "@/utils/json";
|
||||||
import { LanguageDetectionResult, TranslationLLMResponse } from "./types";
|
import { LanguageDetectionResult, TranslationLLMResponse } from "./types";
|
||||||
import { createLogger } from "@/lib/logger";
|
import { createLogger } from "@/lib/logger";
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
"use server";
|
|
||||||
|
|
||||||
type Messages = { role: string; content: string; }[];
|
|
||||||
|
|
||||||
async function callZhipuAPI(
|
|
||||||
messages: Messages,
|
|
||||||
model = process.env.ZHIPU_MODEL_NAME,
|
|
||||||
) {
|
|
||||||
const url = "https://open.bigmodel.cn/api/paas/v4/chat/completions";
|
|
||||||
|
|
||||||
const response = await fetch(url, {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
Authorization: "Bearer " + process.env.ZHIPU_API_KEY,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
model: model,
|
|
||||||
messages: messages,
|
|
||||||
temperature: 0.2,
|
|
||||||
thinking: {
|
|
||||||
type: "disabled",
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`API 调用失败: ${response.status} ${response.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await response.json();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getAnswer(prompt: string): Promise<string>;
|
|
||||||
async function getAnswer(prompt: Messages): Promise<string>;
|
|
||||||
async function getAnswer(prompt: string | Messages): Promise<string> {
|
|
||||||
const messages = typeof prompt === "string"
|
|
||||||
? [{ role: "user", content: prompt }]
|
|
||||||
: prompt;
|
|
||||||
|
|
||||||
const response = await callZhipuAPI(messages);
|
|
||||||
return response.choices[0].message.content.trim() as string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { getAnswer };
|
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
interface LocalStorageOperator<T> {
|
interface LocalStorageOperator<T> {
|
||||||
get: () => T;
|
get: () => T | null;
|
||||||
set: (value: T) => void;
|
set: (value: T) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9,22 +11,29 @@ export function getLocalStorageOperator<T extends z.ZodType>(
|
|||||||
key: string,
|
key: string,
|
||||||
schema: T
|
schema: T
|
||||||
): LocalStorageOperator<z.infer<T>> {
|
): LocalStorageOperator<z.infer<T>> {
|
||||||
const get = (): z.infer<T> => {
|
const get = (): z.infer<T> | null => {
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
return [] as unknown as z.infer<T>;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const item = localStorage.getItem(key);
|
const item = localStorage.getItem(key);
|
||||||
if (item === null) {
|
if (item === null) {
|
||||||
return [] as unknown as z.infer<T>;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsed = JSON.parse(item);
|
const parsed = JSON.parse(item);
|
||||||
return schema.parse(parsed);
|
const result = schema.safeParse(parsed);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
console.warn(`[localStorage] Schema validation failed for key "${key}":`, result.error.message);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error reading from localStorage key "${key}":`, error);
|
console.error(`[localStorage] Error reading key "${key}":`, error instanceof Error ? error.message : String(error));
|
||||||
return [] as unknown as z.infer<T>;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -36,7 +45,7 @@ export function getLocalStorageOperator<T extends z.ZodType>(
|
|||||||
try {
|
try {
|
||||||
localStorage.setItem(key, JSON.stringify(value));
|
localStorage.setItem(key, JSON.stringify(value));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error writing to localStorage key "${key}":`, error);
|
console.error(`[localStorage] Error writing key "${key}":`, error instanceof Error ? error.message : String(error));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
104
src/lib/email.ts
Normal file
104
src/lib/email.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import nodemailer from "nodemailer";
|
||||||
|
import { createLogger } from "@/lib/logger";
|
||||||
|
|
||||||
|
const log = createLogger("email");
|
||||||
|
|
||||||
|
const transporter = nodemailer.createTransport({
|
||||||
|
host: process.env.SMTP_HOST,
|
||||||
|
port: Number(process.env.SMTP_PORT) || 587,
|
||||||
|
secure: process.env.SMTP_SECURE === "true",
|
||||||
|
auth: {
|
||||||
|
user: process.env.SMTP_USER,
|
||||||
|
pass: process.env.SMTP_PASS,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
interface SendEmailOptions {
|
||||||
|
to: string;
|
||||||
|
subject: string;
|
||||||
|
html: string;
|
||||||
|
text?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendEmail({ to, subject, html, text }: SendEmailOptions) {
|
||||||
|
try {
|
||||||
|
const info = await transporter.sendMail({
|
||||||
|
from: process.env.SMTP_FROM || process.env.SMTP_USER,
|
||||||
|
to,
|
||||||
|
subject,
|
||||||
|
html,
|
||||||
|
text,
|
||||||
|
});
|
||||||
|
log.info("Email sent", { to, subject, messageId: info.messageId });
|
||||||
|
return { success: true, messageId: info.messageId };
|
||||||
|
} catch (error) {
|
||||||
|
log.error("Failed to send email", { to, subject, error });
|
||||||
|
return { success: false, error };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateVerificationEmailHtml(url: string, userName: string) {
|
||||||
|
return `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
|
||||||
|
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||||
|
.button { display: inline-block; padding: 12px 24px; background-color: #3b82f6; color: white; text-decoration: none; border-radius: 6px; }
|
||||||
|
.footer { margin-top: 20px; padding-top: 20px; border-top: 1px solid #eee; color: #666; font-size: 14px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>验证您的邮箱地址</h1>
|
||||||
|
<p>您好,${userName}!</p>
|
||||||
|
<p>感谢您注册。请点击下方按钮验证您的邮箱地址:</p>
|
||||||
|
<p>
|
||||||
|
<a href="${url}" class="button">验证邮箱</a>
|
||||||
|
</p>
|
||||||
|
<p>或者复制以下链接到浏览器:</p>
|
||||||
|
<p style="word-break: break-all; color: #666;">${url}</p>
|
||||||
|
<p>此链接将在 24 小时后过期。</p>
|
||||||
|
<div class="footer">
|
||||||
|
<p>如果您没有注册此账户,请忽略此邮件。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateResetPasswordEmailHtml(url: string, userName: string) {
|
||||||
|
return `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }
|
||||||
|
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||||
|
.button { display: inline-block; padding: 12px 24px; background-color: #3b82f6; color: white; text-decoration: none; border-radius: 6px; }
|
||||||
|
.footer { margin-top: 20px; padding-top: 20px; border-top: 1px solid #eee; color: #666; font-size: 14px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>重置您的密码</h1>
|
||||||
|
<p>您好,${userName}!</p>
|
||||||
|
<p>我们收到了重置您账户密码的请求。请点击下方按钮设置新密码:</p>
|
||||||
|
<p>
|
||||||
|
<a href="${url}" class="button">重置密码</a>
|
||||||
|
</p>
|
||||||
|
<p>或者复制以下链接到浏览器:</p>
|
||||||
|
<p style="word-break: break-all; color: #666;">${url}</p>
|
||||||
|
<p>此链接将在 1 小时后过期。</p>
|
||||||
|
<div class="footer">
|
||||||
|
<p>如果您没有请求重置密码,请忽略此邮件,您的密码不会被更改。</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ export const actionLookUpDictionary = async (dto: ActionInputLookUpDictionary):
|
|||||||
message: e.message
|
message: e.message
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
log.error("Dictionary lookup failed", { error: e });
|
log.error("Dictionary lookup failed", { error: e instanceof Error ? e.message : String(e) });
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: 'Unknown error occured.'
|
message: 'Unknown error occured.'
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export const serviceLookUp = async (dto: ServiceInputLookUp) => {
|
|||||||
},
|
},
|
||||||
response.entries
|
response.entries
|
||||||
).catch(error => {
|
).catch(error => {
|
||||||
log.error("Failed to save dictionary data", { error });
|
log.error("Failed to save dictionary data", { error: error instanceof Error ? error.message : String(error) });
|
||||||
});
|
});
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
@@ -54,7 +54,7 @@ export const serviceLookUp = async (dto: ServiceInputLookUp) => {
|
|||||||
definitionLang: definitionLang,
|
definitionLang: definitionLang,
|
||||||
dictionaryItemId: lastLookUpResult.id
|
dictionaryItemId: lastLookUpResult.id
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
log.error("Failed to save dictionary data", { error });
|
log.error("Failed to save dictionary data", { error: error instanceof Error ? error.message : String(error) });
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
standardForm: lastLookUpResult.standardForm,
|
standardForm: lastLookUpResult.standardForm,
|
||||||
|
|||||||
@@ -62,6 +62,12 @@ export type ActionOutputGetPublicFolders = {
|
|||||||
data?: ActionOutputPublicFolder[];
|
data?: ActionOutputPublicFolder[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ActionOutputGetPublicFolderById = {
|
||||||
|
message: string;
|
||||||
|
success: boolean;
|
||||||
|
data?: ActionOutputPublicFolder;
|
||||||
|
};
|
||||||
|
|
||||||
export type ActionOutputSetFolderVisibility = {
|
export type ActionOutputSetFolderVisibility = {
|
||||||
message: string;
|
message: string;
|
||||||
success: boolean;
|
success: boolean;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
ActionInputUpdatePairById,
|
ActionInputUpdatePairById,
|
||||||
ActionOutputGetFoldersWithTotalPairsByUserId,
|
ActionOutputGetFoldersWithTotalPairsByUserId,
|
||||||
ActionOutputGetPublicFolders,
|
ActionOutputGetPublicFolders,
|
||||||
|
ActionOutputGetPublicFolderById,
|
||||||
ActionOutputSetFolderVisibility,
|
ActionOutputSetFolderVisibility,
|
||||||
ActionOutputToggleFavorite,
|
ActionOutputToggleFavorite,
|
||||||
ActionOutputCheckFavorite,
|
ActionOutputCheckFavorite,
|
||||||
@@ -30,6 +31,7 @@ import {
|
|||||||
repoGetFoldersWithTotalPairsByUserId,
|
repoGetFoldersWithTotalPairsByUserId,
|
||||||
repoGetPairsByFolderId,
|
repoGetPairsByFolderId,
|
||||||
repoGetPublicFolders,
|
repoGetPublicFolders,
|
||||||
|
repoGetPublicFolderById,
|
||||||
repoGetUserIdByFolderId,
|
repoGetUserIdByFolderId,
|
||||||
repoRenameFolderById,
|
repoRenameFolderById,
|
||||||
repoSearchPublicFolders,
|
repoSearchPublicFolders,
|
||||||
@@ -383,6 +385,32 @@ export async function actionSearchPublicFolders(query: string): Promise<ActionOu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function actionGetPublicFolderById(folderId: number): Promise<ActionOutputGetPublicFolderById> {
|
||||||
|
try {
|
||||||
|
const folder = await repoGetPublicFolderById(folderId);
|
||||||
|
if (!folder) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'Folder not found.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'success',
|
||||||
|
data: {
|
||||||
|
...folder,
|
||||||
|
visibility: folder.visibility as "PRIVATE" | "PUBLIC",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
log.error("Operation failed", { error: e });
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: 'Unknown error occured.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function actionToggleFavorite(
|
export async function actionToggleFavorite(
|
||||||
folderId: number,
|
folderId: number,
|
||||||
): Promise<ActionOutputToggleFavorite> {
|
): Promise<ActionOutputToggleFavorite> {
|
||||||
@@ -171,6 +171,32 @@ export async function repoGetFolderVisibility(
|
|||||||
return folder;
|
return folder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function repoGetPublicFolderById(
|
||||||
|
folderId: number,
|
||||||
|
): Promise<RepoOutputPublicFolder | null> {
|
||||||
|
const folder = await prisma.folder.findUnique({
|
||||||
|
where: { id: folderId, visibility: Visibility.PUBLIC },
|
||||||
|
include: {
|
||||||
|
_count: { select: { pairs: true, favorites: true } },
|
||||||
|
user: { select: { name: true, username: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!folder) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: folder.id,
|
||||||
|
name: folder.name,
|
||||||
|
visibility: folder.visibility,
|
||||||
|
createdAt: folder.createdAt,
|
||||||
|
userId: folder.userId,
|
||||||
|
userName: folder.user?.name ?? "Unknown",
|
||||||
|
userUsername: folder.user?.username ?? "unknown",
|
||||||
|
totalPairs: folder._count.pairs,
|
||||||
|
favoriteCount: folder._count.favorites,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function repoGetPublicFolders(
|
export async function repoGetPublicFolders(
|
||||||
input: RepoInputGetPublicFolders = {},
|
input: RepoInputGetPublicFolders = {},
|
||||||
): Promise<RepoOutputPublicFolder[]> {
|
): Promise<RepoOutputPublicFolder[]> {
|
||||||
@@ -192,8 +218,8 @@ export async function repoGetPublicFolders(
|
|||||||
visibility: folder.visibility,
|
visibility: folder.visibility,
|
||||||
createdAt: folder.createdAt,
|
createdAt: folder.createdAt,
|
||||||
userId: folder.userId,
|
userId: folder.userId,
|
||||||
userName: folder.user.name,
|
userName: folder.user?.name ?? "Unknown",
|
||||||
userUsername: folder.user.username,
|
userUsername: folder.user?.username ?? "unknown",
|
||||||
totalPairs: folder._count.pairs,
|
totalPairs: folder._count.pairs,
|
||||||
favoriteCount: folder._count.favorites,
|
favoriteCount: folder._count.favorites,
|
||||||
}));
|
}));
|
||||||
@@ -221,8 +247,8 @@ export async function repoSearchPublicFolders(
|
|||||||
visibility: folder.visibility,
|
visibility: folder.visibility,
|
||||||
createdAt: folder.createdAt,
|
createdAt: folder.createdAt,
|
||||||
userId: folder.userId,
|
userId: folder.userId,
|
||||||
userName: folder.user.name,
|
userName: folder.user?.name ?? "Unknown",
|
||||||
userUsername: folder.user.username,
|
userUsername: folder.user?.username ?? "unknown",
|
||||||
totalPairs: folder._count.pairs,
|
totalPairs: folder._count.pairs,
|
||||||
favoriteCount: folder._count.favorites,
|
favoriteCount: folder._count.favorites,
|
||||||
}));
|
}));
|
||||||
@@ -300,8 +326,8 @@ export async function repoGetUserFavorites(input: RepoInputGetUserFavorites) {
|
|||||||
folderCreatedAt: fav.folder.createdAt,
|
folderCreatedAt: fav.folder.createdAt,
|
||||||
folderTotalPairs: fav.folder._count.pairs,
|
folderTotalPairs: fav.folder._count.pairs,
|
||||||
folderOwnerId: fav.folder.userId,
|
folderOwnerId: fav.folder.userId,
|
||||||
folderOwnerName: fav.folder.user.name,
|
folderOwnerName: fav.folder.user?.name ?? "Unknown",
|
||||||
folderOwnerUsername: fav.folder.user.username,
|
folderOwnerUsername: fav.folder.user?.username ?? "unknown",
|
||||||
favoritedAt: fav.createdAt,
|
favoritedAt: fav.createdAt,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
import { ValidateError } from "@/lib/errors";
|
import { ValidateError } from "@/lib/errors";
|
||||||
import { createLogger } from "@/lib/logger";
|
import { createLogger } from "@/lib/logger";
|
||||||
import { serviceTranslateText } from "./translator-service";
|
import { serviceTranslateText } from "./translator-service";
|
||||||
import { getAnswer } from "@/lib/bigmodel/zhipu";
|
import { getAnswer } from "@/lib/bigmodel/llm";
|
||||||
|
|
||||||
const log = createLogger("translator-action");
|
const log = createLogger("translator-action");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user