Compare commits
2 Commits
168f0c161e
...
af684a15ce
| Author | SHA1 | Date | |
|---|---|---|---|
| af684a15ce | |||
| 279eee2953 |
54
AGENTS.md
54
AGENTS.md
@@ -104,6 +104,60 @@ log.info("Fetched folders", { count: folders.length });
|
|||||||
log.error("Failed to fetch folders", { error });
|
log.error("Failed to fetch folders", { error });
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### i18n 翻译检查
|
||||||
|
**注意:翻译缺失不会被 build 检测出来。**
|
||||||
|
|
||||||
|
**系统性检查翻译缺失的方法(改进版):**
|
||||||
|
|
||||||
|
#### 步骤 1: 使用 AST-grep 搜索所有翻译模式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 搜索所有 useTranslations 和 getTranslations 声明
|
||||||
|
ast-grep --pattern 'useTranslations($ARG)' --lang tsx --paths src/
|
||||||
|
|
||||||
|
# 搜索所有带插值的 t() 调用
|
||||||
|
ast-grep --pattern 't($ARG, $OPTS)' --lang tsx --paths src/
|
||||||
|
|
||||||
|
# 搜索所有简单 t() 调用
|
||||||
|
ast-grep --pattern 't($ARG)' --lang tsx --paths src/
|
||||||
|
```
|
||||||
|
|
||||||
|
**AST-grep 能捕获 31 种不同的翻译键模式, 而 grep 只能捕获 1 种模式。**
|
||||||
|
|
||||||
|
#### 步骤 2: 按文件提取所有翻译键
|
||||||
|
|
||||||
|
逐个 `.tsx` 文件检查使用的翻译键:
|
||||||
|
1. 找到该文件使用的 namespace(`useTranslations("namespace")` 或 `getTranslations("namespace")`)
|
||||||
|
2. 提取该文件中所有 `t("...")` 调用
|
||||||
|
3. 注意动态键模式:
|
||||||
|
- 模板字面量: `t(\`prefix.${variable}\`)`
|
||||||
|
- 条件键: `t(condition ? "a" : "b")`
|
||||||
|
- 变量键: `t(variable)`
|
||||||
|
4. 对比 `messages/en-US.json`,找出缺失的键
|
||||||
|
|
||||||
|
5. 先补全 `en-US.json`(作为基准语言)
|
||||||
|
6. 再根据 `en-US.json` 补全其他 7 种语言
|
||||||
|
|
||||||
|
#### 步骤 3: 验证 JSON 文件结构
|
||||||
|
**注意:JSON 语法错误会导致 build 失败,常见错误:**
|
||||||
|
- 重复的键(同一对象中出现两次相同的键名)
|
||||||
|
- 缺少逗号或多余的逗号
|
||||||
|
- 缺少闭合括号 `}`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 验证 JSON 格式
|
||||||
|
node -e "console.log(JSON.parse(require('fs').readFileSync('messages/en-US.json', 'utf8')))"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 步骤 4: 对比验证
|
||||||
|
```bash
|
||||||
|
# 列出代码中使用的所有 namespace
|
||||||
|
ast-grep --pattern 'useTranslations($ARG)' --lang tsx --paths src/ | grep -o 'useTranslations\|getTranslations' | sort | uniq
|
||||||
|
|
||||||
|
# 对比 messages/en-US.json 中的 namespace 列表
|
||||||
|
node -e "console.log(Object.keys(JSON.parse(require('fs').readFileSync('messages/en-US.json', 'utf8'))).join('\n'))"
|
||||||
|
```
|
||||||
|
|
||||||
## 反模式 (本项目)
|
## 反模式 (本项目)
|
||||||
|
|
||||||
- ❌ `index.ts` barrel exports
|
- ❌ `index.ts` barrel exports
|
||||||
|
|||||||
@@ -237,7 +237,7 @@
|
|||||||
"sourceCode": "GitHub",
|
"sourceCode": "GitHub",
|
||||||
"sign_in": "Anmelden",
|
"sign_in": "Anmelden",
|
||||||
"profile": "Profil",
|
"profile": "Profil",
|
||||||
"folders": "Ordner",
|
"folders": "Decks",
|
||||||
"explore": "Erkunden",
|
"explore": "Erkunden",
|
||||||
"favorites": "Favoriten",
|
"favorites": "Favoriten",
|
||||||
"settings": "Einstellungen"
|
"settings": "Einstellungen"
|
||||||
@@ -269,7 +269,9 @@
|
|||||||
"noDeck": "Bitte select a deck",
|
"noDeck": "Bitte select a deck",
|
||||||
"processingFailed": "OCR-Verarbeitung fehlgeschlagen",
|
"processingFailed": "OCR-Verarbeitung fehlgeschlagen",
|
||||||
"tryAgain": "Bitte try again with a clearer image",
|
"tryAgain": "Bitte try again with a clearer image",
|
||||||
"detectedLanguages": "Erkannt: {source} → {target}"
|
"detectedLanguages": "Erkannt: {source} → {target}",
|
||||||
|
"invalidFileType": "Ungültiger Dateityp. Bitte laden Sie eine Bilddatei hoch.",
|
||||||
|
"ocrFailed": "OCR-Verarbeitung fehlgeschlagen."
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"myProfile": "Mein Profil",
|
"myProfile": "Mein Profil",
|
||||||
@@ -310,12 +312,28 @@
|
|||||||
"videoUploadFailed": "Video-Upload fehlgeschlagen",
|
"videoUploadFailed": "Video-Upload fehlgeschlagen",
|
||||||
"subtitleUploadFailed": "Untertitel-Upload fehlgeschlagen",
|
"subtitleUploadFailed": "Untertitel-Upload fehlgeschlagen",
|
||||||
"subtitleLoadSuccess": "Untertitel erfolgreich geladen",
|
"subtitleLoadSuccess": "Untertitel erfolgreich geladen",
|
||||||
"subtitleLoadFailed": "Laden der Untertitel fehlgeschlagen"
|
"subtitleLoadFailed": "Laden der Untertitel fehlgeschlagen",
|
||||||
|
"settings": "Einstellungen",
|
||||||
|
"shortcuts": "Tastenkürzel",
|
||||||
|
"keyboardShortcuts": "Tastaturkürzel",
|
||||||
|
"playPause": "Wiedergabe/Pause",
|
||||||
|
"autoPauseToggle": "Auto-Pause umschalten",
|
||||||
|
"subtitleSettings": "Untertiteleinstellungen",
|
||||||
|
"fontSize": "Schriftgröße",
|
||||||
|
"textColor": "Textfarbe",
|
||||||
|
"backgroundColor": "Hintergrundfarbe",
|
||||||
|
"position": "Position",
|
||||||
|
"opacity": "Deckkraft",
|
||||||
|
"top": "Oben",
|
||||||
|
"center": "Mitte",
|
||||||
|
"bottom": "Unten"
|
||||||
},
|
},
|
||||||
"text_speaker": {
|
"text_speaker": {
|
||||||
"generateIPA": "IPA generieren",
|
"generateIPA": "IPA generieren",
|
||||||
"viewSavedItems": "Gespeicherte Einträge anzeigen",
|
"viewSavedItems": "Gespeicherte Einträge anzeigen",
|
||||||
"confirmDeleteAll": "Sind Sie sicher, dass Sie alles löschen möchten? (J/N)"
|
"confirmDeleteAll": "Sind Sie sicher, dass Sie alles löschen möchten? (J/N)",
|
||||||
|
"saved": "Gespeichert",
|
||||||
|
"clearAll": "Alles löschen"
|
||||||
},
|
},
|
||||||
"translator": {
|
"translator": {
|
||||||
"detectLanguage": "Sprache erkennen",
|
"detectLanguage": "Sprache erkennen",
|
||||||
@@ -462,5 +480,40 @@
|
|||||||
"followingOf": "{username} folgt",
|
"followingOf": "{username} folgt",
|
||||||
"noFollowers": "Noch keine Follower",
|
"noFollowers": "Noch keine Follower",
|
||||||
"noFollowing": "Folgt noch niemandem"
|
"noFollowing": "Folgt noch niemandem"
|
||||||
|
},
|
||||||
|
"deck_id": {
|
||||||
|
"unauthorized": "Sie sind nicht der Besitzer dieses Decks",
|
||||||
|
"back": "Zurück",
|
||||||
|
"cards": "Karten",
|
||||||
|
"itemsCount": "{count} Elemente",
|
||||||
|
"memorize": "Auswendig lernen",
|
||||||
|
"loadingCards": "Karten werden geladen...",
|
||||||
|
"noCards": "Keine Karten in diesem Deck",
|
||||||
|
"card": "Karte",
|
||||||
|
"addNewCard": "Neue Karte hinzufügen",
|
||||||
|
"add": "Hinzufügen",
|
||||||
|
"adding": "Wird hinzugefügt...",
|
||||||
|
"updateCard": "Karte aktualisieren",
|
||||||
|
"update": "Aktualisieren",
|
||||||
|
"updating": "Wird aktualisiert...",
|
||||||
|
"word": "Wort",
|
||||||
|
"definition": "Definition",
|
||||||
|
"ipa": "IPA",
|
||||||
|
"example": "Beispiel",
|
||||||
|
"wordAndDefinitionRequired": "Wort und Definition sind erforderlich",
|
||||||
|
"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...",
|
||||||
|
"cancel": "Abbrechen",
|
||||||
|
"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."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@
|
|||||||
"unfavorite": "Unfavorite",
|
"unfavorite": "Unfavorite",
|
||||||
"pleaseLogin": "Please login first"
|
"pleaseLogin": "Please login first"
|
||||||
},
|
},
|
||||||
"folder_id": {
|
"folder_id": {
|
||||||
"unauthorized": "You are not the owner of this folder",
|
"unauthorized": "You are not the owner of this folder",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
"textPairs": "Text Pairs",
|
"textPairs": "Text Pairs",
|
||||||
@@ -74,6 +74,41 @@
|
|||||||
"deleteFolder": "You do not have permission to delete this folder."
|
"deleteFolder": "You do not have permission to delete this folder."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"deck_id": {
|
||||||
|
"unauthorized": "You are not the owner of this deck",
|
||||||
|
"back": "Back",
|
||||||
|
"cards": "Cards",
|
||||||
|
"itemsCount": "{count} items",
|
||||||
|
"memorize": "Memorize",
|
||||||
|
"loadingCards": "Loading cards...",
|
||||||
|
"noCards": "No cards in this deck",
|
||||||
|
"card": "Card",
|
||||||
|
"addNewCard": "Add New Card",
|
||||||
|
"add": "Add",
|
||||||
|
"adding": "Adding...",
|
||||||
|
"updateCard": "Update Card",
|
||||||
|
"update": "Update",
|
||||||
|
"updating": "Updating...",
|
||||||
|
"word": "Word",
|
||||||
|
"definition": "Definition",
|
||||||
|
"ipa": "IPA",
|
||||||
|
"example": "Example",
|
||||||
|
"wordAndDefinitionRequired": "Word and definition are required",
|
||||||
|
"edit": "Edit",
|
||||||
|
"delete": "Delete",
|
||||||
|
"permissionDenied": "You do not have permission to perform this action",
|
||||||
|
"resetProgress": "Reset",
|
||||||
|
"resetProgressTitle": "Reset Deck Progress",
|
||||||
|
"resetProgressConfirm": "This will reset all cards in this deck to new state. Your learning progress will be lost. Are you sure?",
|
||||||
|
"resetSuccess": "Successfully reset {count} cards",
|
||||||
|
"resetting": "Resetting...",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"error": {
|
||||||
|
"update": "You do not have permission to update this card.",
|
||||||
|
"delete": "You do not have permission to delete this card.",
|
||||||
|
"add": "You do not have permission to add cards to this deck."
|
||||||
|
}
|
||||||
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"title": "Learn Languages",
|
"title": "Learn Languages",
|
||||||
"description": "Here is a very useful website to help you learn almost every language in the world, including constructed ones.",
|
"description": "Here is a very useful website to help you learn almost every language in the world, including constructed ones.",
|
||||||
@@ -228,12 +263,12 @@
|
|||||||
"sourceCode": "GitHub",
|
"sourceCode": "GitHub",
|
||||||
"sign_in": "Sign In",
|
"sign_in": "Sign In",
|
||||||
"profile": "Profile",
|
"profile": "Profile",
|
||||||
"folders": "Folders",
|
"folders": "Decks",
|
||||||
"explore": "Explore",
|
"explore": "Explore",
|
||||||
"favorites": "Favorites",
|
"favorites": "Favorites",
|
||||||
"settings": "Settings"
|
"settings": "Settings"
|
||||||
},
|
},
|
||||||
"ocr": {
|
"ocr": {
|
||||||
"title": "OCR Vocabulary Extractor",
|
"title": "OCR Vocabulary Extractor",
|
||||||
"description": "Upload vocabulary table screenshots from textbooks to extract word-definition pairs",
|
"description": "Upload vocabulary table screenshots from textbooks to extract word-definition pairs",
|
||||||
"uploadSection": "Upload Image",
|
"uploadSection": "Upload Image",
|
||||||
@@ -242,6 +277,7 @@
|
|||||||
"dropOrClick": "Drag and drop an image here, or click to select",
|
"dropOrClick": "Drag and drop an image here, or click to select",
|
||||||
"changeImage": "Click to change image",
|
"changeImage": "Click to change image",
|
||||||
"supportedFormats": "Supports: JPG, PNG, WebP",
|
"supportedFormats": "Supports: JPG, PNG, WebP",
|
||||||
|
"invalidFileType": "Invalid file type. Please upload an image file (JPG, PNG, or WebP).",
|
||||||
"deckSelection": "Select Deck",
|
"deckSelection": "Select Deck",
|
||||||
"selectDeck": "Select a deck",
|
"selectDeck": "Select a deck",
|
||||||
"chooseDeck": "Choose a deck to save extracted pairs",
|
"chooseDeck": "Choose a deck to save extracted pairs",
|
||||||
@@ -265,6 +301,7 @@
|
|||||||
"saving": "Saving...",
|
"saving": "Saving...",
|
||||||
"saved": "Successfully saved {count} pairs to {deck}",
|
"saved": "Successfully saved {count} pairs to {deck}",
|
||||||
"ocrSuccess": "Successfully extracted {count} pairs to {deck}",
|
"ocrSuccess": "Successfully extracted {count} pairs to {deck}",
|
||||||
|
"ocrFailed": "OCR processing failed. Please try again.",
|
||||||
"savedToDeck": "Saved to {deckName}",
|
"savedToDeck": "Saved to {deckName}",
|
||||||
"saveFailed": "Failed to save pairs",
|
"saveFailed": "Failed to save pairs",
|
||||||
"noImage": "Please upload an image first",
|
"noImage": "Please upload an image first",
|
||||||
@@ -315,12 +352,28 @@
|
|||||||
"videoUploadFailed": "Video upload failed",
|
"videoUploadFailed": "Video upload failed",
|
||||||
"subtitleUploadFailed": "Subtitle upload failed",
|
"subtitleUploadFailed": "Subtitle upload failed",
|
||||||
"subtitleLoadSuccess": "Subtitle loaded successfully",
|
"subtitleLoadSuccess": "Subtitle loaded successfully",
|
||||||
"subtitleLoadFailed": "Subtitle load failed"
|
"subtitleLoadFailed": "Subtitle load failed",
|
||||||
|
"settings": "Settings",
|
||||||
|
"shortcuts": "Shortcuts",
|
||||||
|
"keyboardShortcuts": "Keyboard Shortcuts",
|
||||||
|
"playPause": "Play/Pause",
|
||||||
|
"autoPauseToggle": "Toggle Auto Pause",
|
||||||
|
"subtitleSettings": "Subtitle Settings",
|
||||||
|
"fontSize": "Font Size",
|
||||||
|
"textColor": "Text Color",
|
||||||
|
"backgroundColor": "Background Color",
|
||||||
|
"position": "Position",
|
||||||
|
"opacity": "Opacity",
|
||||||
|
"top": "Top",
|
||||||
|
"center": "Center",
|
||||||
|
"bottom": "Bottom"
|
||||||
},
|
},
|
||||||
"text_speaker": {
|
"text_speaker": {
|
||||||
"generateIPA": "Generate IPA",
|
"generateIPA": "Generate IPA",
|
||||||
"viewSavedItems": "View Saved Items",
|
"viewSavedItems": "View Saved Items",
|
||||||
"confirmDeleteAll": "Are you sure you want to delete everything? (Y/N)"
|
"confirmDeleteAll": "Are you sure you want to delete everything? (Y/N)",
|
||||||
|
"saved": "Saved",
|
||||||
|
"clearAll": "Clear All"
|
||||||
},
|
},
|
||||||
"translator": {
|
"translator": {
|
||||||
"detectLanguage": "detect language",
|
"detectLanguage": "detect language",
|
||||||
|
|||||||
@@ -83,7 +83,76 @@
|
|||||||
"deleteFolder": "Vous n'avez pas la permission de supprimer ce dossier."
|
"deleteFolder": "Vous n'avez pas la permission de supprimer ce dossier."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"home": {
|
"deck_id": {
|
||||||
|
"unauthorized": "Vous n'êtes pas le propriétaire de ce deck",
|
||||||
|
"back": "Retour",
|
||||||
|
"cards": "Cartes",
|
||||||
|
"itemsCount": "{count} éléments",
|
||||||
|
"memorize": "Mémoriser",
|
||||||
|
"loadingCards": "Chargement des cartes...",
|
||||||
|
"noCards": "Aucune carte dans ce deck",
|
||||||
|
"card": "Carte",
|
||||||
|
"addNewCard": "Ajouter une nouvelle carte",
|
||||||
|
"add": "Ajouter",
|
||||||
|
"adding": "Ajout en cours...",
|
||||||
|
"updateCard": "Mettre à jour la carte",
|
||||||
|
"update": "Mettre à jour",
|
||||||
|
"updating": "Mise à jour en cours...",
|
||||||
|
"word": "Mot",
|
||||||
|
"definition": "Définition",
|
||||||
|
"ipa": "IPA",
|
||||||
|
"example": "Exemple",
|
||||||
|
"wordAndDefinitionRequired": "Le mot et la définition sont requis",
|
||||||
|
"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...",
|
||||||
|
"cancel": "Annuler",
|
||||||
|
"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."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deck_id": {
|
||||||
|
"unauthorized": "Vous n'êtes pas le propriétaire de ce paquet",
|
||||||
|
"back": "Retour",
|
||||||
|
"cards": "Cartes",
|
||||||
|
"itemsCount": "{count} éléments",
|
||||||
|
"memorize": "Mémoriser",
|
||||||
|
"loadingCards": "Chargement des cartes...",
|
||||||
|
"noCards": "Aucune carte dans ce paquet",
|
||||||
|
"card": "Carte",
|
||||||
|
"addNewCard": "Ajouter une nouvelle carte",
|
||||||
|
"add": "Ajouter",
|
||||||
|
"adding": "Ajout en cours...",
|
||||||
|
"updateCard": "Mettre à jour la carte",
|
||||||
|
"update": "Mettre à jour",
|
||||||
|
"updating": "Mise à jour en cours...",
|
||||||
|
"word": "Mot",
|
||||||
|
"definition": "Définition",
|
||||||
|
"ipa": "IPA",
|
||||||
|
"example": "Exemple",
|
||||||
|
"wordAndDefinitionRequired": "Le mot et la définition sont requis",
|
||||||
|
"edit": "Modifier",
|
||||||
|
"delete": "Supprimer",
|
||||||
|
"permissionDenied": "Vous n'avez pas la permission d'effectuer cette action",
|
||||||
|
"resetProgress": "Réinitialiser",
|
||||||
|
"resetProgressTitle": "Réinitialiser la progression du paquet",
|
||||||
|
"resetProgressConfirm": "Cela réinitialisera toutes les cards in this deck to new state. Your learning progress will be lost. Are you sure?",
|
||||||
|
"resetSuccess": "{count} cards réinitialisées avec succès",
|
||||||
|
"resetting": "Réinitialisation en cours...",
|
||||||
|
"cancel": "Annuler",
|
||||||
|
"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 paquet."
|
||||||
|
}
|
||||||
|
},
|
||||||
"title": "Apprendre les langues",
|
"title": "Apprendre les langues",
|
||||||
"description": "Voici un site Web très utile pour vous aider à apprendre presque toutes les langues du monde, y compris les langues construites.",
|
"description": "Voici un site Web très utile pour vous aider à apprendre presque toutes les langues du monde, y compris les langues construites.",
|
||||||
"explore": "Explorer",
|
"explore": "Explorer",
|
||||||
@@ -237,7 +306,7 @@
|
|||||||
"sourceCode": "GitHub",
|
"sourceCode": "GitHub",
|
||||||
"sign_in": "Connexion",
|
"sign_in": "Connexion",
|
||||||
"profile": "Profil",
|
"profile": "Profil",
|
||||||
"folders": "Dossiers",
|
"folders": "Decks",
|
||||||
"explore": "Explorer",
|
"explore": "Explorer",
|
||||||
"favorites": "Favoris",
|
"favorites": "Favoris",
|
||||||
"settings": "Paramètres"
|
"settings": "Paramètres"
|
||||||
@@ -454,6 +523,15 @@
|
|||||||
"view": "Voir"
|
"view": "Voir"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"follow": {
|
||||||
|
"follow": "Suivre",
|
||||||
|
"following": "Abonnements",
|
||||||
|
"followers": "Abonnés",
|
||||||
|
"followersOf": "Abonnés de {username}",
|
||||||
|
"followingOf": "Abonnements de {username}",
|
||||||
|
"noFollowers": "Pas encore d'abonnés",
|
||||||
|
"noFollowing": "Ne suit personne"
|
||||||
|
},
|
||||||
"follow": {
|
"follow": {
|
||||||
"follow": "Suivre",
|
"follow": "Suivre",
|
||||||
"following": "Abonné",
|
"following": "Abonné",
|
||||||
|
|||||||
@@ -83,7 +83,41 @@
|
|||||||
"deleteFolder": "Non hai il permesso di eliminare questa cartella."
|
"deleteFolder": "Non hai il permesso di eliminare questa cartella."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"home": {
|
"deck_id": {
|
||||||
|
"unauthorized": "Non sei il proprietario di questo deck",
|
||||||
|
"back": "Indietro",
|
||||||
|
"cards": "Schede",
|
||||||
|
"itemsCount": "{count} elementi",
|
||||||
|
"memorize": "Memorizza",
|
||||||
|
"loadingCards": "Caricamento schede...",
|
||||||
|
"noCards": "Nessuna scheda in questo deck",
|
||||||
|
"card": "Scheda",
|
||||||
|
"addNewCard": "Aggiungi nuova scheda",
|
||||||
|
"add": "Aggiungi",
|
||||||
|
"adding": "Aggiunta in corso...",
|
||||||
|
"updateCard": "Aggiorna scheda",
|
||||||
|
"update": "Aggiorna",
|
||||||
|
"updating": "Aggiornamento in corso...",
|
||||||
|
"word": "Parola",
|
||||||
|
"definition": "Definizione",
|
||||||
|
"ipa": "IPA",
|
||||||
|
"example": "Esempio",
|
||||||
|
"wordAndDefinitionRequired": "Parola e definizione sono obbligatori",
|
||||||
|
"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...",
|
||||||
|
"cancel": "Annulla",
|
||||||
|
"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."
|
||||||
|
}
|
||||||
|
},
|
||||||
"title": "Impara le Lingue",
|
"title": "Impara le Lingue",
|
||||||
"description": "Ecco un sito molto utile per aiutarti a imparare quasi tutte le lingue del mondo, incluse quelle costruite.",
|
"description": "Ecco un sito molto utile per aiutarti a imparare quasi tutte le lingue del mondo, incluse quelle costruite.",
|
||||||
"explore": "Esplora",
|
"explore": "Esplora",
|
||||||
@@ -237,7 +271,7 @@
|
|||||||
"sourceCode": "GitHub",
|
"sourceCode": "GitHub",
|
||||||
"sign_in": "Accedi",
|
"sign_in": "Accedi",
|
||||||
"profile": "Profilo",
|
"profile": "Profilo",
|
||||||
"folders": "Cartelle",
|
"folders": "Mazzi",
|
||||||
"explore": "Esplora",
|
"explore": "Esplora",
|
||||||
"favorites": "Preferiti",
|
"favorites": "Preferiti",
|
||||||
"settings": "Impostazioni"
|
"settings": "Impostazioni"
|
||||||
@@ -447,13 +481,22 @@
|
|||||||
"decks": {
|
"decks": {
|
||||||
"title": "Mazzi",
|
"title": "Mazzi",
|
||||||
"noDecks": "Nessun mazzo ancora",
|
"noDecks": "Nessun mazzo ancora",
|
||||||
"deckName": "Nome Mazzo",
|
"deckName": "Nome del mazzo",
|
||||||
"totalCards": "Carte Totali",
|
"totalCards": "Totale carte",
|
||||||
"createdAt": "Creata Il",
|
"createdAt": "Creata Il",
|
||||||
"actions": "Azioni",
|
"actions": "Azioni",
|
||||||
"view": "Visualizza"
|
"view": "Visualizza"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"follow": {
|
||||||
|
"follow": "Segui",
|
||||||
|
"following": "Seguiti",
|
||||||
|
"followers": "Seguaci",
|
||||||
|
"followersOf": "Seguaci di {username}",
|
||||||
|
"followingOf": "Seguiti di {username}",
|
||||||
|
"noFollowers": "Nessun seguace ancora",
|
||||||
|
"noFollowing": "Non segui ancora nessuno"
|
||||||
|
},
|
||||||
"follow": {
|
"follow": {
|
||||||
"follow": "Segui",
|
"follow": "Segui",
|
||||||
"following": "Stai seguendo",
|
"following": "Stai seguendo",
|
||||||
|
|||||||
@@ -74,6 +74,41 @@
|
|||||||
"deleteFolder": "このフォルダーを削除する権限がありません。"
|
"deleteFolder": "このフォルダーを削除する権限がありません。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"deck_id": {
|
||||||
|
"unauthorized": "このデッキの所有者ではありません",
|
||||||
|
"back": "戻る",
|
||||||
|
"cards": "カード",
|
||||||
|
"itemsCount": "{count}件",
|
||||||
|
"memorize": "暗記",
|
||||||
|
"loadingCards": "カードを読み込み中...",
|
||||||
|
"noCards": "このデッキにはカードがありません",
|
||||||
|
"card": "カード",
|
||||||
|
"addNewCard": "新しいカードを追加",
|
||||||
|
"add": "追加",
|
||||||
|
"adding": "追加中...",
|
||||||
|
"updateCard": "カードを更新",
|
||||||
|
"update": "更新",
|
||||||
|
"updating": "更新中...",
|
||||||
|
"word": "単語",
|
||||||
|
"definition": "定義",
|
||||||
|
"ipa": "発音記号",
|
||||||
|
"example": "例文",
|
||||||
|
"wordAndDefinitionRequired": "単語と定義は必須です",
|
||||||
|
"edit": "編集",
|
||||||
|
"delete": "削除",
|
||||||
|
"permissionDenied": "この操作を実行する権限がありません",
|
||||||
|
"resetProgress": "リセット",
|
||||||
|
"resetProgressTitle": "デッキの進捗をリセット",
|
||||||
|
"resetProgressConfirm": "このデッキのすべてのカードが新しい状態にリセットされます。学習の進捗は失われます。続行してもよろしいですか?",
|
||||||
|
"resetSuccess": "{count}枚のカードを正常にリセットしました",
|
||||||
|
"resetting": "リセット中...",
|
||||||
|
"cancel": "キャンセル",
|
||||||
|
"error": {
|
||||||
|
"update": "このカードを更新する権限がありません。",
|
||||||
|
"delete": "このカードを削除する権限がありません。",
|
||||||
|
"add": "このデッキにカードを追加する権限がありません。"
|
||||||
|
}
|
||||||
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"title": "言語を学ぶ",
|
"title": "言語を学ぶ",
|
||||||
"description": "ここは世界のほぼすべての言語(人工言語を含む)を学ぶのに役立つ非常に便利なウェブサイトです。",
|
"description": "ここは世界のほぼすべての言語(人工言語を含む)を学ぶのに役立つ非常に便利なウェブサイトです。",
|
||||||
@@ -228,7 +263,7 @@
|
|||||||
"sourceCode": "GitHub",
|
"sourceCode": "GitHub",
|
||||||
"sign_in": "サインイン",
|
"sign_in": "サインイン",
|
||||||
"profile": "プロフィール",
|
"profile": "プロフィール",
|
||||||
"folders": "フォルダー",
|
"folders": "デッキ",
|
||||||
"explore": "探索",
|
"explore": "探索",
|
||||||
"favorites": "お気に入り",
|
"favorites": "お気に入り",
|
||||||
"settings": "設定"
|
"settings": "設定"
|
||||||
@@ -260,7 +295,9 @@
|
|||||||
"noDeck": "デッキを選択してください",
|
"noDeck": "デッキを選択してください",
|
||||||
"processingFailed": "OCR処理に失敗しました",
|
"processingFailed": "OCR処理に失敗しました",
|
||||||
"tryAgain": "より鮮明な画像でお試しください",
|
"tryAgain": "より鮮明な画像でお試しください",
|
||||||
"detectedLanguages": "検出:{source} → {target}"
|
"detectedLanguages": "検出:{source} → {target}",
|
||||||
|
"invalidFileType": "無効なファイルタイプです。画像ファイルをアップロードしてください。",
|
||||||
|
"ocrFailed": "OCR処理に失敗しました。"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"myProfile": "マイプロフィール",
|
"myProfile": "マイプロフィール",
|
||||||
@@ -301,12 +338,28 @@
|
|||||||
"videoUploadFailed": "ビデオのアップロードに失敗しました",
|
"videoUploadFailed": "ビデオのアップロードに失敗しました",
|
||||||
"subtitleUploadFailed": "字幕のアップロードに失敗しました",
|
"subtitleUploadFailed": "字幕のアップロードに失敗しました",
|
||||||
"subtitleLoadSuccess": "字幕の読み込みに成功しました",
|
"subtitleLoadSuccess": "字幕の読み込みに成功しました",
|
||||||
"subtitleLoadFailed": "字幕の読み込みに失敗しました"
|
"subtitleLoadFailed": "字幕の読み込みに失敗しました",
|
||||||
|
"settings": "設定",
|
||||||
|
"shortcuts": "ショートカット",
|
||||||
|
"keyboardShortcuts": "キーボードショートカット",
|
||||||
|
"playPause": "再生/一時停止",
|
||||||
|
"autoPauseToggle": "自動一時停止の切り替え",
|
||||||
|
"subtitleSettings": "字幕設定",
|
||||||
|
"fontSize": "フォントサイズ",
|
||||||
|
"textColor": "文字色",
|
||||||
|
"backgroundColor": "背景色",
|
||||||
|
"position": "位置",
|
||||||
|
"opacity": "不透明度",
|
||||||
|
"top": "上",
|
||||||
|
"center": "中央",
|
||||||
|
"bottom": "下"
|
||||||
},
|
},
|
||||||
"text_speaker": {
|
"text_speaker": {
|
||||||
"generateIPA": "IPAを生成",
|
"generateIPA": "IPAを生成",
|
||||||
"viewSavedItems": "保存済み項目を表示",
|
"viewSavedItems": "保存済み項目を表示",
|
||||||
"confirmDeleteAll": "すべて削除してもよろしいですか? (Y/N)"
|
"confirmDeleteAll": "すべて削除してもよろしいですか? (Y/N)",
|
||||||
|
"saved": "保存済み",
|
||||||
|
"clearAll": "すべてクリア"
|
||||||
},
|
},
|
||||||
"translator": {
|
"translator": {
|
||||||
"detectLanguage": "言語を検出",
|
"detectLanguage": "言語を検出",
|
||||||
@@ -464,12 +517,12 @@
|
|||||||
"clickToUpload": "クリックしてAPKGファイルをアップロード",
|
"clickToUpload": "クリックしてAPKGファイルをアップロード",
|
||||||
"apkgFilesOnly": ".apkgファイルのみ対応",
|
"apkgFilesOnly": ".apkgファイルのみ対応",
|
||||||
"parsing": "解析中...",
|
"parsing": "解析中...",
|
||||||
"foundDecks": "{count}個のデッキが見つかりました",
|
"foundDecks": "{count} 個のデッキが見つかりました",
|
||||||
"deckName": "デッキ名",
|
"deckName": "デッキ名",
|
||||||
"back": "戻る",
|
"back": "戻る",
|
||||||
"import": "インポート",
|
"import": "インポート",
|
||||||
"importing": "インポート中...",
|
"importing": "インポート中...",
|
||||||
"exportSuccess": "デッキをエクスポートしました",
|
"exportSuccess": "デッキのエクスポートが成功しました",
|
||||||
"goToDecks": "デッキへ移動"
|
"goToDecks": "デッキへ移動"
|
||||||
},
|
},
|
||||||
"follow": {
|
"follow": {
|
||||||
@@ -477,7 +530,7 @@
|
|||||||
"following": "フォロー中",
|
"following": "フォロー中",
|
||||||
"followers": "フォロワー",
|
"followers": "フォロワー",
|
||||||
"followersOf": "{username}のフォロワー",
|
"followersOf": "{username}のフォロワー",
|
||||||
"followingOf": "{username}のフォロー",
|
"followingOf": "{username}のフォロー中",
|
||||||
"noFollowers": "まだフォロワーがいません",
|
"noFollowers": "まだフォロワーがいません",
|
||||||
"noFollowing": "まだ誰もフォローしていません"
|
"noFollowing": "まだ誰もフォローしていません"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -237,7 +237,7 @@
|
|||||||
"sourceCode": "GitHub",
|
"sourceCode": "GitHub",
|
||||||
"sign_in": "로그인",
|
"sign_in": "로그인",
|
||||||
"profile": "프로필",
|
"profile": "프로필",
|
||||||
"folders": "폴더",
|
"folders": "덱",
|
||||||
"explore": "탐색",
|
"explore": "탐색",
|
||||||
"favorites": "즐겨찾기",
|
"favorites": "즐겨찾기",
|
||||||
"settings": "설정"
|
"settings": "설정"
|
||||||
@@ -454,6 +454,15 @@
|
|||||||
"view": "보기"
|
"view": "보기"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"follow": {
|
||||||
|
"follow": "팔로우",
|
||||||
|
"following": "팔로잉",
|
||||||
|
"followers": "팔로워",
|
||||||
|
"followersOf": "{username}의 팔로워",
|
||||||
|
"followingOf": "{username}의 팔로잉",
|
||||||
|
"noFollowers": "아직 팔로워가 없습니다",
|
||||||
|
"noFollowing": "아직 팔로잉하는 사람이 없습니다"
|
||||||
|
},
|
||||||
"follow": {
|
"follow": {
|
||||||
"follow": "팔로우",
|
"follow": "팔로우",
|
||||||
"following": "팔로잉",
|
"following": "팔로잉",
|
||||||
|
|||||||
@@ -237,7 +237,7 @@
|
|||||||
"sourceCode": "GitHub",
|
"sourceCode": "GitHub",
|
||||||
"sign_in": "كىرىش",
|
"sign_in": "كىرىش",
|
||||||
"profile": "شەخسىي ئۇچۇر",
|
"profile": "شەخسىي ئۇچۇر",
|
||||||
"folders": "قىسقۇچلار",
|
"folders": "دېكلار",
|
||||||
"explore": "ئىزدىنىش",
|
"explore": "ئىزدىنىش",
|
||||||
"favorites": "يىغىپ ساقلاش",
|
"favorites": "يىغىپ ساقلاش",
|
||||||
"settings": "تەڭشەكلەر"
|
"settings": "تەڭشەكلەر"
|
||||||
@@ -454,6 +454,15 @@
|
|||||||
"view": "كۆرۈش"
|
"view": "كۆرۈش"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"follow": {
|
||||||
|
"follow": "ئەگىشىش",
|
||||||
|
"following": "ئەگىشىۋاتقانلار",
|
||||||
|
"followers": "ئەگەشكۈچىلەر",
|
||||||
|
"followersOf": "{username} نىڭ ئەگەشكۈچىلىرى",
|
||||||
|
"followingOf": "{username} نىڭ ئەگىشىۋاتقانلىرى",
|
||||||
|
"noFollowers": "تېخى ئەگەشكۈچى يوق",
|
||||||
|
"noFollowing": "تېخى ئەگىشىۋاتقان يوق"
|
||||||
|
},
|
||||||
"follow": {
|
"follow": {
|
||||||
"follow": "ئەگىشىش",
|
"follow": "ئەگىشىش",
|
||||||
"following": "ئەگىشىۋاتىدۇ",
|
"following": "ئەگىشىۋاتىدۇ",
|
||||||
|
|||||||
@@ -74,6 +74,41 @@
|
|||||||
"deleteFolder": "您没有权限删除此文件夹"
|
"deleteFolder": "您没有权限删除此文件夹"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"deck_id": {
|
||||||
|
"unauthorized": "您不是此牌组的所有者",
|
||||||
|
"back": "返回",
|
||||||
|
"cards": "卡片",
|
||||||
|
"itemsCount": "{count} 个",
|
||||||
|
"memorize": "记忆",
|
||||||
|
"loadingCards": "加载卡片中...",
|
||||||
|
"noCards": "此牌组中没有卡片",
|
||||||
|
"card": "卡片",
|
||||||
|
"addNewCard": "添加新卡片",
|
||||||
|
"add": "添加",
|
||||||
|
"adding": "添加中...",
|
||||||
|
"updateCard": "更新卡片",
|
||||||
|
"update": "更新",
|
||||||
|
"updating": "更新中...",
|
||||||
|
"word": "单词",
|
||||||
|
"definition": "释义",
|
||||||
|
"ipa": "音标",
|
||||||
|
"example": "例句",
|
||||||
|
"wordAndDefinitionRequired": "单词和释义都是必需的",
|
||||||
|
"edit": "编辑",
|
||||||
|
"delete": "删除",
|
||||||
|
"permissionDenied": "您没有权限执行此操作",
|
||||||
|
"resetProgress": "重置进度",
|
||||||
|
"resetProgressTitle": "重置牌组进度",
|
||||||
|
"resetProgressConfirm": "这将把牌组内所有卡片重置为新卡片状态。您的学习进度将会丢失。确定要继续吗?",
|
||||||
|
"resetSuccess": "成功重置 {count} 张卡片",
|
||||||
|
"resetting": "重置中...",
|
||||||
|
"cancel": "取消",
|
||||||
|
"error": {
|
||||||
|
"update": "您没有权限更新此卡片",
|
||||||
|
"delete": "您没有权限删除此卡片",
|
||||||
|
"add": "您没有权限向此牌组添加卡片"
|
||||||
|
}
|
||||||
|
},
|
||||||
"home": {
|
"home": {
|
||||||
"title": "学语言",
|
"title": "学语言",
|
||||||
"description": "这里是一个非常有用的网站,可以帮助您学习世界上几乎每一种语言,包括人造语言。",
|
"description": "这里是一个非常有用的网站,可以帮助您学习世界上几乎每一种语言,包括人造语言。",
|
||||||
@@ -228,7 +263,7 @@
|
|||||||
"sourceCode": "源码",
|
"sourceCode": "源码",
|
||||||
"sign_in": "登录",
|
"sign_in": "登录",
|
||||||
"profile": "个人资料",
|
"profile": "个人资料",
|
||||||
"folders": "文件夹",
|
"folders": "牌组",
|
||||||
"explore": "探索",
|
"explore": "探索",
|
||||||
"favorites": "收藏",
|
"favorites": "收藏",
|
||||||
"settings": "设置"
|
"settings": "设置"
|
||||||
@@ -242,6 +277,7 @@
|
|||||||
"dropOrClick": "拖放图片到此处,或点击选择",
|
"dropOrClick": "拖放图片到此处,或点击选择",
|
||||||
"changeImage": "点击更换图片",
|
"changeImage": "点击更换图片",
|
||||||
"supportedFormats": "支持格式:JPG、PNG、WebP",
|
"supportedFormats": "支持格式:JPG、PNG、WebP",
|
||||||
|
"invalidFileType": "无效的文件类型,请上传图片文件(JPG、PNG 或 WebP)",
|
||||||
"deckSelection": "选择牌组",
|
"deckSelection": "选择牌组",
|
||||||
"selectDeck": "选择牌组",
|
"selectDeck": "选择牌组",
|
||||||
"chooseDeck": "选择保存提取词汇的牌组",
|
"chooseDeck": "选择保存提取词汇的牌组",
|
||||||
@@ -315,12 +351,28 @@
|
|||||||
"videoUploadFailed": "视频上传失败",
|
"videoUploadFailed": "视频上传失败",
|
||||||
"subtitleUploadFailed": "字幕上传失败",
|
"subtitleUploadFailed": "字幕上传失败",
|
||||||
"subtitleLoadSuccess": "字幕加载成功",
|
"subtitleLoadSuccess": "字幕加载成功",
|
||||||
"subtitleLoadFailed": "字幕加载失败"
|
"subtitleLoadFailed": "字幕加载失败",
|
||||||
|
"settings": "设置",
|
||||||
|
"shortcuts": "快捷键",
|
||||||
|
"keyboardShortcuts": "键盘快捷键",
|
||||||
|
"playPause": "播放/暂停",
|
||||||
|
"autoPauseToggle": "切换自动暂停",
|
||||||
|
"subtitleSettings": "字幕设置",
|
||||||
|
"fontSize": "字体大小",
|
||||||
|
"textColor": "文字颜色",
|
||||||
|
"backgroundColor": "背景颜色",
|
||||||
|
"position": "位置",
|
||||||
|
"opacity": "不透明度",
|
||||||
|
"top": "顶部",
|
||||||
|
"center": "居中",
|
||||||
|
"bottom": "底部"
|
||||||
},
|
},
|
||||||
"text_speaker": {
|
"text_speaker": {
|
||||||
"generateIPA": "生成IPA",
|
"generateIPA": "生成IPA",
|
||||||
"viewSavedItems": "查看保存项",
|
"viewSavedItems": "查看保存项",
|
||||||
"confirmDeleteAll": "确定删光吗?(Y/N)"
|
"confirmDeleteAll": "确定删光吗?(Y/N)",
|
||||||
|
"saved": "已保存",
|
||||||
|
"clearAll": "清空全部"
|
||||||
},
|
},
|
||||||
"translator": {
|
"translator": {
|
||||||
"detectLanguage": "检测语言",
|
"detectLanguage": "检测语言",
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ArrowLeft, Plus } from "lucide-react";
|
import { ArrowLeft, Plus, RotateCcw } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { redirect, useRouter } from "next/navigation";
|
import { redirect, useRouter } from "next/navigation";
|
||||||
import { AddCardModal } from "./AddCardModal";
|
import { AddCardModal } from "./AddCardModal";
|
||||||
import { CardItem } from "./CardItem";
|
import { CardItem } from "./CardItem";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { PageLayout } from "@/components/ui/PageLayout";
|
import { PageLayout } from "@/components/ui/PageLayout";
|
||||||
import { PrimaryButton, CircleButton, LinkButton } from "@/design-system/base/button";
|
import { PrimaryButton, CircleButton, LinkButton, LightButton } from "@/design-system/base/button";
|
||||||
import { CardList } from "@/components/ui/CardList";
|
import { CardList } from "@/components/ui/CardList";
|
||||||
import { actionGetCardsByDeckIdWithNotes, actionDeleteCard } from "@/modules/card/card-action";
|
import { Modal } from "@/design-system/overlay/modal";
|
||||||
|
import { actionGetCardsByDeckIdWithNotes, actionDeleteCard, actionResetDeckCards } from "@/modules/card/card-action";
|
||||||
import type { ActionOutputCardWithNote } from "@/modules/card/card-action-dto";
|
import type { ActionOutputCardWithNote } from "@/modules/card/card-action-dto";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
@@ -18,6 +19,8 @@ export function InDeck({ deckId, isReadOnly }: { deckId: number; isReadOnly: boo
|
|||||||
const [cards, setCards] = useState<ActionOutputCardWithNote[]>([]);
|
const [cards, setCards] = useState<ActionOutputCardWithNote[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [openAddModal, setAddModal] = useState(false);
|
const [openAddModal, setAddModal] = useState(false);
|
||||||
|
const [openResetModal, setResetModal] = useState(false);
|
||||||
|
const [resetting, setResetting] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const t = useTranslations("deck_id");
|
const t = useTranslations("deck_id");
|
||||||
|
|
||||||
@@ -54,6 +57,24 @@ export function InDeck({ deckId, isReadOnly }: { deckId: number; isReadOnly: boo
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleResetDeck = async () => {
|
||||||
|
setResetting(true);
|
||||||
|
try {
|
||||||
|
const result = await actionResetDeckCards({ deckId });
|
||||||
|
if (result.success) {
|
||||||
|
toast.success(t("resetSuccess", { count: result.data?.count ?? 0 }));
|
||||||
|
setResetModal(false);
|
||||||
|
await refreshCards();
|
||||||
|
} else {
|
||||||
|
toast.error(result.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error instanceof Error ? error.message : "Unknown error");
|
||||||
|
} finally {
|
||||||
|
setResetting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
@@ -84,6 +105,13 @@ export function InDeck({ deckId, isReadOnly }: { deckId: number; isReadOnly: boo
|
|||||||
{t("memorize")}
|
{t("memorize")}
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
{!isReadOnly && (
|
{!isReadOnly && (
|
||||||
|
<>
|
||||||
|
<LightButton
|
||||||
|
onClick={() => setResetModal(true)}
|
||||||
|
leftIcon={<RotateCcw size={16} />}
|
||||||
|
>
|
||||||
|
{t("resetProgress")}
|
||||||
|
</LightButton>
|
||||||
<CircleButton
|
<CircleButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setAddModal(true);
|
setAddModal(true);
|
||||||
@@ -91,6 +119,7 @@ export function InDeck({ deckId, isReadOnly }: { deckId: number; isReadOnly: boo
|
|||||||
>
|
>
|
||||||
<Plus size={18} className="text-gray-700" />
|
<Plus size={18} className="text-gray-700" />
|
||||||
</CircleButton>
|
</CircleButton>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -131,12 +160,30 @@ export function InDeck({ deckId, isReadOnly }: { deckId: number; isReadOnly: boo
|
|||||||
)}
|
)}
|
||||||
</CardList>
|
</CardList>
|
||||||
|
|
||||||
<AddCardModal
|
<AddCardModal
|
||||||
isOpen={openAddModal}
|
isOpen={openAddModal}
|
||||||
onClose={() => setAddModal(false)}
|
onClose={() => setAddModal(false)}
|
||||||
deckId={deckId}
|
deckId={deckId}
|
||||||
onAdded={refreshCards}
|
onAdded={refreshCards}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Reset Progress Confirmation Modal */}
|
||||||
|
<Modal open={openResetModal} onClose={() => setResetModal(false)} size="sm">
|
||||||
|
<Modal.Header>
|
||||||
|
<Modal.Title>{t("resetProgressTitle")}</Modal.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>
|
||||||
|
<p className="text-gray-600">{t("resetProgressConfirm")}</p>
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Footer>
|
||||||
|
<LightButton onClick={() => setResetModal(false)}>
|
||||||
|
{t("cancel")}
|
||||||
|
</LightButton>
|
||||||
|
<PrimaryButton onClick={handleResetDeck} loading={resetting}>
|
||||||
|
{resetting ? t("resetting") : t("resetProgress")}
|
||||||
|
</PrimaryButton>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Modal>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -162,3 +162,17 @@ export type ActionOutputGetCardById = {
|
|||||||
message: string;
|
message: string;
|
||||||
data?: ActionOutputCardWithNote;
|
data?: ActionOutputCardWithNote;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const schemaActionInputResetDeckCards = z.object({
|
||||||
|
deckId: z.number().int().positive(),
|
||||||
|
});
|
||||||
|
export type ActionInputResetDeckCards = z.infer<typeof schemaActionInputResetDeckCards>;
|
||||||
|
export const validateActionInputResetDeckCards = generateValidator(schemaActionInputResetDeckCards);
|
||||||
|
|
||||||
|
export type ActionOutputResetDeckCards = {
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
data?: {
|
||||||
|
count: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
ActionInputGetCardStats,
|
ActionInputGetCardStats,
|
||||||
ActionInputDeleteCard,
|
ActionInputDeleteCard,
|
||||||
ActionInputGetCardById,
|
ActionInputGetCardById,
|
||||||
|
ActionInputResetDeckCards,
|
||||||
ActionOutputCreateCard,
|
ActionOutputCreateCard,
|
||||||
ActionOutputAnswerCard,
|
ActionOutputAnswerCard,
|
||||||
ActionOutputGetCards,
|
ActionOutputGetCards,
|
||||||
@@ -23,6 +24,7 @@ import {
|
|||||||
ActionOutputCard,
|
ActionOutputCard,
|
||||||
ActionOutputCardWithNote,
|
ActionOutputCardWithNote,
|
||||||
ActionOutputScheduledCard,
|
ActionOutputScheduledCard,
|
||||||
|
ActionOutputResetDeckCards,
|
||||||
validateActionInputCreateCard,
|
validateActionInputCreateCard,
|
||||||
validateActionInputAnswerCard,
|
validateActionInputAnswerCard,
|
||||||
validateActionInputGetCardsForReview,
|
validateActionInputGetCardsForReview,
|
||||||
@@ -31,6 +33,7 @@ import {
|
|||||||
validateActionInputGetCardStats,
|
validateActionInputGetCardStats,
|
||||||
validateActionInputDeleteCard,
|
validateActionInputDeleteCard,
|
||||||
validateActionInputGetCardById,
|
validateActionInputGetCardById,
|
||||||
|
validateActionInputResetDeckCards,
|
||||||
} from "./card-action-dto";
|
} from "./card-action-dto";
|
||||||
import {
|
import {
|
||||||
serviceCreateCard,
|
serviceCreateCard,
|
||||||
@@ -43,6 +46,7 @@ import {
|
|||||||
serviceDeleteCard,
|
serviceDeleteCard,
|
||||||
serviceGetCardByIdWithNote,
|
serviceGetCardByIdWithNote,
|
||||||
serviceCheckCardOwnership,
|
serviceCheckCardOwnership,
|
||||||
|
serviceResetDeckCards,
|
||||||
} from "./card-service";
|
} from "./card-service";
|
||||||
import { CardQueue } from "../../../generated/prisma/enums";
|
import { CardQueue } from "../../../generated/prisma/enums";
|
||||||
|
|
||||||
@@ -425,3 +429,36 @@ export async function actionGetCardById(
|
|||||||
return { success: false, message: "An error occurred while fetching the card" };
|
return { success: false, message: "An error occurred while fetching the card" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function actionResetDeckCards(
|
||||||
|
input: unknown,
|
||||||
|
): Promise<ActionOutputResetDeckCards> {
|
||||||
|
try {
|
||||||
|
const userId = await getCurrentUserId();
|
||||||
|
if (!userId) {
|
||||||
|
return { success: false, message: "Unauthorized" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const validated = validateActionInputResetDeckCards(input);
|
||||||
|
const result = await serviceResetDeckCards({
|
||||||
|
deckId: validated.deckId,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return { success: false, message: result.message };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: result.message,
|
||||||
|
data: { count: result.count },
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof ValidateError) {
|
||||||
|
return { success: false, message: e.message };
|
||||||
|
}
|
||||||
|
log.error("Failed to reset deck cards", { error: e });
|
||||||
|
return { success: false, message: "An error occurred while resetting deck cards" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -102,3 +102,11 @@ export type RepoOutputCardStats = {
|
|||||||
review: number;
|
review: number;
|
||||||
due: number;
|
due: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface RepoInputResetDeckCards {
|
||||||
|
deckId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RepoOutputResetDeckCards = {
|
||||||
|
count: number;
|
||||||
|
};
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ import {
|
|||||||
RepoInputGetCardsForReview,
|
RepoInputGetCardsForReview,
|
||||||
RepoInputGetNewCards,
|
RepoInputGetNewCards,
|
||||||
RepoInputBulkUpdateCards,
|
RepoInputBulkUpdateCards,
|
||||||
|
RepoInputResetDeckCards,
|
||||||
RepoOutputCard,
|
RepoOutputCard,
|
||||||
RepoOutputCardWithNote,
|
RepoOutputCardWithNote,
|
||||||
RepoOutputCardStats,
|
RepoOutputCardStats,
|
||||||
|
RepoOutputResetDeckCards,
|
||||||
} from "./card-repository-dto";
|
} from "./card-repository-dto";
|
||||||
import { CardType, CardQueue } from "../../../generated/prisma/enums";
|
import { CardType, CardQueue } from "../../../generated/prisma/enums";
|
||||||
|
|
||||||
@@ -307,3 +309,29 @@ export async function repoGetCardsByNoteId(noteId: bigint): Promise<RepoOutputCa
|
|||||||
});
|
});
|
||||||
return cards;
|
return cards;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function repoResetDeckCards(
|
||||||
|
input: RepoInputResetDeckCards,
|
||||||
|
): Promise<RepoOutputResetDeckCards> {
|
||||||
|
log.debug("Resetting deck cards", { deckId: input.deckId });
|
||||||
|
|
||||||
|
const result = await prisma.card.updateMany({
|
||||||
|
where: { deckId: input.deckId },
|
||||||
|
data: {
|
||||||
|
type: CardType.NEW,
|
||||||
|
queue: CardQueue.NEW,
|
||||||
|
due: 0,
|
||||||
|
ivl: 0,
|
||||||
|
factor: 2500,
|
||||||
|
reps: 0,
|
||||||
|
lapses: 0,
|
||||||
|
left: 0,
|
||||||
|
odue: 0,
|
||||||
|
odid: 0,
|
||||||
|
mod: Math.floor(Date.now() / 1000),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
log.info("Deck cards reset", { deckId: input.deckId, count: result.count });
|
||||||
|
return { count: result.count };
|
||||||
|
}
|
||||||
|
|||||||
@@ -99,6 +99,24 @@ export type ServiceOutputReviewResult = {
|
|||||||
scheduled: ServiceOutputScheduledCard;
|
scheduled: ServiceOutputScheduledCard;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface ServiceInputResetDeckCards {
|
||||||
|
deckId: number;
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServiceInputCheckDeckOwnership {
|
||||||
|
deckId: number;
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ServiceOutputCheckDeckOwnership = boolean;
|
||||||
|
|
||||||
|
export type ServiceOutputResetDeckCards = {
|
||||||
|
success: boolean;
|
||||||
|
count: number;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
export const SM2_CONFIG = {
|
export const SM2_CONFIG = {
|
||||||
LEARNING_STEPS: [1, 10],
|
LEARNING_STEPS: [1, 10],
|
||||||
RELEARNING_STEPS: [10],
|
RELEARNING_STEPS: [10],
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ import {
|
|||||||
repoDeleteCard,
|
repoDeleteCard,
|
||||||
repoGetCardsByNoteId,
|
repoGetCardsByNoteId,
|
||||||
repoGetCardDeckOwnerId,
|
repoGetCardDeckOwnerId,
|
||||||
|
repoResetDeckCards,
|
||||||
} from "./card-repository";
|
} from "./card-repository";
|
||||||
|
import { repoGetUserIdByDeckId } from "@/modules/deck/deck-repository";
|
||||||
import {
|
import {
|
||||||
RepoInputUpdateCard,
|
RepoInputUpdateCard,
|
||||||
RepoOutputCard,
|
RepoOutputCard,
|
||||||
@@ -25,12 +27,15 @@ import {
|
|||||||
ServiceInputGetCardsByDeckId,
|
ServiceInputGetCardsByDeckId,
|
||||||
ServiceInputGetCardStats,
|
ServiceInputGetCardStats,
|
||||||
ServiceInputCheckCardOwnership,
|
ServiceInputCheckCardOwnership,
|
||||||
|
ServiceInputResetDeckCards,
|
||||||
|
ServiceInputCheckDeckOwnership,
|
||||||
ServiceOutputCard,
|
ServiceOutputCard,
|
||||||
ServiceOutputCardWithNote,
|
ServiceOutputCardWithNote,
|
||||||
ServiceOutputCardStats,
|
ServiceOutputCardStats,
|
||||||
ServiceOutputScheduledCard,
|
ServiceOutputScheduledCard,
|
||||||
ServiceOutputReviewResult,
|
ServiceOutputReviewResult,
|
||||||
ServiceOutputCheckCardOwnership,
|
ServiceOutputCheckCardOwnership,
|
||||||
|
ServiceOutputResetDeckCards,
|
||||||
ReviewEase,
|
ReviewEase,
|
||||||
SM2_CONFIG,
|
SM2_CONFIG,
|
||||||
} from "./card-service-dto";
|
} from "./card-service-dto";
|
||||||
@@ -495,3 +500,27 @@ export async function serviceCheckCardOwnership(
|
|||||||
const ownerId = await repoGetCardDeckOwnerId(input.cardId);
|
const ownerId = await repoGetCardDeckOwnerId(input.cardId);
|
||||||
return ownerId === input.userId;
|
return ownerId === input.userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function serviceCheckDeckOwnership(
|
||||||
|
input: ServiceInputCheckDeckOwnership,
|
||||||
|
): Promise<ServiceOutputCheckCardOwnership> {
|
||||||
|
log.debug("Checking deck ownership", { deckId: input.deckId });
|
||||||
|
const ownerId = await repoGetUserIdByDeckId(input.deckId);
|
||||||
|
return ownerId === input.userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function serviceResetDeckCards(
|
||||||
|
input: ServiceInputResetDeckCards,
|
||||||
|
): Promise<ServiceOutputResetDeckCards> {
|
||||||
|
log.info("Resetting deck cards", { deckId: input.deckId, userId: input.userId });
|
||||||
|
|
||||||
|
const isOwner = await serviceCheckDeckOwnership({ deckId: input.deckId, userId: input.userId });
|
||||||
|
if (!isOwner) {
|
||||||
|
return { success: false, count: 0, message: "You do not have permission to reset this deck" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await repoResetDeckCards({ deckId: input.deckId });
|
||||||
|
|
||||||
|
log.info("Deck cards reset successfully", { deckId: input.deckId, count: result.count });
|
||||||
|
return { success: true, count: result.count, message: "Deck cards reset successfully" };
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ export function repoGenerateGuid(): string {
|
|||||||
|
|
||||||
export function repoCalculateCsum(text: string): number {
|
export function repoCalculateCsum(text: string): number {
|
||||||
const hash = createHash("sha1").update(text.normalize("NFC")).digest("hex");
|
const hash = createHash("sha1").update(text.normalize("NFC")).digest("hex");
|
||||||
return parseInt(hash.substring(0, 8), 16);
|
// Use 7 hex chars to stay within INTEGER range (max 268,435,455 < 2,147,483,647)
|
||||||
|
return parseInt(hash.substring(0, 7), 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function repoJoinFields(fields: string[]): string {
|
export function repoJoinFields(fields: string[]): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user