From de7c1321c281204b39ff77fdbe12bfee54992311 Mon Sep 17 00:00:00 2001 From: goddonebianu Date: Tue, 17 Mar 2026 20:24:42 +0800 Subject: [PATCH] refactor: remove Anki import/export and simplify card system - Remove Anki apkg import/export functionality - Remove OCR feature module - Remove note and note-type modules - Simplify card/deck modules (remove spaced repetition complexity) - Update translator and dictionary features - Clean up unused translations and update i18n files - Simplify prisma schema --- messages/de-DE.json | 227 +++++-- messages/en-US.json | 53 +- messages/fr-FR.json | 236 +++++-- messages/it-IT.json | 242 ++++++-- messages/ja-JP.json | 177 ++++-- messages/ko-KR.json | 239 +++++-- messages/ug-CN.json | 250 +++++--- messages/zh-CN.json | 174 ++++-- prisma/schema.prisma | 304 +++------ .../dictionary/DictionaryClient.tsx | 95 +-- src/app/(features)/dictionary/page.tsx | 6 +- src/app/(features)/ocr/OCRClient.tsx | 286 --------- src/app/(features)/ocr/page.tsx | 20 - src/app/(features)/text-speaker/page.tsx | 65 +- src/app/(features)/translator/page.tsx | 328 ++++++---- src/app/decks/DecksClient.tsx | 2 - src/app/decks/[deck_id]/AddCardModal.tsx | 312 +++++++--- src/app/decks/[deck_id]/CardItem.tsx | 161 +++-- src/app/decks/[deck_id]/EditCardModal.tsx | 229 +++++++ src/app/decks/[deck_id]/InDeck.tsx | 224 ++----- src/app/decks/[deck_id]/UpdateCardModal.tsx | 132 ---- src/app/decks/[deck_id]/learn/Memorize.tsx | 288 +++------ .../decks/[deck_id]/learn/interval-preview.ts | 85 --- src/components/deck/ImportExport.tsx | 254 -------- src/lib/anki/apkg-exporter.ts | 414 ------------- src/lib/anki/apkg-parser.ts | 175 ------ src/lib/anki/types.ts | 193 ------ .../dictionary/stage4-entriesGeneration.ts | 25 +- src/lib/bigmodel/llm.ts | 33 +- src/lib/bigmodel/translator/orchestrator.ts | 12 +- src/modules/auth/auth-repository.ts | 22 +- src/modules/card/card-action-dto.ts | 204 +----- src/modules/card/card-action.ts | 537 ++++------------ src/modules/card/card-repository-dto.ts | 138 ++--- src/modules/card/card-repository.ts | 442 ++++--------- src/modules/card/card-service-dto.ts | 157 +---- src/modules/card/card-service.ts | 581 +++--------------- src/modules/deck/deck-action-dto.ts | 7 - src/modules/deck/deck-action.ts | 3 - src/modules/deck/deck-repository-dto.ts | 24 +- src/modules/deck/deck-repository.ts | 24 - src/modules/deck/deck-service-dto.ts | 7 - src/modules/deck/deck-service.ts | 3 - .../dictionary/dictionary-action-dto.ts | 33 +- src/modules/dictionary/dictionary-action.ts | 54 +- .../dictionary/dictionary-repository-dto.ts | 58 -- .../dictionary/dictionary-repository.ts | 75 --- .../dictionary/dictionary-service-dto.ts | 18 +- src/modules/dictionary/dictionary-service.ts | 79 --- src/modules/export/export-action.ts | 134 ---- src/modules/import/import-action.ts | 308 ---------- src/modules/note-type/note-type-action-dto.ts | 106 ---- src/modules/note-type/note-type-action.ts | 255 -------- .../note-type/note-type-repository-dto.ts | 181 ------ src/modules/note-type/note-type-repository.ts | 151 ----- .../note-type/note-type-service-dto.ts | 60 -- src/modules/note-type/note-type-service.ts | 272 -------- src/modules/note/note-action-dto.ts | 131 ---- src/modules/note/note-action.ts | 344 ----------- src/modules/note/note-repository-dto.ts | 72 --- src/modules/note/note-repository.ts | 284 --------- src/modules/note/note-service-dto.ts | 60 -- src/modules/note/note-service.ts | 200 ------ src/modules/ocr/ocr-action-dto.ts | 20 - src/modules/ocr/ocr-action.ts | 36 -- src/modules/ocr/ocr-repository-dto.ts | 1 - src/modules/ocr/ocr-repository.ts | 1 - src/modules/ocr/ocr-service-dto.ts | 21 - src/modules/ocr/ocr-service.ts | 155 ----- src/modules/translator/translator-action.ts | 34 +- .../translator/translator-repository-dto.ts | 23 - .../translator/translator-repository.ts | 41 -- src/modules/translator/translator-service.ts | 76 +-- src/shared/anki-type.ts | 165 ----- src/shared/card-type.ts | 33 + src/shared/dictionary-type.ts | 1 + src/shared/theme-presets.ts | 2 +- 77 files changed, 2767 insertions(+), 8107 deletions(-) delete mode 100644 src/app/(features)/ocr/OCRClient.tsx delete mode 100644 src/app/(features)/ocr/page.tsx create mode 100644 src/app/decks/[deck_id]/EditCardModal.tsx delete mode 100644 src/app/decks/[deck_id]/UpdateCardModal.tsx delete mode 100644 src/app/decks/[deck_id]/learn/interval-preview.ts delete mode 100644 src/components/deck/ImportExport.tsx delete mode 100644 src/lib/anki/apkg-exporter.ts delete mode 100644 src/lib/anki/apkg-parser.ts delete mode 100644 src/lib/anki/types.ts delete mode 100644 src/modules/dictionary/dictionary-repository-dto.ts delete mode 100644 src/modules/dictionary/dictionary-repository.ts delete mode 100644 src/modules/dictionary/dictionary-service.ts delete mode 100644 src/modules/export/export-action.ts delete mode 100644 src/modules/import/import-action.ts delete mode 100644 src/modules/note-type/note-type-action-dto.ts delete mode 100644 src/modules/note-type/note-type-action.ts delete mode 100644 src/modules/note-type/note-type-repository-dto.ts delete mode 100644 src/modules/note-type/note-type-repository.ts delete mode 100644 src/modules/note-type/note-type-service-dto.ts delete mode 100644 src/modules/note-type/note-type-service.ts delete mode 100644 src/modules/note/note-action-dto.ts delete mode 100644 src/modules/note/note-action.ts delete mode 100644 src/modules/note/note-repository-dto.ts delete mode 100644 src/modules/note/note-repository.ts delete mode 100644 src/modules/note/note-service-dto.ts delete mode 100644 src/modules/note/note-service.ts delete mode 100644 src/modules/ocr/ocr-action-dto.ts delete mode 100644 src/modules/ocr/ocr-action.ts delete mode 100644 src/modules/ocr/ocr-repository-dto.ts delete mode 100644 src/modules/ocr/ocr-repository.ts delete mode 100644 src/modules/ocr/ocr-service-dto.ts delete mode 100644 src/modules/ocr/ocr-service.ts delete mode 100644 src/modules/translator/translator-repository-dto.ts delete mode 100644 src/modules/translator/translator-repository.ts delete mode 100644 src/shared/anki-type.ts create mode 100644 src/shared/card-type.ts diff --git a/messages/de-DE.json b/messages/de-DE.json index 048fdfc..31c3f5b 100644 --- a/messages/de-DE.json +++ b/messages/de-DE.json @@ -53,7 +53,30 @@ "totalCards": "Gesamtkarten", "createdAt": "Erstellt am", "actions": "Aktionen", - "view": "Anzeigen" + "view": "Anzeigen", + "subtitle": "Lern-Decks verwalten", + "newDeck": "Neues Deck", + "noDecksYet": "Noch keine Decks", + "loading": "Laden...", + "deckInfo": "ID: {id} · {totalCards} Karten", + "enterDeckName": "Deck-Name eingeben:", + "enterNewName": "Neuen Namen eingeben:", + "confirmDelete": "\"{name}\" eingeben zum Löschen:", + "public": "Öffentlich", + "private": "Privat", + "setPublic": "Öffentlich machen", + "setPrivate": "Privat machen", + "importApkg": "APKG importieren", + "exportApkg": "APKG exportieren", + "clickToUpload": "Klicken zum Hochladen", + "apkgFilesOnly": "Nur .apkg Dateien", + "parsing": "Analysieren...", + "foundDecks": "{count} Decks gefunden", + "back": "Zurück", + "import": "Importieren", + "importing": "Importieren...", + "exportSuccess": "Export erfolgreich", + "goToDecks": "Zu Decks" }, "folder_id": { "unauthorized": "Sie sind nicht der Besitzer dieses Ordners", @@ -187,8 +210,8 @@ }, "memorize": { "deck_selector": { - "selectDeck": "Deck auswählen", - "noDecks": "Keine Decks gefunden", + "selectDeck": "Deck wählen", + "noDecks": "Keine Decks", "goToDecks": "Zu Decks", "noCards": "Keine Karten", "new": "Neu", @@ -199,44 +222,45 @@ "review": { "loading": "Laden...", "backToDecks": "Zurück zu Decks", - "allDone": "Fertig!", - "allDoneDesc": "Alle fälligen Karten wurden wiederholt.", + "allDone": "Alles erledigt!", + "allDoneDesc": "Lernen für heute abgeschlossen!", "reviewedCount": "{count} Karten wiederholt", "progress": "{current} / {total}", "nextReview": "Nächste Wiederholung", "interval": "Intervall", - "ease": "Leichtigkeit", - "lapses": "Verlernungen", + "ease": "Schwierigkeit", + "lapses": "Fehler", "showAnswer": "Antwort zeigen", "again": "Nochmal", "hard": "Schwer", "good": "Gut", "easy": "Leicht", - "now": "jetzt", - "lessThanMinute": "<1 Min", - "inMinutes": "{count} Min", - "inHours": "{count} Std", - "inDays": "{count} Tage", - "inMonths": "{count} Monate", - "minutes": "<1 Min", - "days": "{count} Tage", - "months": "{count} Monate", - "minAbbr": "m", - "dayAbbr": "T", + "now": "Jetzt", + "lessThanMinute": "weniger als 1 Minute", + "inMinutes": "in {n} Minute{n, plural, one {} other {n}}", + "inHours": "in {n} Stunde{n, plural, one {} other {n}}", + "inDays": "in {n} Tag{en}", + "inMonths": "in {n} Monat{en}", + "minutes": "Minuten", + "days": "Tage", + "months": "Monate", + "minAbbr": "min", + "dayAbbr": "d", "cardTypeNew": "Neu", "cardTypeLearning": "Lernen", - "cardTypeReview": "Wiederholung", + "cardTypeReview": "Wiederholen", "cardTypeRelearning": "Neu lernen", "reverse": "Umkehren", "dictation": "Diktat", "clickToPlay": "Klicken zum Abspielen", "yourAnswer": "Ihre Antwort", - "typeWhatYouHear": "Geben Sie ein, was Sie hören", - "correct": "Richtig", - "incorrect": "Falsch" + "typeWhatYouHear": "Schreiben Sie was Sie hören", + "correct": "Richtig!", + "incorrect": "Falsch", + "nextCard": "Nächste" }, "page": { - "unauthorized": "Sie sind nicht berechtigt, auf dieses Deck zuzugreifen" + "unauthorized": "Nicht autorisiert" } }, "navbar": { @@ -250,35 +274,49 @@ "settings": "Einstellungen" }, "ocr": { - "title": "OCR Vokabel-Extraktion", - "description": "Laden Sie Screenshots von Vokabeltabellen aus Lehrbüchern hoch, um Wort-Definition-Paare zu extrahieren", + "title": "OCR-Erkennung", + "description": "Text aus Bildern extrahieren", "uploadImage": "Bild hochladen", - "dragDropHint": "Ziehen Sie ein Bild hierher oder klicken Sie zum Auswählen", - "supportedFormats": "Unterstüt: JPG, PNG, WebP", - "selectDeck": "Deck auswählen", - "chooseDeck": "Wählen Sie einen Deck zum Speichern der extrahierten Paare", - "noDecks": "Keine Decks verfügbar. Bitte create a deck first.", - "languageHints": "Sprachhinweise (Optional)", - "sourceLanguageHint": "Quellsprache (z.B. Englisch)", - "targetLanguageHint": "Ziel-/Übersetzungssprache (z.B. Chinesisch)", - "process": "Bild verarbeiten", - "processing": "Verarbeitung...", + "dragDropHint": "Ziehen und ablegen", + "supportedFormats": "Unterstützt: JPG, PNG, WEBP", + "selectDeck": "Deck wählen", + "chooseDeck": "Deck wählen", + "noDecks": "Keine Decks verfügbar", + "languageHints": "Sprachhinweise", + "sourceLanguageHint": "Quellsprache", + "targetLanguageHint": "Zielsprache", + "process": "Verarbeiten", + "processing": "Verarbeiten...", "preview": "Vorschau", "extractedPairs": "Extrahierte Paare", "word": "Wort", "definition": "Definition", - "pairsCount": "{count} Paare extrahiert", - "savePairs": "In Deck speichern", + "pairsCount": "{count} Paare", + "savePairs": "Speichern", "saving": "Speichern...", - "saved": "{count} Paare erfolgreich in {deck} gespeichert", + "saved": "Gespeichert", "saveFailed": "Speichern fehlgeschlagen", - "noImage": "Bitte laden Sie zuerst ein Bild hoch", - "noDeck": "Bitte select a deck", - "processingFailed": "OCR-Verarbeitung fehlgeschlagen", - "tryAgain": "Bitte try again with a clearer image", - "detectedLanguages": "Erkannt: {source} → {target}", - "invalidFileType": "Ungültiger Dateityp. Bitte laden Sie eine Bilddatei hoch.", - "ocrFailed": "OCR-Verarbeitung fehlgeschlagen." + "noImage": "Bitte Bild hochladen", + "noDeck": "Bitte Deck wählen", + "processingFailed": "Verarbeitung fehlgeschlagen", + "tryAgain": "Erneut versuchen", + "detectedLanguages": "Erkannte Sprachen", + "invalidFileType": "Ungültiger Dateityp", + "ocrFailed": "OCR fehlgeschlagen", + "uploadSection": "Bild hochladen", + "dropOrClick": "Ablegen oder klicken", + "changeImage": "Bild ändern", + "deckSelection": "Deck wählen", + "sourceLanguagePlaceholder": "z.B. Englisch", + "targetLanguagePlaceholder": "z.B. Deutsch", + "processButton": "Erkennung starten", + "resultsPreview": "Ergebnisvorschau", + "saveButton": "In Deck speichern", + "ocrSuccess": "OCR erfolgreich", + "savedToDeck": "In Deck gespeichert", + "noResultsToSave": "Keine Ergebnisse", + "detectedSourceLanguage": "Erkannte Quellsprache", + "detectedTargetLanguage": "Erkannte Zielsprache" }, "profile": { "myProfile": "Mein Profil", @@ -324,7 +362,7 @@ "shortcuts": "Tastenkürzel", "keyboardShortcuts": "Tastaturkürzel", "playPause": "Wiedergabe/Pause", - "autoPauseToggle": "Auto-Pause umschalten", + "autoPauseToggle": "Auto-Pause", "subtitleSettings": "Untertiteleinstellungen", "fontSize": "Schriftgröße", "textColor": "Textfarbe", @@ -340,7 +378,22 @@ "viewSavedItems": "Gespeicherte Einträge anzeigen", "confirmDeleteAll": "Sind Sie sicher, dass Sie alles löschen möchten? (J/N)", "saved": "Gespeichert", - "clearAll": "Alles löschen" + "clearAll": "Alles löschen", + "language": "Sprache", + "customLanguage": "oder Sprache eingeben...", + "languages": { + "auto": "Automatisch", + "chinese": "Chinesisch", + "english": "Englisch", + "japanese": "Japanisch", + "korean": "Koreanisch", + "french": "Französisch", + "german": "Deutsch", + "italian": "Italienisch", + "spanish": "Spanisch", + "portuguese": "Portugiesisch", + "russian": "Russisch" + } }, "translator": { "detectLanguage": "Sprache erkennen", @@ -374,7 +427,19 @@ "error": "Fehler beim Hinzufügen des Textpaars zum Ordner" }, "autoSave": "Autom. Speichern", - "customLanguage": "oder Sprache eingeben..." + "customLanguage": "oder Sprache eingeben...", + "pleaseLogin": "Bitte anmelden um Karten zu speichern", + "pleaseCreateDeck": "Bitte erst zuerst ein Deck", + "noTranslationToSave": "Keine Übersetzung zum Speichern", + "noDeckSelected": "Kein Deck ausgewählt", + "saveAsCard": "Als Karte speichern", + "selectDeck": "Deck wählen", + "front": "Vorderseite", + "back": "Rückseite", + "cancel": "Abbrechen", + "save": "Speichern", + "savedToDeck": "Karte in {deckName} gespeichert", + "saveFailed": "Karte speichern fehlgeschlagen" }, "dictionary": { "title": "Wörterbuch", @@ -419,7 +484,9 @@ "unfavorite": "Aus Favoriten entfernen", "pleaseLogin": "Bitte melden Sie sich zuerst an", "sortByFavorites": "Nach Favoriten sortieren", - "sortByFavoritesActive": "Sortierung nach Favoriten aufheben" + "sortByFavoritesActive": "Sortierung nach Favoriten aufheben", + "noDecks": "Keine öffentlichen Decks", + "deckInfo": "{userName} · {totalCards} Karten" }, "exploreDetail": { "title": "Ordnerdetails", @@ -433,7 +500,8 @@ "unfavorite": "Aus Favoriten entfernen", "favorited": "Favorisiert", "unfavorited": "Aus Favoriten entfernt", - "pleaseLogin": "Bitte melden Sie sich zuerst an" + "pleaseLogin": "Bitte melden Sie sich zuerst an", + "totalCards": "{count} Karten" }, "favorites": { "title": "Meine Favoriten", @@ -478,6 +546,16 @@ "createdAt": "Erstellt am", "actions": "Aktionen", "view": "Anzeigen" + }, + "joined": "Beigetreten", + "decks": { + "title": "Meine Decks", + "noDecks": "Keine Decks", + "deckName": "Deck-Name", + "totalCards": "Gesamtkarten", + "createdAt": "Erstellt am", + "actions": "Aktionen", + "view": "Ansehen" } }, "follow": { @@ -512,28 +590,47 @@ "edit": "Bearbeiten", "delete": "Löschen", "permissionDenied": "Sie haben keine Berechtigung für diese Aktion", - "resetProgress": "Zurücksetzen", - "resetProgressTitle": "Deck-Fortschritt zurücksetzen", - "resetProgressConfirm": "Dies setzt alle Karten in diesem Deck auf den neuen Zustand zurück. Ihr Lernfortschritt geht verloren. Sind Sie sicher?", - "resetSuccess": "{count} Karten erfolgreich zurückgesetzt", - "resetting": "Wird zurückgesetzt...", + "resetProgress": "Fortschritt zurücksetzen", + "resetProgressTitle": "Lernfortschritt zurücksetzen", + "resetProgressConfirm": "Fortschritt wirklich zurücksetzen?", + "resetSuccess": "Fortschritt zurückgesetzt", + "resetting": "Zurücksetzen...", "cancel": "Abbrechen", "settings": "Einstellungen", "settingsTitle": "Deck-Einstellungen", - "newPerDay": "Neue Karten pro Tag", - "newPerDayHint": "Maximale Anzahl neuer Karten pro Tag", + "newPerDay": "Neue pro Tag", + "newPerDayHint": "Neue Karten pro Tag", "revPerDay": "Wiederholungen pro Tag", - "revPerDayHint": "Maximale Anzahl wiederholter Karten pro Tag", + "revPerDayHint": "Wiederholungen pro Tag", "save": "Speichern", - "saving": "Wird gespeichert...", + "saving": "Speichern...", "settingsSaved": "Einstellungen gespeichert", - "todayNew": "Neu", - "todayReview": "Wiederholung", + "todayNew": "Heute neu", + "todayReview": "Heute wiederholen", "todayLearning": "Lernen", "error": { - "update": "Sie haben keine Berechtigung, diese Karte zu aktualisieren.", - "delete": "Sie haben keine Berechtigung, diese Karte zu löschen.", - "add": "Sie haben keine Berechtigung, Karten zu diesem Deck hinzuzufügen." - } + "update": "Keine Berechtigung zum Aktualisieren", + "delete": "Keine Berechtigung zum Löschen", + "add": "Keine Berechtigung zum Hinzufügen" + }, + "ipaPlaceholder": "IPA eingeben", + "examplePlaceholder": "Beispiel eingeben", + "wordRequired": "Bitte Wort eingeben", + "definitionRequired": "Bitte Definition eingeben", + "cardAdded": "Karte hinzugefügt", + "cardType": "Kartentyp", + "wordCard": "Wortkarte", + "phraseCard": "Phrasenkarte", + "sentenceCard": "Satzkarte", + "sentence": "Satz", + "sentencePlaceholder": "Satz eingeben", + "wordPlaceholder": "Wort eingeben", + "queryLang": "Abfragesprache", + "meanings": "Bedeutungen", + "addMeaning": "Bedeutung hinzufügen", + "partOfSpeech": "Wortart", + "deleteConfirm": "Karte wirklich löschen?", + "cardDeleted": "Karte gelöscht", + "cardUpdated": "Karte aktualisiert" } } diff --git a/messages/en-US.json b/messages/en-US.json index 4a8ea3a..ce8fdc8 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -92,10 +92,28 @@ "word": "Word", "definition": "Definition", "ipa": "IPA", + "ipaPlaceholder": "Enter IPA pronunciation", "example": "Example", + "examplePlaceholder": "Enter an example sentence", "wordAndDefinitionRequired": "Word and definition are required", + "wordRequired": "Word is required", + "definitionRequired": "At least one definition is required", + "cardAdded": "Card added successfully", + "cardType": "Card Type", + "wordCard": "Word", + "phraseCard": "Phrase", + "sentenceCard": "Sentence", + "sentence": "Sentence", + "sentencePlaceholder": "Enter a sentence", + "wordPlaceholder": "Enter a word", + "queryLang": "Language", + "meanings": "Meanings", + "addMeaning": "Add Meaning", + "partOfSpeech": "Part of Speech", "edit": "Edit", "delete": "Delete", + "deleteConfirm": "Are you sure you want to delete this card?", + "cardDeleted": "Card deleted", "permissionDenied": "You do not have permission to perform this action", "resetProgress": "Reset", "resetProgressTitle": "Reset Deck Progress", @@ -115,6 +133,10 @@ "todayNew": "New", "todayReview": "Review", "todayLearning": "Learning", + "updating": "Updating...", + "cardUpdated": "Card updated", + "wordRequired": "Word is required", + "definitionRequired": "At least one definition is required", "error": { "update": "You do not have permission to update this card.", "delete": "You do not have permission to delete this card.", @@ -392,7 +414,22 @@ "viewSavedItems": "View Saved Items", "confirmDeleteAll": "Are you sure you want to delete everything? (Y/N)", "saved": "Saved", - "clearAll": "Clear All" + "clearAll": "Clear All", + "language": "Language", + "customLanguage": "or type language...", + "languages": { + "auto": "Auto", + "chinese": "Chinese", + "english": "English", + "japanese": "Japanese", + "korean": "Korean", + "french": "French", + "german": "German", + "italian": "Italian", + "spanish": "Spanish", + "portuguese": "Portuguese", + "russian": "Russian" + } }, "translator": { "detectLanguage": "detect language", @@ -426,7 +463,19 @@ "success": "Text pair added to folder", "error": "Failed to add text pair to folder" }, - "autoSave": "Auto Save" + "autoSave": "Auto Save", + "pleaseLogin": "Please login to save cards", + "pleaseCreateDeck": "Please create a deck first", + "noTranslationToSave": "No translation to save", + "noDeckSelected": "No deck selected", + "saveAsCard": "Save as Card", + "selectDeck": "Select Deck", + "front": "Front", + "back": "Back", + "cancel": "Cancel", + "save": "Save", + "savedToDeck": "Card saved to {deckName}", + "saveFailed": "Failed to save card" }, "dictionary": { "title": "Dictionary", diff --git a/messages/fr-FR.json b/messages/fr-FR.json index 91668bf..881787c 100644 --- a/messages/fr-FR.json +++ b/messages/fr-FR.json @@ -53,7 +53,30 @@ "totalCards": "Total des cartes", "createdAt": "Créé le", "actions": "Actions", - "view": "Voir" + "view": "Voir", + "subtitle": "Gérer vos decks d'apprentissage", + "newDeck": "Nouveau deck", + "noDecksYet": "Pas encore de decks", + "loading": "Chargement...", + "deckInfo": "ID: {id} · {totalCards} cartes", + "enterDeckName": "Nom du deck:", + "enterNewName": "Nouveau nom:", + "confirmDelete": "Tapez \"{name}\" pour supprimer:", + "public": "Public", + "private": "Privé", + "setPublic": "Rendre public", + "setPrivate": "Rendre privé", + "importApkg": "Importer APKG", + "exportApkg": "Exporter APKG", + "clickToUpload": "Cliquez pour télécharger", + "apkgFilesOnly": "Fichiers .apkg uniquement", + "parsing": "Analyse...", + "foundDecks": "{count} decks trouvés", + "back": "Retour", + "import": "Importer", + "importing": "Import...", + "exportSuccess": "Export réussi", + "goToDecks": "Aller aux decks" }, "folder_id": { "unauthorized": "Vous n'êtes pas le propriétaire de ce dossier", @@ -106,29 +129,48 @@ "edit": "Modifier", "delete": "Supprimer", "permissionDenied": "Vous n'avez pas la permission d'effectuer cette action", - "resetProgress": "Réinitialiser", - "resetProgressTitle": "Réinitialiser la progression du deck", - "resetProgressConfirm": "Cela réinitialisera toutes les cartes de ce deck à l'état neuf. Votre progression d'apprentissage sera perdue. Êtes-vous sûr?", - "resetSuccess": "{count} cartes réinitialisées avec succès", - "resetting": "Réinitialisation en cours...", + "resetProgress": "Réinitialiser progression", + "resetProgressTitle": "Réinitialiser la progression", + "resetProgressConfirm": "Réinitialiser la progression?", + "resetSuccess": "Progression réinitialisée", + "resetting": "Réinitialisation...", "cancel": "Annuler", "settings": "Paramètres", "settingsTitle": "Paramètres du deck", - "newPerDay": "Nouvelles cartes par jour", - "newPerDayHint": "Nombre maximum de nouvelles cartes par jour", + "newPerDay": "Nouvelles par jour", + "newPerDayHint": "Nouvelles cartes par jour", "revPerDay": "Révisions par jour", - "revPerDayHint": "Nombre maximum de cartes à réviser par jour", + "revPerDayHint": "Révisions par jour", "save": "Enregistrer", "saving": "Enregistrement...", "settingsSaved": "Paramètres enregistrés", - "todayNew": "Nouvelles", - "todayReview": "Révisions", - "todayLearning": "En cours", + "todayNew": "Nouvelles aujourd'hui", + "todayReview": "Révisions aujourd'hui", + "todayLearning": "En apprentissage", "error": { - "update": "Vous n'avez pas la permission de mettre à jour cette carte.", - "delete": "Vous n'avez pas la permission de supprimer cette carte.", - "add": "Vous n'avez pas la permission d'ajouter des cartes à ce deck." - } + "update": "Pas autorisé à modifier", + "delete": "Pas autorisé à supprimer", + "add": "Pas autorisé à ajouter" + }, + "ipaPlaceholder": "Entrer IPA", + "examplePlaceholder": "Entrer exemple", + "wordRequired": "Veuillez entrer un mot", + "definitionRequired": "Veuillez entrer une définition", + "cardAdded": "Carte ajoutée", + "cardType": "Type de carte", + "wordCard": "Carte mot", + "phraseCard": "Carte phrase", + "sentenceCard": "Carte phrase", + "sentence": "Phrase", + "sentencePlaceholder": "Entrer phrase", + "wordPlaceholder": "Entrer mot", + "queryLang": "Langue de requête", + "meanings": "Significations", + "addMeaning": "Ajouter signification", + "partOfSpeech": "Partie du discours", + "deleteConfirm": "Supprimer cette carte?", + "cardDeleted": "Carte supprimée", + "cardUpdated": "Carte mise à jour" }, "home": { "title": "Apprendre les langues", @@ -234,10 +276,10 @@ }, "memorize": { "deck_selector": { - "selectDeck": "Sélectionner un deck", - "noDecks": "Aucun deck trouvé", + "selectDeck": "Choisir deck", + "noDecks": "Pas de decks", "goToDecks": "Aller aux decks", - "noCards": "Aucune carte", + "noCards": "Pas de cartes", "new": "Nouveau", "learning": "Apprentissage", "review": "Révision", @@ -246,29 +288,29 @@ "review": { "loading": "Chargement...", "backToDecks": "Retour aux decks", - "allDone": "Terminé !", - "allDoneDesc": "Vous avez révisé toutes les cartes dues.", + "allDone": "Tout terminé!", + "allDoneDesc": "Apprentissage terminé pour aujourd'hui!", "reviewedCount": "{count} cartes révisées", "progress": "{current} / {total}", "nextReview": "Prochaine révision", "interval": "Intervalle", "ease": "Facilité", - "lapses": "Oublis", - "showAnswer": "Afficher la réponse", + "lapses": "Erreurs", + "showAnswer": "Montrer réponse", "again": "Encore", "hard": "Difficile", "good": "Bien", "easy": "Facile", - "now": "maintenant", - "lessThanMinute": "<1 min", - "inMinutes": "{count} min", - "inHours": "{count}h", - "inDays": "{count}j", - "inMonths": "{count}mois", - "minutes": "<1 min", - "days": "{count}j", - "months": "{count}mois", - "minAbbr": "m", + "now": "Maintenant", + "lessThanMinute": "moins d'une minute", + "inMinutes": "dans {n} minute{s}", + "inHours": "dans {n} heure{s}", + "inDays": "dans {n} jour{s}", + "inMonths": "dans {n} mois", + "minutes": "minutes", + "days": "jours", + "months": "mois", + "minAbbr": "min", "dayAbbr": "j", "cardTypeNew": "Nouveau", "cardTypeLearning": "Apprentissage", @@ -276,14 +318,15 @@ "cardTypeRelearning": "Réapprentissage", "reverse": "Inverser", "dictation": "Dictée", - "clickToPlay": "Cliquez pour jouer", + "clickToPlay": "Cliquer pour jouer", "yourAnswer": "Votre réponse", "typeWhatYouHear": "Tapez ce que vous entendez", - "correct": "Correct", - "incorrect": "Incorrect" + "correct": "Correct!", + "incorrect": "Incorrect", + "nextCard": "Suivant" }, "page": { - "unauthorized": "Vous n'êtes pas autorisé à accéder à ce deck" + "unauthorized": "Non autorisé" } }, "navbar": { @@ -297,33 +340,49 @@ "settings": "Paramètres" }, "ocr": { - "title": "Extraction OCR de vocabulaire", - "description": "Téléchargez des captures d'écran de tableaux de vocabulaire pour extraire les paires mot-définition", - "uploadImage": "Télécharger une image", - "dragDropHint": "Glissez-déposez une image ici, ou cliquez pour sélectionner", - "supportedFormats": "Supportés : JPG, PNG, WebP", - "selectDeck": "Sélectionner un deck", - "chooseDeck": "Choisissez a deck to save the extracted pairs", - "noDecks": "Aucun deck disponible. Please create a deck first.", - "languageHints": "Indices de langue (Optionnel)", - "sourceLanguageHint": "Langue source (ex : Anglais)", - "targetLanguageHint": "Langue cible/traduction (ex : Chinois)", - "process": "Traiter l'image", + "title": "Reconnaissance OCR", + "description": "Extraire le texte des images", + "uploadImage": "Télécharger image", + "dragDropHint": "Glisser-déposer", + "supportedFormats": "Formats: JPG, PNG, WEBP", + "selectDeck": "Choisir deck", + "chooseDeck": "Choisir un deck", + "noDecks": "Pas de decks disponibles", + "languageHints": "Indications de langue", + "sourceLanguageHint": "Langue source", + "targetLanguageHint": "Langue cible", + "process": "Traiter", "processing": "Traitement...", "preview": "Aperçu", "extractedPairs": "Paires extraites", "word": "Mot", "definition": "Définition", - "pairsCount": "{count} paires extraites", - "savePairs": "Sauvegarder dans le deck", - "saving": "Sauvegarde...", - "saved": "{count} paires sauvegardées dans {deck}", - "saveFailed": "Échec de la sauvegarde", - "noImage": "Veuillez first upload an image", - "noDeck": "Please select a deck", - "processingFailed": "Échec du traitement OCR", - "tryAgain": "Please try again with a clearer image", - "detectedLanguages": "Détecté : {source} → {target}" + "pairsCount": "{count} paires", + "savePairs": "Enregistrer", + "saving": "Enregistrement...", + "saved": "Enregistré", + "saveFailed": "Échec de l'enregistrement", + "noImage": "Veuillez télécharger une image", + "noDeck": "Veuillez choisir un deck", + "processingFailed": "Traitement échoué", + "tryAgain": "Réessayer", + "detectedLanguages": "Langues détectées", + "uploadSection": "Télécharger image", + "dropOrClick": "Déposer ou cliquer", + "changeImage": "Changer image", + "invalidFileType": "Type de fichier invalide", + "deckSelection": "Choisir deck", + "sourceLanguagePlaceholder": "ex: Anglais", + "targetLanguagePlaceholder": "ex: Français", + "processButton": "Démarrer reconnaissance", + "resultsPreview": "Aperçu des résultats", + "saveButton": "Enregistrer dans le deck", + "ocrSuccess": "OCR réussi", + "ocrFailed": "OCR échoué", + "savedToDeck": "Enregistré dans le deck", + "noResultsToSave": "Pas de résultats", + "detectedSourceLanguage": "Langue source détectée", + "detectedTargetLanguage": "Langue cible détectée" }, "profile": { "myProfile": "Mon profil", @@ -364,12 +423,43 @@ "videoUploadFailed": "Échec du téléchargement de la vidéo", "subtitleUploadFailed": "Échec du téléchargement des sous-titres", "subtitleLoadSuccess": "Sous-titres chargés avec succès", - "subtitleLoadFailed": "Échec du chargement des sous-titres" + "subtitleLoadFailed": "Échec du chargement des sous-titres", + "settings": "Paramètres", + "shortcuts": "Raccourcis", + "keyboardShortcuts": "Raccourcis clavier", + "playPause": "Lecture/Pause", + "autoPauseToggle": "Pause auto", + "subtitleSettings": "Paramètres sous-titres", + "fontSize": "Taille police", + "textColor": "Couleur texte", + "backgroundColor": "Couleur fond", + "position": "Position", + "opacity": "Opacité", + "top": "Haut", + "center": "Centre", + "bottom": "Bas" }, "text_speaker": { "generateIPA": "Générer l'API", "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)", + "saved": "Enregistré", + "clearAll": "Tout effacer", + "language": "Langue", + "customLanguage": "ou entrer une langue...", + "languages": { + "auto": "Auto", + "chinese": "Chinois", + "english": "Anglais", + "japanese": "Japonais", + "korean": "Coréen", + "french": "Français", + "german": "Allemand", + "italian": "Italien", + "spanish": "Espagnol", + "portuguese": "Portugais", + "russian": "Russe" + } }, "translator": { "detectLanguage": "détecter la langue", @@ -403,7 +493,19 @@ "error": "Échec de l'ajout de la paire de texte au dossier" }, "autoSave": "Sauvegarde automatique", - "customLanguage": "ou tapez la langue..." + "customLanguage": "ou tapez la langue...", + "pleaseLogin": "Connectez-vous pour sauvegarder", + "pleaseCreateDeck": "Créez d'abord un deck", + "noTranslationToSave": "Pas de traduction à sauvegarder", + "noDeckSelected": "Aucun deck sélectionné", + "saveAsCard": "Sauvegarder comme carte", + "selectDeck": "Sélectionner deck", + "front": "Recto", + "back": "Verso", + "cancel": "Annuler", + "save": "Sauvegarder", + "savedToDeck": "Carte sauvegardée dans {deckName}", + "saveFailed": "Échec de la sauvegarde" }, "dictionary": { "title": "Dictionnaire", @@ -448,7 +550,9 @@ "unfavorite": "Retirer des favoris", "pleaseLogin": "Veuillez vous connecter d'abord", "sortByFavorites": "Trier par favoris", - "sortByFavoritesActive": "Annuler le tri par favoris" + "sortByFavoritesActive": "Annuler le tri par favoris", + "noDecks": "Pas de decks publics", + "deckInfo": "{userName} · {totalCards} cartes" }, "exploreDetail": { "title": "Détails du dossier", @@ -462,7 +566,8 @@ "unfavorite": "Retirer des favoris", "favorited": "Ajouté aux favoris", "unfavorited": "Retiré des favoris", - "pleaseLogin": "Veuillez vous connecter d'abord" + "pleaseLogin": "Veuillez vous connecter d'abord", + "totalCards": "{count} cartes" }, "favorites": { "title": "Mes favoris", @@ -507,7 +612,8 @@ "createdAt": "Créé le", "actions": "Actions", "view": "Voir" - } + }, + "joined": "Inscrit le" }, "follow": { "follow": "Suivre", diff --git a/messages/it-IT.json b/messages/it-IT.json index 861f9ad..2f84bad 100644 --- a/messages/it-IT.json +++ b/messages/it-IT.json @@ -53,7 +53,30 @@ "totalCards": "Totale carte", "createdAt": "Creato il", "actions": "Azioni", - "view": "Visualizza" + "view": "Visualizza", + "subtitle": "Gestisci i tuoi deck", + "newDeck": "Nuovo deck", + "noDecksYet": "Nessun deck ancora", + "loading": "Caricamento...", + "deckInfo": "ID: {id} · {totalCards} carte", + "enterDeckName": "Nome deck:", + "enterNewName": "Nuovo nome:", + "confirmDelete": "Digita \"{name}\" per eliminare:", + "public": "Pubblico", + "private": "Privato", + "setPublic": "Rendi pubblico", + "setPrivate": "Rendi privato", + "importApkg": "Importa APKG", + "exportApkg": "Esporta APKG", + "clickToUpload": "Clicca per caricare", + "apkgFilesOnly": "Solo file .apkg", + "parsing": "Analisi...", + "foundDecks": "{count} deck trovati", + "back": "Indietro", + "import": "Importa", + "importing": "Importazione...", + "exportSuccess": "Esportazione riuscita", + "goToDecks": "Vai ai deck" }, "folder_id": { "unauthorized": "Non sei il proprietario di questa cartella", @@ -106,29 +129,48 @@ "edit": "Modifica", "delete": "Elimina", "permissionDenied": "Non hai il permesso per questa accion", - "resetProgress": "Ripristina", - "resetProgressTitle": "Ripristina progresso del deck", - "resetProgressConfirm": "Questo ripristinerá tutte las tarjetas de este deck al nuevo estado. Su progreso de aprendizaje se perderá Are you seguro?", - "resetSuccess": "{count} tarjetas ripristinate exitosamente", - "resetting": "Ripristazione en curso...", + "resetProgress": "Reimposta progresso", + "resetProgressTitle": "Reimposta progresso di apprendimento", + "resetProgressConfirm": "Reimpostare il progresso?", + "resetSuccess": "Progresso reimpostato", + "resetting": "Reimpostazione...", "cancel": "Annulla", "settings": "Impostazioni", "settingsTitle": "Impostazioni deck", - "newPerDay": "Nuove schede al giorno", - "newPerDayHint": "Numero massimo di nuove schede al giorno", - "revPerDay": "Ripassi al giorno", - "revPerDayHint": "Numero massimo di schede da ripassare al giorno", + "newPerDay": "Nuove al giorno", + "newPerDayHint": "Nuove carte al giorno", + "revPerDay": "Ripassate al giorno", + "revPerDayHint": "Ripassi al giorno", "save": "Salva", "saving": "Salvataggio...", "settingsSaved": "Impostazioni salvate", - "todayNew": "Nuove", - "todayReview": "Ripasso", - "todayLearning": "In corso", + "todayNew": "Oggi nuove", + "todayReview": "Oggi ripasso", + "todayLearning": "In apprendimento", "error": { - "update": "Non hai il permesso per aggiorn this card.", - "delete": "Non hai il permesso per delete this card.", - "add": "Non hai il permesso per add cards to this deck." - } + "update": "Nessun permesso di aggiornare", + "delete": "Nessun permesso di eliminare", + "add": "Nessun permesso di aggiungere" + }, + "ipaPlaceholder": "Inserisci IPA", + "examplePlaceholder": "Inserisci esempio", + "wordRequired": "Inserisci una parola", + "definitionRequired": "Inserisci una definizione", + "cardAdded": "Carta aggiunta", + "cardType": "Tipo di carta", + "wordCard": "Carta parola", + "phraseCard": "Carta frase", + "sentenceCard": "Carta frase", + "sentence": "Frase", + "sentencePlaceholder": "Inserisci frase", + "wordPlaceholder": "Inserisci parola", + "queryLang": "Lingua di query", + "meanings": "Significati", + "addMeaning": "Aggiungi significato", + "partOfSpeech": "Parte del discorso", + "deleteConfirm": "Eliminare questa carta?", + "cardDeleted": "Carta eliminata", + "cardUpdated": "Carta aggiornata" }, "home": { "title": "Impara le Lingue", @@ -234,41 +276,41 @@ }, "memorize": { "deck_selector": { - "selectDeck": "Seleziona un mazzo", - "noDecks": "Nessun mazzo trovato", - "goToDecks": "Vai ai mazzi", + "selectDeck": "Seleziona deck", + "noDecks": "Nessun deck", + "goToDecks": "Vai ai deck", "noCards": "Nessuna carta", - "new": "Nuove", - "learning": "In apprendimento", + "new": "Nuovo", + "learning": "Apprendimento", "review": "Ripasso", "due": "In scadenza" }, "review": { "loading": "Caricamento...", - "backToDecks": "Torna ai mazzi", - "allDone": "Fatto!", - "allDoneDesc": "Hai ripassato tutte le carte in scadenza.", + "backToDecks": "Torna ai deck", + "allDone": "Tutto fatto!", + "allDoneDesc": "Apprendimento di oggi completato!", "reviewedCount": "{count} carte ripassate", "progress": "{current} / {total}", - "nextReview": "Prossima revisione", + "nextReview": "Prossimo ripasso", "interval": "Intervallo", - "ease": "Facilità", - "lapses": "Dimenticanze", + "ease": "Difficoltà", + "lapses": "Errori", "showAnswer": "Mostra risposta", "again": "Ancora", "hard": "Difficile", - "good": "Bene", + "good": "Buono", "easy": "Facile", - "now": "ora", - "lessThanMinute": "<1 min", - "inMinutes": "{count} min", - "inHours": "{count}h", - "inDays": "{count}g", - "inMonths": "{count}mesi", - "minutes": "<1 min", - "days": "{count}g", - "months": "{count}mesi", - "minAbbr": "m", + "now": "Ora", + "lessThanMinute": "meno di 1 minuto", + "inMinutes": "tra {n} minuti", + "inHours": "tra {n} ore", + "inDays": "tra {n} giorni", + "inMonths": "tra {n} mesi", + "minutes": "minuti", + "days": "giorni", + "months": "mesi", + "minAbbr": "min", "dayAbbr": "g", "cardTypeNew": "Nuovo", "cardTypeLearning": "Apprendimento", @@ -278,12 +320,13 @@ "dictation": "Dettato", "clickToPlay": "Clicca per riprodurre", "yourAnswer": "La tua risposta", - "typeWhatYouHear": "Scrivi ciò che senti", - "correct": "Corretto", - "incorrect": "Errato" + "typeWhatYouHear": "Scrivi cosa senti", + "correct": "Corretto!", + "incorrect": "Errato", + "nextCard": "Prossima" }, "page": { - "unauthorized": "Non sei autorizzato ad accedere a questo mazzo" + "unauthorized": "Non autorizzato" } }, "navbar": { @@ -297,33 +340,49 @@ "settings": "Impostazioni" }, "ocr": { - "title": "Estrazione vocaboli OCR", - "description": "Carica screenshot di tabelle di vocaboli per estrarre coppie parola-definizione", + "title": "Riconoscimento OCR", + "description": "Estrai testo dalle immagini", "uploadImage": "Carica immagine", - "dragDropHint": "Trascina e rilascia un'immagine qui, o clicca per selezionare", - "supportedFormats": "Supportati: JPG, PNG, WebP", - "selectDeck": "Seleziona un mazzo", - "chooseDeck": "Scegli un mazzo per salvare le coppie estratte", - "noDecks": "Nessun mazzo disponibile. Creane prima un mazzo.", - "languageHints": "Suggerimenti lingua (Opzionale)", - "sourceLanguageHint": "Lingua sorgente (es: Inglese)", - "targetLanguageHint": "Lingua target/traduzione (es: Cinese)", - "process": "Elabora immagine", + "dragDropHint": "Trascina e rilascia", + "supportedFormats": "Supportati: JPG, PNG, WEBP", + "selectDeck": "Seleziona deck", + "chooseDeck": "Scegli un deck", + "noDecks": "Nessun deck disponibile", + "languageHints": "Suggerimenti lingua", + "sourceLanguageHint": "Lingua sorgente", + "targetLanguageHint": "Lingua target", + "process": "Elabora", "processing": "Elaborazione...", "preview": "Anteprima", "extractedPairs": "Coppie estratte", "word": "Parola", "definition": "Definizione", - "pairsCount": "{count} coppie estratte", - "savePairs": "Salva nel mazzo", + "pairsCount": "{count} coppie", + "savePairs": "Salva", "saving": "Salvataggio...", - "saved": "{count} coppie salvate in {deck}", + "saved": "Salvato", "saveFailed": "Salvataggio fallito", - "noImage": "Carica prima un'immagine", - "noDeck": "Seleziona un mazzo", - "processingFailed": "Elaborazione OCR fallita", - "tryAgain": "Riprova con un'immagine più chiara", - "detectedLanguages": "Rilevato: {source} → {target}" + "noImage": "Carica un'immagine", + "noDeck": "Seleziona un deck", + "processingFailed": "Elaborazione fallita", + "tryAgain": "Riprova", + "detectedLanguages": "Lingue rilevate", + "uploadSection": "Carica immagine", + "dropOrClick": "Rilascia o clicca", + "changeImage": "Cambia immagine", + "invalidFileType": "Tipo di file non valido", + "deckSelection": "Seleziona deck", + "sourceLanguagePlaceholder": "es: Inglese", + "targetLanguagePlaceholder": "es: Italiano", + "processButton": "Avvia riconoscimento", + "resultsPreview": "Anteprima risultati", + "saveButton": "Salva nel deck", + "ocrSuccess": "OCR riuscito", + "ocrFailed": "OCR fallito", + "savedToDeck": "Salvato nel deck", + "noResultsToSave": "Nessun risultato", + "detectedSourceLanguage": "Lingua sorgente rilevata", + "detectedTargetLanguage": "Lingua target rilevata" }, "profile": { "myProfile": "Il Mio Profilo", @@ -364,12 +423,43 @@ "videoUploadFailed": "Caricamento video fallito", "subtitleUploadFailed": "Caricamento sottotitoli fallito", "subtitleLoadSuccess": "Sottotitoli caricati con successo", - "subtitleLoadFailed": "Caricamento sottotitoli fallito" + "subtitleLoadFailed": "Caricamento sottotitoli fallito", + "settings": "Impostazioni", + "shortcuts": "Scorciatoie", + "keyboardShortcuts": "Scorciatoie tastiera", + "playPause": "Riproduci/Pausa", + "autoPauseToggle": "Auto-pausa", + "subtitleSettings": "Impostazioni sottotitoli", + "fontSize": "Dimensione carattere", + "textColor": "Colore testo", + "backgroundColor": "Colore sfondo", + "position": "Posizione", + "opacity": "Opacità", + "top": "Alto", + "center": "Centro", + "bottom": "Basso" }, "text_speaker": { "generateIPA": "Genera IPA", "viewSavedItems": "Visualizza Elementi Salvati", - "confirmDeleteAll": "Sei sicuro di voler eliminare tutto? (S/N)" + "confirmDeleteAll": "Sei sicuro di voler eliminare tutto? (S/N)", + "saved": "Salvato", + "clearAll": "Cancella tutto", + "language": "Lingua", + "customLanguage": "o inserisci lingua...", + "languages": { + "auto": "Auto", + "chinese": "Cinese", + "english": "Inglese", + "japanese": "Giapponese", + "korean": "Coreano", + "french": "Francese", + "german": "Tedesco", + "italian": "Italiano", + "spanish": "Spagnolo", + "portuguese": "Portoghese", + "russian": "Russo" + } }, "translator": { "detectLanguage": "rileva lingua", @@ -403,7 +493,19 @@ "error": "Impossibile aggiungere coppia di testo alla cartella" }, "autoSave": "Salvataggio Automatico", - "customLanguage": "o digita lingua..." + "customLanguage": "o digita lingua...", + "pleaseLogin": "Accedi per salvare le carte", + "pleaseCreateDeck": "Crea prima un deck", + "noTranslationToSave": "Nessuna traduzione da salvare", + "noDeckSelected": "Nessun deck selezionato", + "saveAsCard": "Salva come carta", + "selectDeck": "Seleziona deck", + "front": "Fronte", + "back": "Retro", + "cancel": "Annulla", + "save": "Salva", + "savedToDeck": "Carta salvata in {deckName}", + "saveFailed": "Salvataggio fallito" }, "dictionary": { "title": "Dizionario", @@ -448,7 +550,9 @@ "unfavorite": "Rimuovi dai preferiti", "pleaseLogin": "Per favore accedi prima", "sortByFavorites": "Ordina per preferiti", - "sortByFavoritesActive": "Annulla ordinamento per preferiti" + "sortByFavoritesActive": "Annulla ordinamento per preferiti", + "noDecks": "Nessun deck pubblico", + "deckInfo": "{userName} · {totalCards} carte" }, "exploreDetail": { "title": "Dettagli Cartella", @@ -462,7 +566,8 @@ "unfavorite": "Rimuovi dai preferiti", "favorited": "Aggiunto ai preferiti", "unfavorited": "Rimosso dai preferiti", - "pleaseLogin": "Per favore accedi prima" + "pleaseLogin": "Per favore accedi prima", + "totalCards": "{count} carte" }, "favorites": { "title": "I Miei Preferiti", @@ -507,7 +612,8 @@ "createdAt": "Creata Il", "actions": "Azioni", "view": "Visualizza" - } + }, + "joined": "Iscritto il" }, "follow": { "follow": "Segui", diff --git a/messages/ja-JP.json b/messages/ja-JP.json index 6721c04..243d2e4 100644 --- a/messages/ja-JP.json +++ b/messages/ja-JP.json @@ -97,29 +97,48 @@ "edit": "編集", "delete": "削除", "permissionDenied": "この操作を実行する権限がありません", - "resetProgress": "リセット", - "resetProgressTitle": "デッキの進捗をリセット", - "resetProgressConfirm": "このデッキのすべてのカードが新しい状態にリセットされます。学習の進捗は失われます。続行してもよろしいですか?", - "resetSuccess": "{count}枚のカードを正常にリセットしました", + "resetProgress": "進捗をリセット", + "resetProgressTitle": "学習進捗をリセット", + "resetProgressConfirm": "このデッキの学習進捗をリセットしますか?", + "resetSuccess": "リセットしました", "resetting": "リセット中...", "cancel": "キャンセル", "settings": "設定", "settingsTitle": "デッキ設定", - "newPerDay": "1日の新規カード数", - "newPerDayHint": "1日に学習する新規カードの最大数", - "revPerDay": "1日の復習カード数", - "revPerDayHint": "1日に復習するカードの最大数", + "newPerDay": "1日の新規カード", + "newPerDayHint": "毎日の新規カード数", + "revPerDay": "1日の復習", + "revPerDayHint": "毎日の復習数", "save": "保存", "saving": "保存中...", "settingsSaved": "設定を保存しました", - "todayNew": "新規", - "todayReview": "復習", + "todayNew": "今日の新規", + "todayReview": "今日の復習", "todayLearning": "学習中", "error": { - "update": "このカードを更新する権限がありません。", - "delete": "このカードを削除する権限がありません。", - "add": "このデッキにカードを追加する権限がありません。" - } + "update": "更新する権限がありません", + "delete": "削除する権限がありません", + "add": "追加する権限がありません" + }, + "ipaPlaceholder": "IPAを入力", + "examplePlaceholder": "例文を入力", + "wordRequired": "単語を入力してください", + "definitionRequired": "定義を入力してください", + "cardAdded": "カードを追加しました", + "cardType": "カードタイプ", + "wordCard": "単語カード", + "phraseCard": "フレーズカード", + "sentenceCard": "文章カード", + "sentence": "文章", + "sentencePlaceholder": "文章を入力", + "wordPlaceholder": "単語を入力", + "queryLang": "検索言語", + "meanings": "意味", + "addMeaning": "意味を追加", + "partOfSpeech": "品詞", + "deleteConfirm": "このカードを削除しますか?", + "cardDeleted": "カードを削除しました", + "cardUpdated": "カードを更新しました" }, "home": { "title": "言語を学ぶ", @@ -271,7 +290,8 @@ "yourAnswer": "あなたの答え", "typeWhatYouHear": "聞こえた内容を入力", "correct": "正解", - "incorrect": "不正解" + "incorrect": "不正解", + "nextCard": "次へ" }, "page": { "unauthorized": "このデッキにアクセスする権限がありません" @@ -288,35 +308,49 @@ "settings": "設定" }, "ocr": { - "title": "OCR語彙抽出", - "description": "教科書の語彙表のスクリーンショットをアップロードして単語と定義のペアを抽出", + "title": "OCR認識", + "description": "画像からテキストを抽出", "uploadImage": "画像をアップロード", - "dragDropHint": "ここに画像をドラッグ&ドロップ、またはクリックして選択", - "supportedFormats": "対応形式:JPG、PNG、WebP", + "dragDropHint": "ドラッグ&ドロップ", + "supportedFormats": "対応形式:JPG, PNG, WEBP", "selectDeck": "デッキを選択", - "chooseDeck": "抽出したペアを保存するデッキを選択", - "noDecks": "デッキがありません。まずデッキを作成してください。", - "languageHints": "言語ヒント(オプション)", - "sourceLanguageHint": "ソース言語(例:英語)", - "targetLanguageHint": "ターゲット/翻訳言語(例:中国語)", - "process": "画像を処理", + "chooseDeck": "デッキを選択", + "noDecks": "デッキがありません", + "languageHints": "言語ヒント", + "sourceLanguageHint": "ソース言語ヒント", + "targetLanguageHint": "ターゲット言語ヒント", + "process": "処理", "processing": "処理中...", "preview": "プレビュー", - "extractedPairs": "抽出されたペア", + "extractedPairs": "抽出ペア", "word": "単語", "definition": "定義", - "pairsCount": "{count} ペアを抽出", - "savePairs": "デッキに保存", + "pairsCount": "{count}ペア", + "savePairs": "保存", "saving": "保存中...", - "saved": "{count} ペアを {deck} に保存しました", - "saveFailed": "保存に失敗しました", - "noImage": "先に画像をアップロードしてください", + "saved": "保存済み", + "saveFailed": "保存失敗", + "noImage": "画像をアップロードしてください", "noDeck": "デッキを選択してください", - "processingFailed": "OCR処理に失敗しました", - "tryAgain": "より鮮明な画像でお試しください", - "detectedLanguages": "検出:{source} → {target}", - "invalidFileType": "無効なファイルタイプです。画像ファイルをアップロードしてください。", - "ocrFailed": "OCR処理に失敗しました。" + "processingFailed": "処理失敗", + "tryAgain": "再試行", + "detectedLanguages": "検出言語", + "invalidFileType": "無効なファイル形式", + "ocrFailed": "OCR失敗", + "uploadSection": "画像をアップロード", + "dropOrClick": "ドロップまたはクリック", + "changeImage": "画像を変更", + "deckSelection": "デッキを選択", + "sourceLanguagePlaceholder": "例:英語", + "targetLanguagePlaceholder": "例:日本語", + "processButton": "認識開始", + "resultsPreview": "結果プレビュー", + "saveButton": "デッキに保存", + "ocrSuccess": "OCR成功", + "savedToDeck": "デッキに保存しました", + "noResultsToSave": "結果がありません", + "detectedSourceLanguage": "検出ソース言語", + "detectedTargetLanguage": "検出ターゲット言語" }, "profile": { "myProfile": "マイプロフィール", @@ -362,7 +396,7 @@ "shortcuts": "ショートカット", "keyboardShortcuts": "キーボードショートカット", "playPause": "再生/一時停止", - "autoPauseToggle": "自動一時停止の切り替え", + "autoPauseToggle": "自動一時停止", "subtitleSettings": "字幕設定", "fontSize": "フォントサイズ", "textColor": "文字色", @@ -378,7 +412,22 @@ "viewSavedItems": "保存済み項目を表示", "confirmDeleteAll": "すべて削除してもよろしいですか? (Y/N)", "saved": "保存済み", - "clearAll": "すべてクリア" + "clearAll": "すべてクリア", + "language": "言語", + "customLanguage": "または言語を入力...", + "languages": { + "auto": "自動", + "chinese": "中国語", + "english": "英語", + "japanese": "日本語", + "korean": "韓国語", + "french": "フランス語", + "german": "ドイツ語", + "italian": "イタリア語", + "spanish": "スペイン語", + "portuguese": "ポルトガル語", + "russian": "ロシア語" + } }, "translator": { "detectLanguage": "言語を検出", @@ -412,7 +461,19 @@ "error": "テキストペアをフォルダーに追加できませんでした" }, "autoSave": "自動保存", - "customLanguage": "または言語を入力..." + "customLanguage": "または言語を入力...", + "pleaseLogin": "ログインしてカードを保存", + "pleaseCreateDeck": "先にデッキを作成", + "noTranslationToSave": "保存する翻訳なし", + "noDeckSelected": "デッキ未選択", + "saveAsCard": "カードとして保存", + "selectDeck": "デッキ選択", + "front": "表面", + "back": "裏面", + "cancel": "キャンセル", + "save": "保存", + "savedToDeck": "{deckName}に保存", + "saveFailed": "保存失敗" }, "dictionary": { "title": "辞書", @@ -457,7 +518,9 @@ "unfavorite": "お気に入り解除", "pleaseLogin": "まずログインしてください", "sortByFavorites": "お気に入り順に並べ替え", - "sortByFavoritesActive": "お気に入り順の並べ替えを解除" + "sortByFavoritesActive": "お気に入り順の並べ替えを解除", + "noDecks": "公開デッキなし", + "deckInfo": "{userName} · {totalCards}枚" }, "exploreDetail": { "title": "フォルダー詳細", @@ -471,7 +534,8 @@ "unfavorite": "お気に入り解除", "favorited": "お気に入りに追加しました", "unfavorited": "お気に入りから削除しました", - "pleaseLogin": "まずログインしてください" + "pleaseLogin": "まずログインしてください", + "totalCards": "{count}枚" }, "favorites": { "title": "マイお気に入り", @@ -516,34 +580,35 @@ "createdAt": "作成日", "actions": "アクション", "view": "表示" - } + }, + "joined": "登録日" }, "decks": { "title": "デッキ", - "subtitle": "フラッシュカードデッキを管理", + "subtitle": "学習デッキを管理", "newDeck": "新規デッキ", - "noDecksYet": "まだデッキがありません", - "loading": "読み込み中...", - "deckInfo": "ID: {id} • {totalCards} 枚のカード", - "enterDeckName": "デッキ名を入力:", - "enterNewName": "新しい名前を入力:", - "confirmDelete": "削除するには「{name}」と入力してください:", + "noDecksYet": "デッキなし", + "loading": "読込中...", + "deckInfo": "ID: {id} · {totalCards}枚", + "enterDeckName": "デッキ名:", + "enterNewName": "新しい名前:", + "confirmDelete": "削除確認:「{name}」を入力", "public": "公開", "private": "非公開", "setPublic": "公開に設定", "setPrivate": "非公開に設定", - "importApkg": "APKGをインポート", - "exportApkg": "APKGをエクスポート", - "clickToUpload": "クリックしてAPKGファイルをアップロード", - "apkgFilesOnly": ".apkgファイルのみ対応", + "importApkg": "APKGインポート", + "exportApkg": "APKGエクスポート", + "clickToUpload": "クリックでアップロード", + "apkgFilesOnly": ".apkgのみ", "parsing": "解析中...", - "foundDecks": "{count} 個のデッキが見つかりました", + "foundDecks": "{count}デッキ発見", "deckName": "デッキ名", "back": "戻る", "import": "インポート", "importing": "インポート中...", - "exportSuccess": "デッキのエクスポートが成功しました", - "goToDecks": "デッキへ移動" + "exportSuccess": "エクスポート成功", + "goToDecks": "デッキへ" }, "follow": { "follow": "フォロー", diff --git a/messages/ko-KR.json b/messages/ko-KR.json index 6dde2d7..4593d53 100644 --- a/messages/ko-KR.json +++ b/messages/ko-KR.json @@ -53,7 +53,30 @@ "totalCards": "총 카드", "createdAt": "생성일", "actions": "작업", - "view": "보기" + "view": "보기", + "subtitle": "학습 덱 관리", + "newDeck": "새 덱", + "noDecksYet": "덱이 없습니다", + "loading": "로딩 중...", + "deckInfo": "ID: {id} · {totalCards}장", + "enterDeckName": "덱 이름 입력:", + "enterNewName": "새 이름 입력:", + "confirmDelete": "삭제하려면 \"{name}\" 입력:", + "public": "공개", + "private": "비공개", + "setPublic": "공개로 설정", + "setPrivate": "비공개로 설정", + "importApkg": "APKG 가져오기", + "exportApkg": "APKG 내보내기", + "clickToUpload": "클릭하여 업로드", + "apkgFilesOnly": ".apkg 파일만", + "parsing": "파싱 중...", + "foundDecks": "{count}개 덱 발견", + "back": "뒤로", + "import": "가져오기", + "importing": "가져오는 중...", + "exportSuccess": "내보내기 성공", + "goToDecks": "덱으로" }, "folder_id": { "unauthorized": "이 폴더의 소유자가 아닙니다", @@ -106,29 +129,48 @@ "edit": "편집", "delete": "삭제", "permissionDenied": "이 작업을 수행할 권한이 없습니다", - "resetProgress": "초기화", - "resetProgressTitle": "덱 진행 초기화", - "resetProgressConfirm": "이 덱의 모든 카드가 새 상태로 초기화됩니다. 학습 진행 상황이 손실됩니다. 계속하시겠습니까?", - "resetSuccess": "{count}개 카드 초기화 완료", + "resetProgress": "진행 초기화", + "resetProgressTitle": "학습 진행 초기화", + "resetProgressConfirm": "이 덱의 학습 진행을 초기화하시겠습니까?", + "resetSuccess": "초기화됨", "resetting": "초기화 중...", "cancel": "취소", "settings": "설정", "settingsTitle": "덱 설정", - "newPerDay": "하루 새 카드 수", - "newPerDayHint": "하루에 학습할 최대 새 카드 수", - "revPerDay": "하루 복습 카드 수", - "revPerDayHint": "하루에 복습할 최대 카드 수", + "newPerDay": "일일 새 카드", + "newPerDayHint": "매일 학습할 새 카드 수", + "revPerDay": "일일 복습", + "revPerDayHint": "매일 복습할 카드 수", "save": "저장", "saving": "저장 중...", "settingsSaved": "설정 저장됨", - "todayNew": "새 카드", - "todayReview": "복습", + "todayNew": "오늘 새 카드", + "todayReview": "오늘 복습", "todayLearning": "학습 중", "error": { - "update": "이 카드를 업데이트할 권한이 없습니다.", - "delete": "이 카드를 삭제할 권한이 없습니다.", - "add": "이 덱에 카드를 추가할 권한이 없습니다." - } + "update": "업데이트 권한이 없습니다", + "delete": "삭제 권한이 없습니다", + "add": "추가 권한이 없습니다" + }, + "ipaPlaceholder": "IPA 입력", + "examplePlaceholder": "예문 입력", + "wordRequired": "단어를 입력하세요", + "definitionRequired": "정의를 입력하세요", + "cardAdded": "카드 추가됨", + "cardType": "카드 유형", + "wordCard": "단어 카드", + "phraseCard": "구문 카드", + "sentenceCard": "문장 카드", + "sentence": "문장", + "sentencePlaceholder": "문장 입력", + "wordPlaceholder": "단어 입력", + "queryLang": "검색 언어", + "meanings": "의미", + "addMeaning": "의미 추가", + "partOfSpeech": "품사", + "deleteConfirm": "이 카드를 삭제하시겠습니까?", + "cardDeleted": "카드 삭제됨", + "cardUpdated": "카드 업데이트됨" }, "home": { "title": "언어 배우기", @@ -235,10 +277,10 @@ "memorize": { "deck_selector": { "selectDeck": "덱 선택", - "noDecks": "덱을 찾을 수 없습니다", + "noDecks": "덱이 없습니다", "goToDecks": "덱으로 이동", - "noCards": "카드 없음", - "new": "새 카드", + "noCards": "카드가 없습니다", + "new": "새로", "learning": "학습 중", "review": "복습", "due": "예정" @@ -246,44 +288,45 @@ "review": { "loading": "로딩 중...", "backToDecks": "덱으로 돌아가기", - "allDone": "완료!", - "allDoneDesc": "모든 복습 카드를 완료했습니다.", - "reviewedCount": "{count}장의 카드 복습함", + "allDone": "모두 완료!", + "allDoneDesc": "오늘의 학습을 완료했습니다!", + "reviewedCount": "{count}장 복습 완료", "progress": "{current} / {total}", "nextReview": "다음 복습", "interval": "간격", "ease": "난이도", - "lapses": "망각 횟수", + "lapses": "실패 횟수", "showAnswer": "정답 보기", "again": "다시", "hard": "어려움", - "good": "보통", + "good": "좋음", "easy": "쉬움", "now": "지금", - "lessThanMinute": "<1분", - "inMinutes": "{count}분", - "inHours": "{count}시간", - "inDays": "{count}일", - "inMonths": "{count}개월", - "minutes": "<1분", - "days": "{count}일", - "months": "{count}개월", + "lessThanMinute": "1분 미만", + "inMinutes": "{n}분 후", + "inHours": "{n}시간 후", + "inDays": "{n}일 후", + "inMonths": "{n}개월 후", + "minutes": "분", + "days": "일", + "months": "개월", "minAbbr": "분", "dayAbbr": "일", "cardTypeNew": "새 카드", "cardTypeLearning": "학습 중", - "cardTypeReview": "복습 중", - "cardTypeRelearning": "재학습 중", - "reverse": "반전", + "cardTypeReview": "복습", + "cardTypeRelearning": "재학습", + "reverse": "반대로", "dictation": "받아쓰기", "clickToPlay": "클릭하여 재생", - "yourAnswer": "당신의 답변", + "yourAnswer": "당신의 답", "typeWhatYouHear": "들은 내용을 입력하세요", - "correct": "정답", - "incorrect": "오답" + "correct": "정답!", + "incorrect": "오답", + "nextCard": "다음" }, "page": { - "unauthorized": "이 덱에 접근할 권한이 없습니다" + "unauthorized": "권한이 없습니다" } }, "navbar": { @@ -297,33 +340,49 @@ "settings": "설정" }, "ocr": { - "title": "OCR 어휘 추출", - "description": "교과서 어휘표 스크린샷 only어업로드하여 단어-정의 쌍 추출", + "title": "OCR 인식", + "description": "이미지에서 텍스트 추출", "uploadImage": "이미지 업로드", - "dragDropHint": "이미지를 여기에 끌어다 놓거나 클릭하여 선택", - "supportedFormats": "지원 형식: JPG, PNG, WebP", + "dragDropHint": "드래그 앤 드롭", + "supportedFormats": "지원 형식: JPG, PNG, WEBP", "selectDeck": "덱 선택", - "chooseDeck": "추출된 쌍을 저장할 덱 선택", - "noDecks": "덱이 없습니다. 먼저 덱을 만드세요.", - "languageHints": "언어 힌트 (선택사항)", - "sourceLanguageHint": "소스 언어 (예: 영어)", - "targetLanguageHint": "대상/번역 언어 (예: 중국어)", - "process": "이미지 처리", - "processing": "처리中...", + "chooseDeck": "덱 선택", + "noDecks": "덱이 없습니다", + "languageHints": "언어 힌트", + "sourceLanguageHint": "원본 언어 힌트", + "targetLanguageHint": "대상 언어 힌트", + "process": "처리", + "processing": "처리 중...", "preview": "미리보기", "extractedPairs": "추출된 쌍", "word": "단어", "definition": "정의", - "pairsCount": "{count} 쌍 추출됨", - "savePairs": "덱에 저장", - "saving": "저장中...", - "saved": "{deck}에 {count} 쌍 저장 완료", + "pairsCount": "{count}쌍", + "savePairs": "저장", + "saving": "저장 중...", + "saved": "저장됨", "saveFailed": "저장 실패", - "noImage": "먼저 이미지를 업로드하세요", + "noImage": "이미지를 업로드하세요", "noDeck": "덱을 선택하세요", - "processingFailed": "OCR 처리 실패", - "tryAgain": "더 선晰的图像로 다시 시도하세요", - "detectedLanguages": "감지됨: {source} → {target}" + "processingFailed": "처리 실패", + "tryAgain": "재시도", + "detectedLanguages": "감지된 언어", + "uploadSection": "이미지 업로드", + "dropOrClick": "드롭 또는 클릭", + "changeImage": "이미지 변경", + "invalidFileType": "잘못된 파일 형식", + "deckSelection": "덱 선택", + "sourceLanguagePlaceholder": "예: 영어", + "targetLanguagePlaceholder": "예: 한국어", + "processButton": "인식 시작", + "resultsPreview": "결과 미리보기", + "saveButton": "덱에 저장", + "ocrSuccess": "OCR 성공", + "ocrFailed": "OCR 실패", + "savedToDeck": "덱에 저장됨", + "noResultsToSave": "저장할 결과 없음", + "detectedSourceLanguage": "감지된 원본 언어", + "detectedTargetLanguage": "감지된 대상 언어" }, "profile": { "myProfile": "내 프로필", @@ -364,12 +423,43 @@ "videoUploadFailed": "비디오 업로드 실패", "subtitleUploadFailed": "자막 업로드 실패", "subtitleLoadSuccess": "자막 로드 성공", - "subtitleLoadFailed": "자막 로드 실패" + "subtitleLoadFailed": "자막 로드 실패", + "settings": "설정", + "shortcuts": "단축키", + "keyboardShortcuts": "키보드 단축키", + "playPause": "재생/일시정지", + "autoPauseToggle": "자동 일시정지", + "subtitleSettings": "자막 설정", + "fontSize": "글꼴 크기", + "textColor": "글자 색", + "backgroundColor": "배경색", + "position": "위치", + "opacity": "불투명도", + "top": "위", + "center": "중앙", + "bottom": "아래" }, "text_speaker": { "generateIPA": "IPA 생성", "viewSavedItems": "저장된 항목 보기", - "confirmDeleteAll": "모든 것을 삭제하시겠습니까? (Y/N)" + "confirmDeleteAll": "모든 것을 삭제하시겠습니까? (Y/N)", + "saved": "저장됨", + "clearAll": "모두 지우기", + "language": "언어", + "customLanguage": "또는 언어 입력...", + "languages": { + "auto": "자동", + "chinese": "중국어", + "english": "영어", + "japanese": "일본어", + "korean": "한국어", + "french": "프랑스어", + "german": "독일어", + "italian": "이탈리아어", + "spanish": "스페인어", + "portuguese": "포르투갈어", + "russian": "러시아어" + } }, "translator": { "detectLanguage": "언어 감지", @@ -403,7 +493,19 @@ "error": "폴더에 텍스트 쌍 추가 실패" }, "autoSave": "자동 저장", - "customLanguage": "또는 언어 입력..." + "customLanguage": "또는 언어 입력...", + "pleaseLogin": "카드를 저장하려면 로그인하세요", + "pleaseCreateDeck": "먼저 덱을 만드세요", + "noTranslationToSave": "저장할 번역이 없습니다", + "noDeckSelected": "덱이 선택되지 않았습니다", + "saveAsCard": "카드로 저장", + "selectDeck": "덱 선택", + "front": "앞면", + "back": "뒷면", + "cancel": "취소", + "save": "저장", + "savedToDeck": "{deckName}에 카드 저장됨", + "saveFailed": "카드 저장 실패" }, "dictionary": { "title": "사전", @@ -448,7 +550,9 @@ "unfavorite": "즐겨찾기 해제", "pleaseLogin": "먼저 로그인해주세요", "sortByFavorites": "즐겨찾기순 정렬", - "sortByFavoritesActive": "즐겨찾기순 정렬 해제" + "sortByFavoritesActive": "즐겨찾기순 정렬 해제", + "noDecks": "공개 덱 없음", + "deckInfo": "{userName} · {totalCards}장" }, "exploreDetail": { "title": "폴더 상세", @@ -462,7 +566,8 @@ "unfavorite": "즐겨찾기 해제", "favorited": "즐겨찾기됨", "unfavorited": "즐겨찾기 해제됨", - "pleaseLogin": "먼저 로그인해주세요" + "pleaseLogin": "먼저 로그인해주세요", + "totalCards": "총 {count}장" }, "favorites": { "title": "내 즐겨찾기", @@ -507,6 +612,16 @@ "createdAt": "생성일", "actions": "작업", "view": "보기" + }, + "joined": "가입일", + "decks": { + "title": "내 덱", + "noDecks": "덱이 없습니다", + "deckName": "덱 이름", + "totalCards": "총 카드", + "createdAt": "생성일", + "actions": "작업", + "view": "보기" } }, "follow": { diff --git a/messages/ug-CN.json b/messages/ug-CN.json index 96776b2..41df408 100644 --- a/messages/ug-CN.json +++ b/messages/ug-CN.json @@ -53,7 +53,30 @@ "totalCards": "جەمئىي كارتا", "createdAt": "قۇرۇلغان ۋاقتى", "actions": "مەشغۇلاتلار", - "view": "كۆرۈش" + "view": "كۆرۈش", + "subtitle": "دېكلەرنى باشقۇرۇڭ", + "newDeck": "يېڭى دېك", + "noDecksYet": "دېك يوق", + "loading": "يۈكلىنىۋاتىدۇ...", + "deckInfo": "ID: {id} · {totalCards} كارتا", + "enterDeckName": "دېك ئاتى:", + "enterNewName": "يېڭى ئات:", + "confirmDelete": "ئۆچۈرۈش: \"{name}\"", + "public": "ئاممىۋىي", + "private": "شەخسىي", + "setPublic": "ئاممىۋىي قىلىش", + "setPrivate": "شەخسىي قىلىش", + "importApkg": "APKG ئەكىرىش", + "exportApkg": "APKG چىقىرىش", + "clickToUpload": "چېكىپ يۈكلەش", + "apkgFilesOnly": ".apkg ھۆججىتىلا", + "parsing": "تەھلىل قىلىنىۋاتىدۇ...", + "foundDecks": "{count} دېك تېپىلدى", + "back": "قايتىش", + "import": "ئەكىرىش", + "importing": "ئەكىرىلىۋاتىدۇ...", + "exportSuccess": "چىقىرىش مۇۋەپپەقىيەتلىك", + "goToDecks": "دېكلەرگە بېرىش" }, "folder_id": { "unauthorized": "بۇ قىسقۇچنىڭ ئىگىسى ئەمەسسىز", @@ -106,29 +129,48 @@ "edit": "تەھرىرلەش", "delete": "ئۆچۈرۈش", "permissionDenied": "بۇ مەشغۇلاتنى ئېلىپ بېرىش ھوقۇقىڭىز يوق", - "resetProgress": "قايتا تەڭشەش", - "resetProgressTitle": "دېك ئىلگىرىلەش قايتا تەڭشەش", - "resetProgressConfirm": "بۇ دېكتىكى بارلىق كارتىلار يېڭى ھالەتكە قايتا تەڭشىلىدۇ. ئۆگىنىش ئىلگىرىلەشلىرىڭىز يوقىلىدۇ. داۋاملاشتۇرامسىز؟", - "resetSuccess": "{count} كارتا مۇۋەپپەقىيەتلىك قايتا تەڭشەلدى", - "resetting": "قايتا تەڭشەۋاتىدۇ...", + "resetProgress": "ئىلگىرىلەشنى ئەسلىگە قايتۇرۇش", + "resetProgressTitle": "ئۆگىنىش ئىلگىرىلەشىنى ئەسلىگە قايتۇرۇش", + "resetProgressConfirm": "ئىلگىرىلەشنى ئەسلىگە قايتۇرامسىز؟", + "resetSuccess": "ئەسلىگە قايتۇرۇلدى", + "resetting": "ئەسلىگە قايتۇرۇۋاتىدۇ...", "cancel": "بىكار قىلىش", "settings": "تەڭشەكلەر", "settingsTitle": "دېك تەڭشەكلىرى", - "newPerDay": "كۈندىلىك يېڭى كارتا سانى", - "newPerDayHint": "ھەر كۈنى ئۆگىنىدىغان ئەڭ كۆپ يېڭى كارتا سانى", - "revPerDay": "كۈندىلىك تەكرار سانى", - "revPerDayHint": "ھەر كۈنى تەكرارلايدىغان ئەڭ كۆپ كارتا سانى", + "newPerDay": "كۈندىلىك يېڭى", + "newPerDayHint": "كۈندە يېڭى كارتا سانى", + "revPerDay": "كۈندىلىك تەكرار", + "revPerDayHint": "كۈندە تەكرار سانى", "save": "ساقلاش", - "saving": "ساقلىنىۋاتىدۇ...", + "saving": "ساقلاۋاتىدۇ...", "settingsSaved": "تەڭشەكلەر ساقلاندى", - "todayNew": "يېڭى", - "todayReview": "تەكرار", + "todayNew": "بۈگۈنكى يېڭى", + "todayReview": "بۈگۈنكى تەكرار", "todayLearning": "ئۆگىنىۋاتىدۇ", "error": { - "update": "بۇ كارتىنى يېڭىلاش ھوقۇقىڭىز يوق.", - "delete": "بۇ كارتىنى ئۆچۈرۈش ھوقۇقىڭىز يوق.", - "add": "بۇ دېككە كارتا قوشۇش ھوقۇقىڭىز يوق." - } + "update": "يېڭىلاش ھوقۇقى يوق", + "delete": "ئۆچۈرۈش ھوقۇقى يوق", + "add": "قوشۇش ھوقۇقى يوق" + }, + "ipaPlaceholder": "IPA كىرگۈزۈڭ", + "examplePlaceholder": "مىسال كىرگۈزۈڭ", + "wordRequired": "سۆز كىرگۈزۈڭ", + "definitionRequired": "ئېنىقلىما كىرگۈزۈڭ", + "cardAdded": "كارتا قوشۇلدى", + "cardType": "كارتا تىپى", + "wordCard": "سۆز كارتىسى", + "phraseCard": "جۈملە كارتىسى", + "sentenceCard": "جۈملە كارتىسى", + "sentence": "جۈملە", + "sentencePlaceholder": "جۈملە كىرگۈزۈڭ", + "wordPlaceholder": "سۆز كىرگۈزۈڭ", + "queryLang": "سۈرۈشتۈرۈش تىلى", + "meanings": "مەنىلىرى", + "addMeaning": "مەنا قوشۇش", + "partOfSpeech": "سۆز بۆلىكى", + "deleteConfirm": "بۇ كارتىنى ئۆچۈرەمسىز؟", + "cardDeleted": "كارتا ئۆچۈرۈلدى", + "cardUpdated": "كارتا يېڭىلاندى" }, "home": { "title": "تىل ئۆگىنىش", @@ -234,56 +276,57 @@ }, "memorize": { "deck_selector": { - "selectDeck": "بىر دېك تاللاڭ", - "noDecks": "دېك تېپىلمىدى", - "goToDecks": "دېكلارغا بېرىڭ", + "selectDeck": "دېك تاللاش", + "noDecks": "دېك يوق", + "goToDecks": "دېكلەرگە بار", "noCards": "كارتا يوق", "new": "يېڭى", - "learning": "ئۆگىنىۋاتىدۇ", + "learning": "ئۆگىنىش", "review": "تەكرار", "due": "ۋاقتى كەلدى" }, "review": { "loading": "يۈكلىنىۋاتىدۇ...", - "backToDecks": "دېكلارغا قايتىڭ", - "allDone": "تامام!", - "allDoneDesc": "بارلىق تەكرارلاش كارتلىرى تاماملاندى.", + "backToDecks": "دېكلەرگە قايتىش", + "allDone": "ھەممىسى تامام!", + "allDoneDesc": "بۈگۈنكى ئۆگىنىش تامام!", "reviewedCount": "{count} كارتا تەكرارلاندى", "progress": "{current} / {total}", "nextReview": "كېيىنكى تەكرار", "interval": "ئارىلىق", - "ease": "ئاسانلىق", - "lapses": "ئۇنتۇش سانى", + "ease": "قىيىنلىق", + "lapses": "خاتالىق", "showAnswer": "جاۋابنى كۆرسەت", "again": "يەنە", "hard": "قىيىن", "good": "ياخشى", "easy": "ئاسان", "now": "ھازىر", - "lessThanMinute": "<1 مىنۇت", - "inMinutes": "{count} مىنۇت", - "inHours": "{count} سائەت", - "inDays": "{count} كۈن", - "inMonths": "{count} ئاي", - "minutes": "<1 مىنۇت", - "days": "{count} كۈن", - "months": "{count} ئاي", - "minAbbr": "م", - "dayAbbr": "ك", + "lessThanMinute": "1 مىنۇتتىن ئاز", + "inMinutes": "{n} مىنۇتتىن كېيىن", + "inHours": "{n} سائەتتىن كېيىن", + "inDays": "{n} كۈندىن كېيىن", + "inMonths": "{n} ئايدىن كېيىن", + "minutes": "مىنۇت", + "days": "كۈن", + "months": "ئاي", + "minAbbr": "مىن", + "dayAbbr": "كۈن", "cardTypeNew": "يېڭى", - "cardTypeLearning": "ئۆگىنىۋاتىدۇ", - "cardTypeReview": "تەكرارلاش", + "cardTypeLearning": "ئۆگىنىش", + "cardTypeReview": "تەكرار", "cardTypeRelearning": "قايتا ئۆگىنىش", - "reverse": "ئەكسى", - "dictation": "ئاڭلاپ يېزىش", + "reverse": "ئەكسىچە", + "dictation": "ئىملا", "clickToPlay": "چېكىپ قويۇش", - "yourAnswer": "سىزنىڭ جاۋابىڭىز", - "typeWhatYouHear": "ئاڭلىغىنىڭىزنى كىرگۈزۈڭ", - "correct": "توغرا", - "incorrect": "خاتا" + "yourAnswer": "جاۋابىڭىز", + "typeWhatYouHear": "ئاڭلىغىنىڭىزنى يېزىڭ", + "correct": "توغرا!", + "incorrect": "خاتا", + "nextCard": "كېيىنكى" }, "page": { - "unauthorized": "بۇ دېكنى زىيارەت قىلىش ھوقۇقىڭىز يوق" + "unauthorized": "ھوقۇقسىز" } }, "navbar": { @@ -297,33 +340,49 @@ "settings": "تەڭشەكلەر" }, "ocr": { - "title": "OCR سۆز ئاستىرىش", - "description": "دەرىسلىك كىتابىدىكى سۆز جەدۋىلى سۈرەتلىرىنى يۈكلەپ سۆز-مەنا جۈپلىرىنى ئاستىرىڭ", - "uploadImage": "سۈرەت يۈكلەش", - "dragDropHint": "سۈرەتنى بۇ يەرگە سۆرەڭ ياكى چېكىپ تاللاڭ", - "supportedFormats": "قوللايدىغان فورماتلار: JPG، PNG، WebP", + "title": "OCR تونۇش", + "description": "رەسىمدىن تېكىست ئېلىش", + "uploadImage": "رەسىم يۈكلەش", + "dragDropHint": "سۆرەپ تاشلاش", + "supportedFormats": "قوللايدىغان فورمات: JPG, PNG, WEBP", "selectDeck": "دېك تاللاش", - "chooseDeck": "ئاستىرىلغان جۈپلەرنى ساقلاش ئۈچۈن دېك تاللاڭ", - "noDecks": "دېك يوق. ئاۋۋال دېك قۇرۇڭ.", - "languageHints": "تىل ئۇچۇرلىرى (ئىختىيارىي)", - "sourceLanguageHint": "مەنبە تىلى (مىسال: ئىنگىلىزچە)", - "targetLanguageHint": "نىشان/تەرجىمە تىلى (مىسال: خەنزۇچە)", - "process": "سۈرەتنى بىر تەرەپ قىلىش", - "processing": "بىر تەرەپ قىلىۋاتىدۇ...", + "chooseDeck": "دېك تاللاڭ", + "noDecks": "دېك يوق", + "languageHints": "تىل بېشارىتى", + "sourceLanguageHint": "مەنبە تىلى", + "targetLanguageHint": "نىشان تىلى", + "process": "بىر تەرەپ قىلىش", + "processing": "بىر تەرەپ قىلىنىۋاتىدۇ...", "preview": "ئالدىن كۆرۈش", - "extractedPairs": "ئاستىرىلغان جۈپلەر", + "extractedPairs": "ئېلىنغان جۈپلەر", "word": "سۆز", - "definition": "مەنا", - "pairsCount": "{count} جۈپ ئاستىرىلدى", - "savePairs": "دېككە ساقلاش", + "definition": "ئېنىقلىما", + "pairsCount": "{count} جۈپ", + "savePairs": "ساقلاش", "saving": "ساقلاۋاتىدۇ...", - "saved": "{deck} غا {count} جۈپ ساقلاندى", + "saved": "ساقلاندى", "saveFailed": "ساقلاش مەغلۇپ بولدى", - "noImage": "ئاۋۋال سۈرەت يۈكلەڭ", + "noImage": "رەسىم يۈكلەڭ", "noDeck": "دېك تاللاڭ", - "processingFailed": "OCR بىر تەرەپ قىلىش مەغلۇپ بولدى", - "tryAgain": "تېخىمۇ ئېنىق سۈرەت بىلەن قايتا سىناڭ", - "detectedLanguages": "بايقالدى: {source} → {target}" + "processingFailed": "بىر تەرەپ قىلىش مەغلۇپ بولدى", + "tryAgain": "قايتا سىناڭ", + "detectedLanguages": "تونۇلغان تىللار", + "uploadSection": "رەسىم يۈكلەش", + "dropOrClick": "تاشلاش ياكى چېكىش", + "changeImage": "رەسىم ئالماشتۇرۇش", + "invalidFileType": "ئىناۋەتسىز فايىل تىپى", + "deckSelection": "دېك تاللاش", + "sourceLanguagePlaceholder": "مەسىلەن: ئىنگلىزچە", + "targetLanguagePlaceholder": "مەسىلەن: ئۇيغۇرچە", + "processButton": "تونۇشنى باشلاش", + "resultsPreview": "نەتىجە ئالدىن كۆرۈش", + "saveButton": "دېككە ساقلاش", + "ocrSuccess": "OCR مۇۋەپپەقىيەتلىك", + "ocrFailed": "OCR مەغلۇپ بولدى", + "savedToDeck": "دېككە ساقلاندى", + "noResultsToSave": "نەتىجە يوق", + "detectedSourceLanguage": "تونۇلغان مەنبە تىلى", + "detectedTargetLanguage": "تونۇلغان نىشان تىلى" }, "profile": { "myProfile": "شەخسىي ئۇچۇرۇم", @@ -364,12 +423,43 @@ "videoUploadFailed": "ۋىدېئو يۈكلەش مەغلۇپ بولدى", "subtitleUploadFailed": "تر پودكاست يۈكلەش مەغلۇپ بولدى", "subtitleLoadSuccess": "تر پودكاست مۇۋەپپەقىيەتلىك يۈكلەندى", - "subtitleLoadFailed": "تر پودكاست يۈكلەش مەغلۇپ بولدى" + "subtitleLoadFailed": "تر پودكاست يۈكلەش مەغلۇپ بولدى", + "settings": "تەڭشەكلەر", + "shortcuts": "تېزلەتمەلەر", + "keyboardShortcuts": "كۇنۇپكا تاختىسى تېزلەتمەلىرى", + "playPause": "قويۇش/توختىتىش", + "autoPauseToggle": "ئاپتوماتىك توختىتىش", + "subtitleSettings": "ئاستى سىزىق تەڭشەكلىرى", + "fontSize": "خەت چوڭلۇقى", + "textColor": "خەت رەڭگى", + "backgroundColor": "تەگلىك رەڭگى", + "position": "ئورنى", + "opacity": "سۈزۈكلۈك", + "top": "ئۈستى", + "center": "ئوتتۇرا", + "bottom": "ئاستى" }, "text_speaker": { "generateIPA": "IPA ھاسىل قىلىش", "viewSavedItems": "ساقلانغان تۈرلەرنى كۆرۈش", - "confirmDeleteAll": "ھەممىنى ئۆچۈرەمسىز؟ (Y/N)" + "confirmDeleteAll": "ھەممىنى ئۆچۈرەمسىز؟ (Y/N)", + "saved": "ساقلاندى", + "clearAll": "ھەممىنى تازىلاش", + "language": "تىل", + "customLanguage": "ياكى تىل كىرگۈزۈڭ...", + "languages": { + "auto": "ئاپتوماتىك", + "chinese": "خەنزۇچە", + "english": "ئىنگلىزچە", + "japanese": "ياپونچە", + "korean": "كورېيەچە", + "french": "فرانسۇزچە", + "german": "گېرمانچە", + "italian": "ئىتاليانچە", + "spanish": "ئىسپانچە", + "portuguese": "پورتۇگالچە", + "russian": "رۇسچە" + } }, "translator": { "detectLanguage": "تىلنى تونۇش", @@ -403,7 +493,19 @@ "error": "تېكىست جۈپىنى قىسقۇچقا قوشۇش مەغلۇپ بولدى" }, "autoSave": "ئاپتوماتىك ساقلاش", - "customLanguage": "ياكى تىل تىل كىرۇڭ..." + "customLanguage": "ياكى تىل تىل كىرۇڭ...", + "pleaseLogin": "كارتا ساقلاش ئۈچۈن كىرىڭ", + "pleaseCreateDeck": "ئاۋۋال دېك قۇرۇڭ", + "noTranslationToSave": "ساقلايدىغان تەرجىمە يوق", + "noDeckSelected": "دېك تاللانمىدى", + "saveAsCard": "كارتا ساقلاش", + "selectDeck": "دېك تاللاش", + "front": "ئالدى", + "back": "كەينى", + "cancel": "بىكار قىلىش", + "save": "ساقلاش", + "savedToDeck": "{deckName} غا ساقلاندى", + "saveFailed": "ساقلاش مەغلۇپ" }, "dictionary": { "title": "لۇغەت", @@ -448,7 +550,9 @@ "unfavorite": "يىغىپ ساقلاشنى بىكار قىل", "pleaseLogin": "ئاۋۋال تىزىمغا كىرىڭ", "sortByFavorites": "يىغىپ ساقلاش بويىچە تەرتىپلەش", - "sortByFavoritesActive": "يىغىپ ساقلاش بويىچە تەرتىپلەشنى بىكار قىلىش" + "sortByFavoritesActive": "يىغىپ ساقلاش بويىچە تەرتىپلەشنى بىكار قىلىش", + "noDecks": "ئاممىۋىي دېك يوق", + "deckInfo": "{userName} · {totalCards} كارتا" }, "exploreDetail": { "title": "قىسقۇچ تەپسىلاتلىرى", @@ -462,7 +566,8 @@ "unfavorite": "يىغىپ ساقلاشنى بىكار قىل", "favorited": "يىغىپ ساقلاندى", "unfavorited": "يىغىپ ساقلاش بىكار قىلىندى", - "pleaseLogin": "ئاۋۋال تىزىمغا كىرىڭ" + "pleaseLogin": "ئاۋۋال تىزىمغا كىرىڭ", + "totalCards": "{count} كارتا" }, "favorites": { "title": "يىغىپ ساقلىغانلىرىم", @@ -507,7 +612,8 @@ "createdAt": "قۇرۇلغان ۋاقتى", "actions": "مەشغۇلاتلار", "view": "كۆرۈش" - } + }, + "joined": "قوشۇلدى" }, "follow": { "follow": "ئەگىشىش", diff --git a/messages/zh-CN.json b/messages/zh-CN.json index 9a45ae4..3dc853e 100644 --- a/messages/zh-CN.json +++ b/messages/zh-CN.json @@ -98,28 +98,47 @@ "delete": "删除", "permissionDenied": "您没有权限执行此操作", "resetProgress": "重置进度", - "resetProgressTitle": "重置牌组进度", - "resetProgressConfirm": "这将把牌组内所有卡片重置为新卡片状态。您的学习进度将会丢失。确定要继续吗?", - "resetSuccess": "成功重置 {count} 张卡片", + "resetProgressTitle": "重置学习进度", + "resetProgressConfirm": "确定要重置这个卡组的学习进度吗?", + "resetSuccess": "进度已重置", "resetting": "重置中...", "cancel": "取消", "settings": "设置", - "settingsTitle": "牌组设置", - "newPerDay": "每日新卡数量", - "newPerDayHint": "每天最多学习的新卡片数量", - "revPerDay": "每日复习数量", - "revPerDayHint": "每天最多复习的卡片数量", + "settingsTitle": "卡组设置", + "newPerDay": "每日新卡", + "newPerDayHint": "每天学习的新卡片数量", + "revPerDay": "每日复习", + "revPerDayHint": "每天复习的卡片数量", "save": "保存", "saving": "保存中...", "settingsSaved": "设置已保存", - "todayNew": "新卡", - "todayReview": "复习", + "todayNew": "今日新卡", + "todayReview": "今日复习", "todayLearning": "学习中", "error": { "update": "您没有权限更新此卡片", "delete": "您没有权限删除此卡片", "add": "您没有权限向此牌组添加卡片" - } + }, + "ipaPlaceholder": "输入IPA音标", + "examplePlaceholder": "输入例句", + "wordRequired": "请输入单词", + "definitionRequired": "请输入至少一个释义", + "cardAdded": "卡片已添加", + "cardType": "卡片类型", + "wordCard": "单词卡", + "phraseCard": "短语卡", + "sentenceCard": "句子卡", + "sentence": "句子", + "sentencePlaceholder": "输入句子", + "wordPlaceholder": "输入单词", + "queryLang": "查询语言", + "meanings": "释义", + "addMeaning": "添加释义", + "partOfSpeech": "词性", + "deleteConfirm": "确定删除这张卡片吗?", + "cardDeleted": "卡片已删除", + "cardUpdated": "卡片已更新" }, "home": { "title": "学语言", @@ -271,7 +290,8 @@ "yourAnswer": "你的答案", "typeWhatYouHear": "输入你听到的内容", "correct": "正确", - "incorrect": "错误" + "incorrect": "错误", + "nextCard": "下一张" }, "page": { "unauthorized": "您无权访问该牌组" @@ -288,48 +308,49 @@ "settings": "设置" }, "ocr": { - "title": "OCR 词汇提取", - "description": "上传教材词汇表截图,提取单词-释义对", + "title": "OCR文字识别", + "description": "从图片中提取文字创建学习卡片", "uploadSection": "上传图片", "uploadImage": "上传图片", - "dragDropHint": "拖放图片到此处,或点击选择", - "dropOrClick": "拖放图片到此处,或点击选择", - "changeImage": "点击更换图片", - "supportedFormats": "支持格式:JPG、PNG、WebP", - "invalidFileType": "无效的文件类型,请上传图片文件(JPG、PNG 或 WebP)", - "deckSelection": "选择牌组", - "selectDeck": "选择牌组", - "chooseDeck": "选择保存提取词汇的牌组", - "noDecks": "暂无牌组,请先创建牌组", - "languageHints": "语言提示(可选)", - "sourceLanguageHint": "源语言(如:英语)", - "targetLanguageHint": "目标/翻译语言(如:中文)", - "sourceLanguagePlaceholder": "源语言(如:英语)", - "targetLanguagePlaceholder": "目标/翻译语言(如:中文)", - "process": "处理图片", - "processButton": "处理图片", + "dragDropHint": "拖放或点击上传", + "dropOrClick": "拖放或点击", + "changeImage": "更换图片", + "supportedFormats": "支持格式:JPG, PNG, WEBP", + "invalidFileType": "无效的文件类型", + "deckSelection": "选择卡组", + "selectDeck": "选择卡组", + "chooseDeck": "选择卡组保存", + "noDecks": "没有可用的卡组", + "languageHints": "语言提示", + "sourceLanguageHint": "源语言提示", + "targetLanguageHint": "目标语言提示", + "sourceLanguagePlaceholder": "如:英语", + "targetLanguagePlaceholder": "如:中文", + "process": "处理", + "processButton": "开始识别", "processing": "处理中...", "preview": "预览", "resultsPreview": "结果预览", - "extractedPairs": "已提取 {count} 个词汇对", + "extractedPairs": "提取的语言对", "word": "单词", "definition": "释义", - "pairsCount": "{count} 个词汇对", - "savePairs": "保存到牌组", - "saveButton": "保存", + "pairsCount": "{count}对", + "savePairs": "保存", + "saveButton": "保存到卡组", "saving": "保存中...", - "saved": "成功将 {count} 个词汇对保存到 {deck}", - "ocrSuccess": "成功将 {count} 个词汇对保存到 {deck}", - "savedToDeck": "已保存到 {deckName}", + "saved": "已保存", + "ocrSuccess": "OCR识别成功", + "savedToDeck": "已保存到卡组", "saveFailed": "保存失败", - "noImage": "请先上传图片", - "noDeck": "请选择牌组", - "noResultsToSave": "没有可保存的结果", - "processingFailed": "OCR 处理失败", - "tryAgain": "请尝试上传更清晰的图片", - "detectedLanguages": "检测到:{source} → {target}", + "noImage": "请上传图片", + "noDeck": "请选择卡组", + "noResultsToSave": "无结果可保存", + "processingFailed": "处理失败", + "tryAgain": "重试", + "detectedLanguages": "检测到的语言", "detectedSourceLanguage": "检测到的源语言", - "detectedTargetLanguage": "检测到的目标语言" + "detectedTargetLanguage": "检测到的目标语言", + "ocrFailed": "OCR识别失败" }, "profile": { "myProfile": "我的个人资料", @@ -375,13 +396,13 @@ "shortcuts": "快捷键", "keyboardShortcuts": "键盘快捷键", "playPause": "播放/暂停", - "autoPauseToggle": "切换自动暂停", + "autoPauseToggle": "自动暂停开关", "subtitleSettings": "字幕设置", "fontSize": "字体大小", "textColor": "文字颜色", "backgroundColor": "背景颜色", "position": "位置", - "opacity": "不透明度", + "opacity": "透明度", "top": "顶部", "center": "居中", "bottom": "底部" @@ -391,7 +412,22 @@ "viewSavedItems": "查看保存项", "confirmDeleteAll": "确定删光吗?(Y/N)", "saved": "已保存", - "clearAll": "清空全部" + "clearAll": "清空全部", + "language": "语言", + "customLanguage": "或输入语言...", + "languages": { + "auto": "自动", + "chinese": "中文", + "english": "英语", + "japanese": "日语", + "korean": "韩语", + "french": "法语", + "german": "德语", + "italian": "意大利语", + "spanish": "西班牙语", + "portuguese": "葡萄牙语", + "russian": "俄语" + } }, "translator": { "detectLanguage": "检测语言", @@ -425,7 +461,19 @@ "error": "添加文本对到文件夹失败" }, "autoSave": "自动保存", - "customLanguage": "或输入语言..." + "customLanguage": "或输入语言...", + "pleaseLogin": "请登录后保存卡片", + "pleaseCreateDeck": "请先创建卡组", + "noTranslationToSave": "没有可保存的翻译", + "noDeckSelected": "未选择卡组", + "saveAsCard": "保存为卡片", + "selectDeck": "选择卡组", + "front": "正面", + "back": "背面", + "cancel": "取消", + "save": "保存", + "savedToDeck": "已保存到 {deckName}", + "saveFailed": "保存失败" }, "dictionary": { "title": "词典", @@ -463,8 +511,8 @@ "subtitle": "发现公开牌组", "searchPlaceholder": "搜索公开牌组...", "loading": "加载中...", - "noDecks": "没有找到公开牌组", - "deckInfo": "{userName} • {cardCount} 张卡片", + "noDecks": "暂无公开卡组", + "deckInfo": "{userName} · {totalCards} 张", "unknownUser": "未知用户", "favorite": "收藏", "unfavorite": "取消收藏", @@ -476,7 +524,7 @@ "title": "牌组详情", "createdBy": "创建者:{name}", "unknownUser": "未知用户", - "totalCards": "卡片数量", + "totalCards": "共 {count} 张", "favorites": "收藏数", "createdAt": "创建时间", "viewContent": "查看内容", @@ -505,7 +553,7 @@ "displayName": "显示名称", "notSet": "未设置", "memberSince": "注册时间", - "joined": "加入于", + "joined": "注册于", "logout": "登出", "deleteAccount": { "button": "注销账号", @@ -534,30 +582,30 @@ }, "decks": { "title": "牌组", - "subtitle": "管理您的闪卡牌组", - "newDeck": "新建牌组", - "noDecksYet": "还没有牌组", + "subtitle": "管理你的学习卡组", + "newDeck": "新建卡组", + "noDecksYet": "暂无卡组", "loading": "加载中...", - "deckInfo": "ID: {id} • {totalCards} 张卡片", - "enterDeckName": "输入牌组名称:", + "deckInfo": "ID: {id} · {totalCards} 张", + "enterDeckName": "输入卡组名称:", "enterNewName": "输入新名称:", - "confirmDelete": "输入 \"{name}\" 以删除:", + "confirmDelete": "输入 \"{name}\" 确认删除:", "public": "公开", "private": "私有", "setPublic": "设为公开", "setPrivate": "设为私有", "importApkg": "导入 APKG", "exportApkg": "导出 APKG", - "clickToUpload": "点击上传 APKG 文件", + "clickToUpload": "点击上传", "apkgFilesOnly": "仅支持 .apkg 文件", "parsing": "解析中...", - "foundDecks": "找到 {count} 个牌组", - "deckName": "牌组名称", + "foundDecks": "发现 {count} 个卡组", + "deckName": "卡组名称", "back": "返回", "import": "导入", "importing": "导入中...", - "exportSuccess": "牌组导出成功", - "goToDecks": "前往牌组" + "exportSuccess": "导出成功", + "goToDecks": "前往卡组" }, "follow": { "follow": "关注", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 92b163b..e2951c0 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -12,27 +12,22 @@ datasource db { // ============================================ model User { - id String @id - name String - email String @unique - emailVerified Boolean @default(false) - image String? - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - displayUsername String? - username String @unique - bio String? - accounts Account[] - dictionaryLookUps DictionaryLookUp[] - // Anki-compatible relations - decks Deck[] - deckFavorites DeckFavorite[] - noteTypes NoteType[] - notes Note[] - sessions Session[] - translationHistories TranslationHistory[] - followers Follow[] @relation("UserFollowers") - following Follow[] @relation("UserFollowing") + id String @id + name String + email String @unique + emailVerified Boolean @default(false) + image String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + displayUsername String? + username String @unique + bio String? + accounts Account[] + decks Deck[] + deckFavorites DeckFavorite[] + sessions Session[] + followers Follow[] @relation("UserFollowers") + following Follow[] @relation("UserFollowing") @@map("user") } @@ -85,90 +80,75 @@ model Verification { } // ============================================ -// Anki-compatible Models +// Deck & Card // ============================================ -/// Card type: 0=new, 1=learning, 2=review, 3=relearning -enum CardType { - NEW - LEARNING - REVIEW - RELEARNING -} - -/// Card queue: -3=user buried, -2=sched buried, -1=suspended, 0=new, 1=learning, 2=review, 3=in learning, 4=preview -enum CardQueue { - USER_BURIED - SCHED_BURIED - SUSPENDED - NEW - LEARNING - REVIEW - IN_LEARNING - PREVIEW -} - -/// Note type: 0=standard, 1=cloze -enum NoteKind { - STANDARD - CLOZE -} - -/// Deck visibility (our extension, not in Anki) enum Visibility { - PRIVATE PUBLIC + PRIVATE } -/// NoteType (Anki: models) - Defines fields and templates for notes -model NoteType { - id Int @id @default(autoincrement()) - name String - kind NoteKind @default(STANDARD) - css String @default("") - fields Json @default("[]") - templates Json @default("[]") - userId String @map("user_id") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - notes Note[] - - @@index([userId]) - @@map("note_types") +enum CardType { + WORD + PHRASE + SENTENCE } -/// Deck (Anki: decks) - Container for cards model Deck { - id Int @id @default(autoincrement()) - name String - desc String @default("") - userId String @map("user_id") - visibility Visibility @default(PRIVATE) - collapsed Boolean @default(false) - conf Json @default("{}") - newPerDay Int @default(20) @map("new_per_day") - revPerDay Int @default(200) @map("rev_per_day") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - cards Card[] - favorites DeckFavorite[] + id Int @id @default(autoincrement()) + name String + desc String @db.Text @default("") + userId String + visibility Visibility @default(PRIVATE) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + cards Card[] + favorites DeckFavorite[] @@index([userId]) @@index([visibility]) @@map("decks") } -/// DeckFavorite - Users can favorite public decks +model Card { + id Int @id @default(autoincrement()) + deckId Int + word String + ipa String? + queryLang String + cardType CardType @default(WORD) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deck Deck @relation(fields: [deckId], references: [id], onDelete: Cascade) + meanings CardMeaning[] + + @@index([deckId]) + @@index([word]) + @@map("cards") +} + +model CardMeaning { + id Int @id @default(autoincrement()) + cardId Int + partOfSpeech String? + definition String + example String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + card Card @relation(fields: [cardId], references: [id], onDelete: Cascade) + + @@index([cardId]) + @@map("card_meanings") +} + model DeckFavorite { id Int @id @default(autoincrement()) - userId String @map("user_id") - deckId Int @map("deck_id") - createdAt DateTime @default(now()) @map("created_at") - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - deck Deck @relation(fields: [deckId], references: [id], onDelete: Cascade) + userId String + deckId Int + createdAt DateTime @default(now()) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + deck Deck @relation(fields: [deckId], references: [id], onDelete: Cascade) @@unique([userId, deckId]) @@index([userId]) @@ -176,156 +156,10 @@ model DeckFavorite { @@map("deck_favorites") } -/// Note (Anki: notes) - Contains field data, one note can have multiple cards -model Note { - id BigInt @id - guid String @unique - noteTypeId Int @map("note_type_id") - mod Int - usn Int @default(-1) - tags String @default(" ") - flds String - sfld String - csum Int - flags Int @default(0) - data String @default("") - userId String @map("user_id") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - noteType NoteType @relation(fields: [noteTypeId], references: [id], onDelete: Cascade) - cards Card[] - - @@index([userId]) - @@index([noteTypeId]) - @@index([csum]) - @@map("notes") -} - -/// Card (Anki: cards) - Scheduling information, what you review -model Card { - id BigInt @id - noteId BigInt @map("note_id") - deckId Int @map("deck_id") - ord Int - mod Int - usn Int @default(-1) - type CardType @default(NEW) - queue CardQueue @default(NEW) - due Int - ivl Int @default(0) - factor Int @default(2500) - reps Int @default(0) - lapses Int @default(0) - left Int @default(0) - odue Int @default(0) - odid Int @default(0) - flags Int @default(0) - data String @default("") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - note Note @relation(fields: [noteId], references: [id], onDelete: Cascade) - deck Deck @relation(fields: [deckId], references: [id], onDelete: Cascade) - revlogs Revlog[] - - @@index([noteId]) - @@index([deckId]) - @@index([deckId, queue, due]) - @@map("cards") -} - -/// Revlog (Anki: revlog) - Review history -model Revlog { - id BigInt @id - cardId BigInt @map("card_id") - usn Int @default(-1) - ease Int - ivl Int - lastIvl Int - factor Int - time Int - type Int - card Card @relation(fields: [cardId], references: [id], onDelete: Cascade) - - @@index([cardId]) - @@map("revlogs") -} - // ============================================ -// Other Models +// Social // ============================================ -model DictionaryLookUp { - id Int @id @default(autoincrement()) - userId String? @map("user_id") - text String - queryLang String @map("query_lang") - definitionLang String @map("definition_lang") - createdAt DateTime @default(now()) @map("created_at") - dictionaryItemId Int? @map("dictionary_item_id") - normalizedText String @default("") @map("normalized_text") - dictionaryItem DictionaryItem? @relation(fields: [dictionaryItemId], references: [id], onDelete: SetNull) - user User? @relation(fields: [userId], references: [id], onDelete: SetNull) - - @@index([userId]) - @@index([createdAt]) - @@index([normalizedText]) - @@map("dictionary_lookups") -} - -model DictionaryItem { - id Int @id @default(autoincrement()) - frequency Int @default(1) - standardForm String @map("standard_form") - queryLang String @map("query_lang") - definitionLang String @map("definition_lang") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - entries DictionaryEntry[] - lookups DictionaryLookUp[] - - @@unique([standardForm, queryLang, definitionLang]) - @@index([standardForm]) - @@index([queryLang, definitionLang]) - @@map("dictionary_items") -} - -model DictionaryEntry { - id Int @id @default(autoincrement()) - itemId Int @map("item_id") - ipa String? - definition String - partOfSpeech String? @map("part_of_speech") - example String - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - item DictionaryItem @relation(fields: [itemId], references: [id], onDelete: Cascade) - - @@index([itemId]) - @@index([createdAt]) - @@map("dictionary_entries") -} - -model TranslationHistory { - id Int @id @default(autoincrement()) - userId String? @map("user_id") - sourceText String @map("source_text") - sourceLanguage String @map("source_language") - targetLanguage String @map("target_language") - translatedText String @map("translated_text") - sourceIpa String? @map("source_ipa") - targetIpa String? @map("target_ipa") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") - user User? @relation(fields: [userId], references: [id]) - - @@index([userId]) - @@index([createdAt]) - @@index([sourceText, targetLanguage]) - @@index([translatedText, sourceLanguage, targetLanguage]) - @@map("translation_history") -} - model Follow { id String @id @default(cuid()) followerId String @map("follower_id") diff --git a/src/app/(features)/dictionary/DictionaryClient.tsx b/src/app/(features)/dictionary/DictionaryClient.tsx index f8ad026..5ec0ea6 100644 --- a/src/app/(features)/dictionary/DictionaryClient.tsx +++ b/src/app/(features)/dictionary/DictionaryClient.tsx @@ -15,14 +15,14 @@ import { DictionaryEntry } from "./DictionaryEntry"; import { LanguageSelector } from "./LanguageSelector"; import { authClient } from "@/lib/auth-client"; import { actionGetDecksByUserId } from "@/modules/deck/deck-action"; -import { actionCreateNote } from "@/modules/note/note-action"; import { actionCreateCard } from "@/modules/card/card-action"; -import { actionGetNoteTypesByUserId, actionCreateDefaultBasicNoteType } from "@/modules/note-type/note-type-action"; -import type { TSharedDeck } from "@/shared/anki-type"; +import type { ActionOutputDeck } from "@/modules/deck/deck-action-dto"; +import type { CardType } from "@/modules/card/card-action-dto"; import { toast } from "sonner"; +import { getNativeName } from "./stores/dictionaryStore"; interface DictionaryClientProps { - initialDecks: TSharedDeck[]; + initialDecks: ActionOutputDeck[]; } export function DictionaryClient({ initialDecks }: DictionaryClientProps) { @@ -45,8 +45,7 @@ export function DictionaryClient({ initialDecks }: DictionaryClientProps) { } = useDictionaryStore(); const { data: session } = authClient.useSession(); - const [decks, setDecks] = useState(initialDecks); - const [defaultNoteTypeId, setDefaultNoteTypeId] = useState(null); + const [decks, setDecks] = useState(initialDecks); const [isSaving, setIsSaving] = useState(false); useEffect(() => { @@ -65,29 +64,7 @@ export function DictionaryClient({ initialDecks }: DictionaryClientProps) { if (session?.user?.id) { actionGetDecksByUserId(session.user.id).then((result) => { if (result.success && result.data) { - setDecks(result.data as TSharedDeck[]); - } - }); - } - }, [session?.user?.id]); - - useEffect(() => { - if (session?.user?.id) { - actionGetNoteTypesByUserId().then(async (result) => { - if (result.success && result.data) { - const basicNoteType = result.data.find( - (nt) => nt.name === "Basic Vocabulary" - ); - if (basicNoteType) { - setDefaultNoteTypeId(basicNoteType.id); - } else if (result.data.length > 0) { - setDefaultNoteTypeId(result.data[0].id); - } else { - const createResult = await actionCreateDefaultBasicNoteType(); - if (createResult.success && createResult.data) { - setDefaultNoteTypeId(createResult.data.id); - } - } + setDecks(result.data); } }); } @@ -116,11 +93,10 @@ export function DictionaryClient({ initialDecks }: DictionaryClientProps) { toast.error(t("pleaseCreateFolder")); return; } - if (!defaultNoteTypeId) { - toast.error("No note type available. Please try again."); + if (!searchResult?.entries?.length) { + toast.error("No dictionary item to save. Please search first."); return; } - if (!searchResult?.entries?.length) return; const deckSelect = document.getElementById("deck-select") as HTMLSelectElement; const deckId = deckSelect?.value ? Number(deckSelect.value) : decks[0]?.id; @@ -132,43 +108,38 @@ export function DictionaryClient({ initialDecks }: DictionaryClientProps) { setIsSaving(true); - const definition = searchResult.entries - .map((e) => e.definition) - .join(" | "); - - const ipa = searchResult.entries[0]?.ipa || ""; - const example = searchResult.entries - .map((e) => e.example) - .filter(Boolean) - .join(" | ") || ""; - try { - const noteResult = await actionCreateNote({ - noteTypeId: defaultNoteTypeId, - fields: [searchResult.standardForm, definition, ipa, example], - tags: ["dictionary"], + const hasIpa = searchResult.entries.some((e) => e.ipa); + const hasSpaces = searchResult.standardForm.includes(" "); + let cardType: CardType = "WORD"; + if (!hasIpa) { + cardType = "SENTENCE"; + } else if (hasSpaces) { + cardType = "PHRASE"; + } + + const ipa = searchResult.entries.find((e) => e.ipa)?.ipa || null; + const meanings = searchResult.entries.map((e) => ({ + partOfSpeech: e.partOfSpeech || null, + definition: e.definition, + example: e.example || null, + })); + + const cardResult = await actionCreateCard({ + deckId, + word: searchResult.standardForm, + ipa, + queryLang: getNativeName(queryLang), + cardType, + meanings, }); - if (!noteResult.success || !noteResult.data) { - toast.error(t("saveFailed")); + if (!cardResult.success) { + toast.error(cardResult.message || t("saveFailed")); setIsSaving(false); return; } - const noteId = BigInt(noteResult.data.id); - - await actionCreateCard({ - noteId, - deckId, - ord: 0, - }); - - await actionCreateCard({ - noteId, - deckId, - ord: 1, - }); - const deckName = decks.find((d) => d.id === deckId)?.name || "Unknown"; toast.success(t("savedToFolder", { folderName: deckName })); } catch (error) { diff --git a/src/app/(features)/dictionary/page.tsx b/src/app/(features)/dictionary/page.tsx index ba26ec6..b6eb5a4 100644 --- a/src/app/(features)/dictionary/page.tsx +++ b/src/app/(features)/dictionary/page.tsx @@ -2,17 +2,17 @@ import { DictionaryClient } from "./DictionaryClient"; import { auth } from "@/auth"; import { headers } from "next/headers"; import { actionGetDecksByUserId } from "@/modules/deck/deck-action"; -import type { TSharedDeck } from "@/shared/anki-type"; +import type { ActionOutputDeck } from "@/modules/deck/deck-action-dto"; export default async function DictionaryPage() { const session = await auth.api.getSession({ headers: await headers() }); - let decks: TSharedDeck[] = []; + let decks: ActionOutputDeck[] = []; if (session?.user?.id) { const result = await actionGetDecksByUserId(session.user.id as string); if (result.success && result.data) { - decks = result.data as TSharedDeck[]; + decks = result.data; } } diff --git a/src/app/(features)/ocr/OCRClient.tsx b/src/app/(features)/ocr/OCRClient.tsx deleted file mode 100644 index 5fbccef..0000000 --- a/src/app/(features)/ocr/OCRClient.tsx +++ /dev/null @@ -1,286 +0,0 @@ -"use client"; - -import { useState, useCallback, useRef } from "react"; -import { useTranslations } from "next-intl"; -import { PageLayout } from "@/components/ui/PageLayout"; -import { PrimaryButton, LightButton } from "@/design-system/base/button"; -import { Input } from "@/design-system/base/input"; -import { Select } from "@/design-system/base/select"; -import { Card } from "@/design-system/base/card"; -import { toast } from "sonner"; -import { Upload, FileImage, Loader2 } from "lucide-react"; -import { actionProcessOCR } from "@/modules/ocr/ocr-action"; -import type { ActionOutputDeck } from "@/modules/deck/deck-action-dto"; - -interface ActionOutputProcessOCR { - success: boolean; - message: string; - data?: { - pairsCreated: number; - sourceLanguage?: string; - targetLanguage?: string; - }; -} - -interface OCRClientProps { - initialDecks: ActionOutputDeck[]; -} - -export function OCRClient({ initialDecks }: OCRClientProps) { - const t = useTranslations("ocr"); - const fileInputRef = useRef(null); - const [decks, setDecks] = useState(initialDecks); - - const [selectedFile, setSelectedFile] = useState(null); - const [previewUrl, setPreviewUrl] = useState(null); - const [selectedDeckId, setSelectedDeckId] = useState( - initialDecks.length > 0 ? initialDecks[0].id : null - ); - const [sourceLanguage, setSourceLanguage] = useState(""); - const [targetLanguage, setTargetLanguage] = useState(""); - const [isProcessing, setIsProcessing] = useState(false); - const [ocrResult, setOcrResult] = useState(null); - - const handleFileChange = useCallback((file: File | null) => { - if (!file) return; - - if (!file.type.startsWith("image/")) { - toast.error(t("invalidFileType")); - return; - } - - const url = URL.createObjectURL(file); - setPreviewUrl(url); - setSelectedFile(file); - setOcrResult(null); - }, [t]); - - const handleDrop = useCallback((e: React.DragEvent) => { - e.preventDefault(); - const file = e.dataTransfer.files[0]; - handleFileChange(file); - }, [handleFileChange]); - - const handleDragOver = useCallback((e: React.DragEvent) => { - e.preventDefault(); - }, []); - - const fileToBase64 = async (file: File): Promise => { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => { - const result = reader.result as string; - const base64 = result.split(",")[1]; - resolve(base64); - }; - reader.onerror = reject; - reader.readAsDataURL(file); - }); - }; - - const handleProcess = async () => { - if (!selectedFile) { - toast.error(t("noImage")); - return; - } - - if (!selectedDeckId) { - toast.error(t("noDeck")); - return; - } - - setIsProcessing(true); - setOcrResult(null); - - try { - const base64 = await fileToBase64(selectedFile); - - const result = await actionProcessOCR({ - imageBase64: base64, - deckId: selectedDeckId, - sourceLanguage: sourceLanguage || undefined, - targetLanguage: targetLanguage || undefined, - }); - - if (result.success && result.data) { - setOcrResult(result); - const deckName = decks.find(d => d.id === selectedDeckId)?.name || ""; - toast.success(t("ocrSuccess", { count: result.data.pairsCreated, deck: deckName })); - } else { - toast.error(result.message || t("ocrFailed")); - } - } catch { - toast.error(t("processingFailed")); - } finally { - setIsProcessing(false); - } - }; - - const handleSave = async () => { - if (!ocrResult || !selectedDeckId) { - toast.error(t("noResultsToSave")); - return; - } - - try { - const deckName = decks.find(d => d.id === selectedDeckId)?.name || "Unknown"; - toast.success(t("savedToDeck", { deckName })); - } catch (error) { - toast.error(t("saveFailed")); - } - }; - - const clearImage = () => { - if (previewUrl) { - URL.revokeObjectURL(previewUrl); - } - setPreviewUrl(null); - setSelectedFile(null); - setOcrResult(null); - if (fileInputRef.current) { - fileInputRef.current.value = ""; - } - }; - - return ( - -
-

- {t("title")} -

-

- {t("description")} -

-
- - -
- {/* Upload Section */} -
-

- {t("uploadSection")} -

-
fileInputRef.current?.click()} - className="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center cursor-pointer hover:border-primary-500 hover:bg-primary-50 transition-colors" - > - {previewUrl ? ( -
- Preview -

{t("changeImage")}

-
- ) : ( -
- -

{t("dropOrClick")}

-

{t("supportedFormats")}

-
- )} -
- handleFileChange(e.target.files?.[0] || null)} - className="hidden" - /> -
- - {/* Deck Selection */} -
-

- {t("deckSelection")} -

- -
- - {/* Language Hints */} -
-

- {t("languageHints")} -

-
- setSourceLanguage(e.target.value)} - className="w-full" - /> - setTargetLanguage(e.target.value)} - className="w-full" - /> -
-
- - {/* Process Button */} -
- - {t("processButton")} - -
- - {/* Results Preview */} - {ocrResult && ocrResult.data && ( -
-

- {t("resultsPreview")} -

-
-
-
-

{t("extractedPairs", { count: ocrResult.data.pairsCreated })}

-
-
- {ocrResult.data.sourceLanguage && ( -
- {t("detectedSourceLanguage")}: {ocrResult.data.sourceLanguage} -
- )} - {ocrResult.data.targetLanguage && ( -
- {t("detectedTargetLanguage")}: {ocrResult.data.targetLanguage} -
- )} -
- -
- - {t("saveButton")} - -
-
- )} -
-
-
- ); -} diff --git a/src/app/(features)/ocr/page.tsx b/src/app/(features)/ocr/page.tsx deleted file mode 100644 index 4ebd9ff..0000000 --- a/src/app/(features)/ocr/page.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { OCRClient } from "./OCRClient"; -import { auth } from "@/auth"; -import { headers } from "next/headers"; -import { actionGetDecksByUserId } from "@/modules/deck/deck-action"; -import type { ActionOutputDeck } from "@/modules/deck/deck-action-dto"; - -export default async function OCRPage() { - const session = await auth.api.getSession({ headers: await headers() }); - - let decks: ActionOutputDeck[] = []; - - if (session?.user?.id) { - const result = await actionGetDecksByUserId(session.user.id as string); - if (result.success && result.data) { - decks = result.data; - } - } - - return ; -} diff --git a/src/app/(features)/text-speaker/page.tsx b/src/app/(features)/text-speaker/page.tsx index e2a03ea..0e8a794 100644 --- a/src/app/(features)/text-speaker/page.tsx +++ b/src/app/(features)/text-speaker/page.tsx @@ -1,6 +1,7 @@ "use client"; import { LightButton, IconClick } from "@/design-system/base/button"; +import { Input } from "@/design-system/base/input"; import { Textarea } from "@/design-system/base/textarea"; import { IMAGES } from "@/config/images"; import { useAudioPlayer } from "@/hooks/useAudioPlayer"; @@ -18,6 +19,20 @@ import { genIPA, genLanguage } from "@/modules/translator/translator-action"; import { PageLayout } from "@/components/ui/PageLayout"; import { getTTSUrl, TTS_SUPPORTED_LANGUAGES } from "@/lib/bigmodel/tts"; +const TTS_LANGUAGES = [ + { value: "Auto", labelKey: "auto" }, + { value: "Chinese", labelKey: "chinese" }, + { value: "English", labelKey: "english" }, + { value: "Japanese", labelKey: "japanese" }, + { value: "Korean", labelKey: "korean" }, + { value: "French", labelKey: "french" }, + { value: "German", labelKey: "german" }, + { value: "Italian", labelKey: "italian" }, + { value: "Spanish", labelKey: "spanish" }, + { value: "Portuguese", labelKey: "portuguese" }, + { value: "Russian", labelKey: "russian" }, +] as const; + export default function TextSpeakerPage() { const t = useTranslations("text_speaker"); const textareaRef = useRef(null); @@ -30,6 +45,8 @@ export default function TextSpeakerPage() { const [autopause, setAutopause] = useState(true); const textRef = useRef(""); const [language, setLanguage] = useState(null); + const [selectedLanguage, setSelectedLanguage] = useState("Auto"); + const [customLanguage, setCustomLanguage] = useState(""); const [ipa, setIPA] = useState(""); const objurlRef = useRef(null); const [processing, setProcessing] = useState(false); @@ -93,8 +110,15 @@ export default function TextSpeakerPage() { } else { // 第一次播放 try { - let theLanguage = language; - if (!theLanguage) { + let theLanguage: string; + + if (customLanguage.trim()) { + theLanguage = customLanguage.trim(); + } else if (selectedLanguage !== "Auto") { + theLanguage = selectedLanguage; + } else if (language) { + theLanguage = language; + } else { const tmp_language = await genLanguage(textRef.current.slice(0, 30)); setLanguage(tmp_language); theLanguage = tmp_language; @@ -102,7 +126,6 @@ export default function TextSpeakerPage() { theLanguage = theLanguage.toLowerCase().replace(/[^a-z]/g, '').replace(/^./, match => match.toUpperCase()); - // 检查语言是否在 TTS 支持列表中 const supportedLanguages: TTS_SUPPORTED_LANGUAGES[] = [ "Auto", "Chinese", "English", "German", "Italian", "Portuguese", "Spanish", "Japanese", "Korean", "French", "Russian" @@ -138,6 +161,8 @@ export default function TextSpeakerPage() { const handleInputChange = (e: ChangeEvent) => { textRef.current = e.target.value.trim(); setLanguage(null); + setSelectedLanguage("Auto"); + setCustomLanguage(""); setIPA(""); if (objurlRef.current) URL.revokeObjectURL(objurlRef.current); objurlRef.current = null; @@ -318,6 +343,40 @@ export default function TextSpeakerPage() { alt="save" className={`${saving ? "bg-gray-200" : ""}`} > + {/* 语言选择器 */} +
+ {t("language")} + {TTS_LANGUAGES.slice(0, 6).map((lang) => ( + { + setSelectedLanguage(lang.value); + setCustomLanguage(""); + if (objurlRef.current) URL.revokeObjectURL(objurlRef.current); + objurlRef.current = null; + setPause(true); + }} + size="sm" + > + {t(`languages.${lang.labelKey}`)} + + ))} + { + setCustomLanguage(e.target.value); + setSelectedLanguage("Auto"); + if (objurlRef.current) URL.revokeObjectURL(objurlRef.current); + objurlRef.current = null; + setPause(true); + }} + placeholder={t("customLanguage")} + className="w-auto min-w-[120px]" + /> +
{/* 功能开关按钮 */}
(null); + const sourceContainerRef = useRef(null); + const targetContainerRef = useRef(null); const [sourceLanguage, setSourceLanguage] = useState("Auto"); const [targetLanguage, setTargetLanguage] = useState("Chinese"); + const [customSourceLanguage, setCustomSourceLanguage] = useState(""); const [customTargetLanguage, setCustomTargetLanguage] = useState(""); const [translationResult, setTranslationResult] = useState(null); const [needIpa, setNeedIpa] = useState(true); @@ -55,38 +70,72 @@ export default function TranslatorPage() { sourceLanguage: string; targetLanguage: string; } | null>(null); + const [sourceButtonCount, setSourceButtonCount] = useState(2); + const [targetButtonCount, setTargetButtonCount] = useState(2); const { load, play } = useAudioPlayer(); - const lastTTS = useRef({ - text: "", - url: "", - }); + + const { data: session } = authClient.useSession(); + const [decks, setDecks] = useState([]); + const [showSaveModal, setShowSaveModal] = useState(false); + const [isSaving, setIsSaving] = useState(false); - const tts = async (text: string, locale: string) => { - if (lastTTS.current.text !== text) { - try { - // Map language name to TTS format - let theLanguage = locale.toLowerCase().replace(/[^a-z]/g, '').replace(/^./, match => match.toUpperCase()); - - // Check if language is in TTS supported list - const supportedLanguages: TTS_SUPPORTED_LANGUAGES[] = [ - "Auto", "Chinese", "English", "German", "Italian", "Portuguese", - "Spanish", "Japanese", "Korean", "French", "Russian" - ]; - - if (!supportedLanguages.includes(theLanguage as TTS_SUPPORTED_LANGUAGES)) { - theLanguage = "Auto"; + useEffect(() => { + if (session?.user?.id) { + actionGetDecksByUserId(session.user.id).then((result) => { + if (result.success && result.data) { + setDecks(result.data); } - - const url = await getTTSUrl(text, theLanguage as TTS_SUPPORTED_LANGUAGES); - await load(url); - await play(); - lastTTS.current.text = text; - lastTTS.current.url = url; - } catch (error) { - toast.error("Failed to generate audio"); - } + }); } - }; + }, [session?.user?.id]); + + // Calculate how many buttons to show based on container width + const calculateButtonCount = useCallback((containerWidth: number, hasIpa: boolean) => { + // Reserve space for label, input, and IPA button (for source) + const reservedWidth = LABEL_WIDTH + INPUT_WIDTH + (hasIpa ? IPA_BUTTON_WIDTH : 0); + const availableWidth = containerWidth - reservedWidth; + return Math.max(0, Math.floor(availableWidth / BUTTON_WIDTH)); + }, []); + + useEffect(() => { + const updateButtonCounts = () => { + if (sourceContainerRef.current) { + const width = sourceContainerRef.current.offsetWidth; + setSourceButtonCount(calculateButtonCount(width, true)); + } + if (targetContainerRef.current) { + const width = targetContainerRef.current.offsetWidth; + setTargetButtonCount(calculateButtonCount(width, false)); + } + }; + + updateButtonCounts(); + window.addEventListener("resize", updateButtonCounts); + return () => window.removeEventListener("resize", updateButtonCounts); + }, [calculateButtonCount]); + + const tts = useCallback(async (text: string, locale: string) => { + try { + // Map language name to TTS format + let theLanguage = locale.toLowerCase().replace(/[^a-z]/g, '').replace(/^./, match => match.toUpperCase()); + + // Check if language is in TTS supported list + const supportedLanguages: TTS_SUPPORTED_LANGUAGES[] = [ + "Auto", "Chinese", "English", "German", "Italian", "Portuguese", + "Spanish", "Japanese", "Korean", "French", "Russian" + ]; + + if (!supportedLanguages.includes(theLanguage as TTS_SUPPORTED_LANGUAGES)) { + theLanguage = "Auto"; + } + + const url = await getTTSUrl(text, theLanguage as TTS_SUPPORTED_LANGUAGES); + await load(url); + await play(); + } catch (error) { + toast.error("Failed to generate audio"); + } + }, [load, play]); const translate = async () => { if (!taref.current || processing) return; @@ -94,12 +143,13 @@ export default function TranslatorPage() { setProcessing(true); const sourceText = taref.current.value; + const effectiveSourceLanguage = customSourceLanguage.trim() || sourceLanguage; const effectiveTargetLanguage = customTargetLanguage.trim() || targetLanguage; // 判断是否需要强制重新翻译 const forceRetranslate = lastTranslation?.sourceText === sourceText && - lastTranslation?.sourceLanguage === sourceLanguage && + lastTranslation?.sourceLanguage === effectiveSourceLanguage && lastTranslation?.targetLanguage === effectiveTargetLanguage; try { @@ -108,14 +158,14 @@ export default function TranslatorPage() { targetLanguage: effectiveTargetLanguage, forceRetranslate, needIpa, - sourceLanguage: sourceLanguage === "Auto" ? undefined : sourceLanguage, + sourceLanguage: effectiveSourceLanguage === "Auto" ? undefined : effectiveSourceLanguage, }); if (result.success && result.data) { setTranslationResult(result.data); setLastTranslation({ sourceText, - sourceLanguage, + sourceLanguage: effectiveSourceLanguage, targetLanguage: effectiveTargetLanguage, }); } else { @@ -129,6 +179,66 @@ export default function TranslatorPage() { } }; + const visibleSourceButtons = SOURCE_LANGUAGES.slice(0, sourceButtonCount); + const visibleTargetButtons = TARGET_LANGUAGES.slice(0, targetButtonCount); + + const handleSaveCard = async () => { + if (!session) { + toast.error(t("pleaseLogin")); + return; + } + if (decks.length === 0) { + toast.error(t("pleaseCreateDeck")); + return; + } + if (!lastTranslation?.sourceText || !translationResult?.translatedText) { + toast.error(t("noTranslationToSave")); + return; + } + + const deckSelect = document.getElementById("deck-select-translator") as HTMLSelectElement; + const deckId = deckSelect?.value ? Number(deckSelect.value) : decks[0]?.id; + + if (!deckId) { + toast.error(t("noDeckSelected")); + return; + } + + setIsSaving(true); + + try { + const sourceText = lastTranslation.sourceText; + const hasSpaces = sourceText.includes(" "); + let cardType: CardType = "WORD"; + if (!translationResult.sourceIpa) { + cardType = "SENTENCE"; + } else if (hasSpaces) { + cardType = "PHRASE"; + } + + await actionCreateCard({ + deckId, + word: sourceText, + ipa: translationResult.sourceIpa || null, + queryLang: lastTranslation.sourceLanguage, + cardType, + meanings: [{ + partOfSpeech: null, + definition: translationResult.translatedText, + example: null, + }], + }); + + const deckName = decks.find((d) => d.id === deckId)?.name || "Unknown"; + toast.success(t("savedToDeck", { deckName })); + setShowSaveModal(false); + } catch (error) { + toast.error(t("saveFailed")); + } finally { + setIsSaving(false); + } + }; + return (
{/* TCard Component */} @@ -161,49 +271,36 @@ export default function TranslatorPage() { src={IMAGES.play_arrow} alt="play" onClick={() => { - const t = taref.current?.value; - if (!t) return; - tts(t, translationResult?.sourceLanguage || ""); + const text = taref.current?.value; + if (!text) return; + tts(text, translationResult?.sourceLanguage || ""); }} >
-
+
{t("sourceLanguage")} - setSourceLanguage("Auto")} - className="shrink-0 hidden lg:inline-flex" - > - {t("auto")} - - setSourceLanguage("Chinese")} - className="shrink-0 hidden lg:inline-flex" - > - {t("chinese")} - - setSourceLanguage("English")} - className="shrink-0 hidden xl:inline-flex" - > - {t("english")} - - - {SOURCE_LANGUAGES.map((lang) => ( - - ))} - + value={customSourceLanguage} + onChange={(e) => setCustomSourceLanguage(e.target.value)} + placeholder={t("customLanguage")} + className="w-auto min-w-[120px] shrink-0" + />
-
+
{t("translateInto")} - { - setTargetLanguage("Chinese"); - setCustomTargetLanguage(""); - }} - className="shrink-0 hidden lg:inline-flex" - > - {t("chinese")} - - { - setTargetLanguage("English"); - setCustomTargetLanguage(""); - }} - className="shrink-0 hidden lg:inline-flex" - > - {t("english")} - - { - setTargetLanguage("Japanese"); - setCustomTargetLanguage(""); - }} - className="shrink-0 hidden xl:inline-flex" - > - {t("japanese")} - + {visibleTargetButtons.map((lang) => ( + { + setTargetLanguage(lang.value); + setCustomTargetLanguage(""); + }} + className="shrink-0" + > + {t(lang.labelKey)} + + ))} {/* TranslateButton Component */} -
+
{t("translate")} + {translationResult && session && decks.length > 0 && ( + setShowSaveModal(true)} + title={t("saveAsCard")} + > + + + )}
+ + {showSaveModal && ( +
+
+

{t("saveAsCard")}

+
+ + +
+
+
{t("front")}:
+
{lastTranslation?.sourceText}
+
{t("back")}:
+
{translationResult?.translatedText}
+
+
+ setShowSaveModal(false)}> + {t("cancel")} + + + {t("save")} + +
+
+
+ )}
); } diff --git a/src/app/decks/DecksClient.tsx b/src/app/decks/DecksClient.tsx index d32d055..4542578 100644 --- a/src/app/decks/DecksClient.tsx +++ b/src/app/decks/DecksClient.tsx @@ -27,7 +27,6 @@ import { actionGetDeckById, } from "@/modules/deck/deck-action"; import type { ActionOutputDeck } from "@/modules/deck/deck-action-dto"; -import { ImportButton, ExportButton } from "@/components/deck/ImportExport"; interface DeckCardProps { deck: ActionOutputDeck; @@ -199,7 +198,6 @@ export function DecksClient({ userId }: DecksClientProps) { {t("newDeck")} -
diff --git a/src/app/decks/[deck_id]/AddCardModal.tsx b/src/app/decks/[deck_id]/AddCardModal.tsx index 68f2d96..eccd252 100644 --- a/src/app/decks/[deck_id]/AddCardModal.tsx +++ b/src/app/decks/[deck_id]/AddCardModal.tsx @@ -1,15 +1,25 @@ "use client"; -import { LightButton } from "@/design-system/base/button"; +import { LightButton, PrimaryButton } from "@/design-system/base/button"; import { Input } from "@/design-system/base/input"; -import { X } from "lucide-react"; -import { useRef, useState } from "react"; +import { Select } from "@/design-system/base/select"; +import { Textarea } from "@/design-system/base/textarea"; +import { Modal } from "@/design-system/overlay/modal"; +import { VStack, HStack } from "@/design-system/layout/stack"; +import { Plus, Trash2 } from "lucide-react"; +import { useState } from "react"; import { useTranslations } from "next-intl"; -import { actionCreateNote } from "@/modules/note/note-action"; import { actionCreateCard } from "@/modules/card/card-action"; -import { actionGetNoteTypesByUserId, actionCreateDefaultBasicNoteType } from "@/modules/note-type/note-type-action"; +import type { CardType, CardMeaning } from "@/modules/card/card-action-dto"; import { toast } from "sonner"; +const QUERY_LANGUAGES = [ + { value: "en", labelKey: "english" }, + { value: "zh", labelKey: "chinese" }, + { value: "ja", labelKey: "japanese" }, + { value: "ko", labelKey: "korean" }, +] as const; + interface AddCardModalProps { isOpen: boolean; onClose: () => void; @@ -24,75 +34,89 @@ export function AddCardModal({ onAdded, }: AddCardModalProps) { const t = useTranslations("deck_id"); - const wordRef = useRef(null); - const definitionRef = useRef(null); - const ipaRef = useRef(null); - const exampleRef = useRef(null); + + const [cardType, setCardType] = useState("WORD"); + const [word, setWord] = useState(""); + const [ipa, setIpa] = useState(""); + const [queryLang, setQueryLang] = useState("en"); + const [customQueryLang, setCustomQueryLang] = useState(""); + const [meanings, setMeanings] = useState([ + { partOfSpeech: null, definition: "", example: null } + ]); const [isSubmitting, setIsSubmitting] = useState(false); - if (!isOpen) return null; + const showIpa = cardType === "WORD" || cardType === "PHRASE"; + + const addMeaning = () => { + setMeanings([...meanings, { partOfSpeech: null, definition: "", example: null }]); + }; + + const removeMeaning = (index: number) => { + if (meanings.length > 1) { + setMeanings(meanings.filter((_, i) => i !== index)); + } + }; + + const updateMeaning = ( + index: number, + field: "partOfSpeech" | "definition" | "example", + value: string + ) => { + const updated = [...meanings]; + updated[index] = { + ...updated[index], + [field]: value || null + }; + setMeanings(updated); + }; + + const resetForm = () => { + setCardType("WORD"); + setWord(""); + setIpa(""); + setQueryLang("en"); + setCustomQueryLang(""); + setMeanings([{ partOfSpeech: null, definition: "", example: null }]); + }; const handleAdd = async () => { - const word = wordRef.current?.value?.trim(); - const definition = definitionRef.current?.value?.trim(); + if (!word.trim()) { + toast.error(t("wordRequired")); + return; + } - if (!word || !definition) { - toast.error(t("wordAndDefinitionRequired")); + const validMeanings = meanings.filter(m => m.definition?.trim()); + if (validMeanings.length === 0) { + toast.error(t("definitionRequired")); return; } setIsSubmitting(true); + const effectiveQueryLang = customQueryLang.trim() || queryLang; + try { - let noteTypesResult = await actionGetNoteTypesByUserId(); - - if (!noteTypesResult.success || !noteTypesResult.data || noteTypesResult.data.length === 0) { - const createResult = await actionCreateDefaultBasicNoteType(); - if (!createResult.success || !createResult.data) { - throw new Error(createResult.message || "Failed to create note type"); - } - noteTypesResult = await actionGetNoteTypesByUserId(); - } - - if (!noteTypesResult.success || !noteTypesResult.data || noteTypesResult.data.length === 0) { - throw new Error("No note type available"); - } - - const noteTypeId = noteTypesResult.data[0].id; - - const fields = [ - word, - definition, - ipaRef.current?.value?.trim() || "", - exampleRef.current?.value?.trim() || "", - ]; - - const noteResult = await actionCreateNote({ - noteTypeId, - fields, - tags: [], - }); - - if (!noteResult.success || !noteResult.data) { - throw new Error(noteResult.message || "Failed to create note"); - } - const cardResult = await actionCreateCard({ - noteId: BigInt(noteResult.data.id), deckId, + word: word.trim(), + ipa: showIpa && ipa.trim() ? ipa.trim() : null, + queryLang: effectiveQueryLang, + cardType, + meanings: validMeanings.map(m => ({ + partOfSpeech: cardType === "SENTENCE" ? null : (m.partOfSpeech?.trim() || null), + definition: m.definition!.trim(), + example: m.example?.trim() || null, + })), }); if (!cardResult.success) { throw new Error(cardResult.message || "Failed to create card"); } - if (wordRef.current) wordRef.current.value = ""; - if (definitionRef.current) definitionRef.current.value = ""; - if (ipaRef.current) ipaRef.current.value = ""; - if (exampleRef.current) exampleRef.current.value = ""; - + resetForm(); onAdded(); onClose(); + toast.success(t("cardAdded") || "Card added successfully"); } catch (error) { toast.error(error instanceof Error ? error.message : "Unknown error"); } finally { @@ -100,55 +124,155 @@ export function AddCardModal({ } }; + const handleClose = () => { + resetForm(); + onClose(); + }; + return ( -
{ - if (e.key === "Enter") { - e.preventDefault(); - handleAdd(); - } - }} - > -
-
-

- {t("addNewCard")} -

- + + + {t("addNewCard")} + + + + + +
+ + +
+
+ +
+ + + {QUERY_LANGUAGES.map((lang) => ( + { + setQueryLang(lang.value); + setCustomQueryLang(""); + }} + size="sm" + > + {t(lang.labelKey)} + + ))} + setCustomQueryLang(e.target.value)} + placeholder={t("enterLanguageName")} + className="w-auto min-w-[100px] flex-1" + size="sm" + /> +
-
-
- - -
-
- - -
+ +
+ + setWord(e.target.value)} + className="w-full" + placeholder={cardType === "SENTENCE" ? t("sentencePlaceholder") : t("wordPlaceholder")} + /> +
+ + {showIpa && (
- + setIpa(e.target.value)} + className="w-full" + placeholder={t("ipaPlaceholder")} + />
-
-