diff --git a/backend/controllers/vocabController.js b/backend/controllers/vocabController.js
index db5591f..d8d7979 100755
--- a/backend/controllers/vocabController.js
+++ b/backend/controllers/vocabController.js
@@ -43,10 +43,16 @@ class VocabController {
this.getCourseSrsDue = this._wrapWithUser((userId, req) =>
this.service.getCourseSrsDue(userId, req.params.courseId, req.query)
);
+ this.getCourseHardSrsItems = this._wrapWithUser((userId, req) =>
+ this.service.getCourseHardSrsItems(userId, req.params.courseId)
+ );
this.reviewSrsItem = this._wrapWithUser((userId, req) =>
this.service.reviewSrsItem(userId, req.body),
{ successStatus: 201 }
);
+ this.clearSrsItemHardStatus = this._wrapWithUser((userId, req) =>
+ this.service.clearSrsItemHardStatus(userId, req.body)
+ );
this.getCourseByShareCode = this._wrapWithUser((userId, req) => this.service.getCourseByShareCode(userId, req.body.shareCode));
this.updateCourse = this._wrapWithUser((userId, req) => this.service.updateCourse(userId, req.params.courseId, req.body));
this.deleteCourse = this._wrapWithUser((userId, req) => this.service.deleteCourse(userId, req.params.courseId));
diff --git a/backend/migrations-active/20260820000000-add-vocab-srs-hard-status.cjs b/backend/migrations-active/20260820000000-add-vocab-srs-hard-status.cjs
new file mode 100644
index 0000000..e3de9f2
--- /dev/null
+++ b/backend/migrations-active/20260820000000-add-vocab-srs-hard-status.cjs
@@ -0,0 +1,24 @@
+'use strict';
+
+module.exports = {
+ async up(queryInterface) {
+ await queryInterface.sequelize.query(`
+ ALTER TABLE community.vocab_srs_item
+ ADD COLUMN IF NOT EXISTS is_hard BOOLEAN NOT NULL DEFAULT FALSE;
+ `);
+ await queryInterface.sequelize.query(`
+ CREATE INDEX IF NOT EXISTS idx_vocab_srs_item_hard
+ ON community.vocab_srs_item (user_id, course_id, is_hard, next_due_at);
+ `);
+ },
+
+ async down(queryInterface) {
+ await queryInterface.sequelize.query(`
+ DROP INDEX IF EXISTS community.idx_vocab_srs_item_hard;
+ `);
+ await queryInterface.sequelize.query(`
+ ALTER TABLE community.vocab_srs_item
+ DROP COLUMN IF EXISTS is_hard;
+ `);
+ }
+};
diff --git a/backend/models/community/vocab_srs_item.js b/backend/models/community/vocab_srs_item.js
index b5c2bbd..54114ea 100755
--- a/backend/models/community/vocab_srs_item.js
+++ b/backend/models/community/vocab_srs_item.js
@@ -81,6 +81,12 @@ VocabSrsItem.init({
allowNull: false,
defaultValue: 0,
field: 'lapse_count'
+ },
+ isHard: {
+ type: DataTypes.BOOLEAN,
+ allowNull: false,
+ defaultValue: false,
+ field: 'is_hard'
}
}, {
sequelize,
diff --git a/backend/routers/vocabRouter.js b/backend/routers/vocabRouter.js
index 349f990..cac65ff 100755
--- a/backend/routers/vocabRouter.js
+++ b/backend/routers/vocabRouter.js
@@ -36,10 +36,12 @@ router.get('/courses/:courseId/completed-lesson-vocabs', vocabController.getComp
router.get('/courses/:courseId/dictionary', vocabController.getCourseDictionary);
router.get('/courses/:courseId/distractor-pool', vocabController.getVocabDistractorPool);
router.get('/courses/:courseId/srs/due', vocabController.getCourseSrsDue);
+router.get('/courses/:courseId/srs/hard', vocabController.getCourseHardSrsItems);
router.get('/courses/:courseId', vocabController.getCourse);
router.put('/courses/:courseId', vocabController.updateCourse);
router.delete('/courses/:courseId', vocabController.deleteCourse);
router.post('/srs/review', vocabController.reviewSrsItem);
+router.patch('/srs/hard', vocabController.clearSrsItemHardStatus);
// Lessons
router.post('/courses/:courseId/lessons', vocabController.addLessonToCourse);
diff --git a/backend/services/vocabService.js b/backend/services/vocabService.js
index 41ec97b..6eb3dc6 100755
--- a/backend/services/vocabService.js
+++ b/backend/services/vocabService.js
@@ -15,6 +15,8 @@ import { notifyUser } from '../utils/socket.js';
import { Op } from 'sequelize';
import { BISAYA_PHASE1_DIDACTICS, BISAYA_DIDACTICS_FRAGMENTS } from '../scripts/bisaya-course-phase1.js';
+const DAILY_SRS_LIMIT = 50;
+
export default class VocabService {
_stripGermanNumberSeparators(value) {
return String(value || '').replace(/[\s.-]+/g, '');
@@ -202,35 +204,23 @@ export default class VocabService {
return (leftLooksShortFragment && rightLooksSentence) || (rightLooksShortFragment && leftLooksSentence);
}
- _calculateSrsSchedule(item, { correct, rating = null } = {}) {
+ _calculateSrsSchedule(item, { correct } = {}) {
const now = new Date();
- const previousStage = Math.max(0, Number(item?.stage) || 0);
- const previousInterval = Math.max(0, Number(item?.intervalDays) || 0);
- const normalizedRating = String(rating || '').toLowerCase();
- const isCorrect = Boolean(correct) && normalizedRating !== 'again';
- if (!isCorrect) {
+ if (!correct) {
return {
- stage: Math.max(0, previousStage - 1),
+ stage: 0,
intervalDays: 0,
nextDueAt: new Date(now.getTime() + 10 * 60 * 1000),
lapseDelta: 1
};
}
- // Neue einfache Policy:
- // - 'easy' -> 7 Tage
- // - 'good'/'normal' -> 4 Tage
- // - 'hard' -> 1 Tag
- // Außerdem: nextDueAt darf nicht mehr am gleichen Kalendertag liegen.
- let intervalDays;
- if (normalizedRating === 'easy') {
- intervalDays = 7;
- } else if (normalizedRating === 'hard') {
- intervalDays = 1;
- } else {
- // default / 'good' / unspecified
- intervalDays = 4;
- }
+ // Intervals are based only on successful answers in the daily SRS review.
+ // There is deliberately no user-selected difficulty rating: each successful
+ // recall moves the term to the next, less frequent interval.
+ const intervals = [1, 3, 7, 14, 30, 60, 120];
+ const previousCorrectCount = Math.max(0, Number(item?.correctCount) || 0);
+ const intervalDays = intervals[Math.min(previousCorrectCount, intervals.length - 1)];
// Bestimme nextDueAt als Start des Tages (00:00) nach intervalDays
const nextDueAt = new Date(now);
@@ -239,8 +229,7 @@ export default class VocabService {
// Gehe vorwärts: morgen + (intervalDays - 1)
nextDueAt.setDate(nextDueAt.getDate() + 1 + Math.max(0, intervalDays - 1));
- // Stage-Logik: einfache Fortschrittsstufe basierend auf intervalDays
- let nextStage = Math.min(8, Math.max(0, Math.floor(Math.log2(intervalDays + 1))));
+ const nextStage = Math.min(intervals.length, previousCorrectCount + 1);
return {
stage: nextStage,
@@ -2012,7 +2001,10 @@ export default class VocabService {
throw err;
}
- const limit = this._clampInteger(query?.limit, { min: 1, max: 100, fallback: 30 });
+ // A daily SRS batch is intentionally capped. Excess due items remain due
+ // and are shown on the following day rather than silently overloading today.
+ const requestedLimit = this._clampInteger(query?.limit, { min: 1, max: DAILY_SRS_LIMIT, fallback: DAILY_SRS_LIMIT });
+ const limit = Math.min(DAILY_SRS_LIMIT, requestedLimit);
const now = new Date();
const dueWhere = {
userId: user.id,
@@ -2052,6 +2044,7 @@ export default class VocabService {
dueAt: now.toISOString(),
count: rows.length,
totalDueCount,
+ dailyLimit: DAILY_SRS_LIMIT,
limit,
items: rows.map((item) => ({
itemKey: item.itemKey,
@@ -2066,7 +2059,42 @@ export default class VocabService {
nextDueAt: this._normalizeIsoDate(item.nextDueAt),
correctCount: item.correctCount,
wrongCount: item.wrongCount,
- lapseCount: item.lapseCount
+ lapseCount: item.lapseCount,
+ isHard: item.isHard
+ }))
+ };
+ }
+
+ async getCourseHardSrsItems(hashedUserId, courseId) {
+ const user = await this._getUserByHashedId(hashedUserId);
+ const course = await VocabCourse.findByPk(courseId);
+ if (!course) {
+ const err = new Error('Course not found');
+ err.status = 404;
+ throw err;
+ }
+ if (course.ownerUserId !== user.id && !course.isPublic) {
+ const err = new Error('Access denied');
+ err.status = 403;
+ throw err;
+ }
+
+ const items = await VocabSrsItem.findAll({
+ where: { userId: user.id, courseId: Number(course.id), isHard: true },
+ order: [['nextDueAt', 'ASC'], ['wrongCount', 'DESC']]
+ });
+ return {
+ courseId: course.id,
+ items: items.filter((item) => this._isTrainableSrsPair(item)).map((item) => ({
+ itemKey: item.itemKey,
+ courseId: item.courseId,
+ lessonId: item.lessonId,
+ learning: item.learning,
+ reference: item.reference,
+ direction: item.direction,
+ correctCount: item.correctCount,
+ wrongCount: item.wrongCount,
+ isHard: true
}))
};
}
@@ -2143,8 +2171,7 @@ export default class VocabService {
const correct = Boolean(payload?.correct);
const schedule = this._calculateSrsSchedule(item, {
- correct,
- rating: payload?.rating
+ correct
});
item.stage = schedule.stage;
@@ -2156,6 +2183,7 @@ export default class VocabService {
} else {
item.wrongCount += 1;
item.lapseCount += schedule.lapseDelta;
+ item.isHard = true;
}
await item.save();
@@ -2180,10 +2208,31 @@ export default class VocabService {
nextDueAt: this._normalizeIsoDate(item.nextDueAt),
correctCount: item.correctCount,
wrongCount: item.wrongCount,
- lapseCount: item.lapseCount
+ lapseCount: item.lapseCount,
+ isHard: item.isHard
};
}
+ async clearSrsItemHardStatus(hashedUserId, payload = {}) {
+ const user = await this._getUserByHashedId(hashedUserId);
+ const courseId = this._clampInteger(payload?.courseId, { min: 1, max: 1_000_000, fallback: 0 });
+ const itemKey = this._sanitizeShortString(payload?.itemKey, 80);
+ if (!courseId || !itemKey) {
+ const err = new Error('Missing SRS item');
+ err.status = 400;
+ throw err;
+ }
+ const item = await VocabSrsItem.findOne({ where: { userId: user.id, courseId, itemKey } });
+ if (!item) {
+ const err = new Error('SRS item not found');
+ err.status = 404;
+ throw err;
+ }
+ item.isHard = false;
+ await item.save();
+ return { itemKey: item.itemKey, isHard: false };
+ }
+
async searchVocabs(hashedUserId, languageId, { q = '', learning = '', motherTongue = '' } = {}) {
const user = await this._getUserByHashedId(hashedUserId);
const access = await this._getLanguageAccess(user.id, languageId);
diff --git a/frontend/src/dialogues/socialnetwork/VocabPracticeDialog.vue b/frontend/src/dialogues/socialnetwork/VocabPracticeDialog.vue
index d5c6424..bf6a9e3 100755
--- a/frontend/src/dialogues/socialnetwork/VocabPracticeDialog.vue
+++ b/frontend/src/dialogues/socialnetwork/VocabPracticeDialog.vue
@@ -45,22 +45,6 @@
-
-
{{ $t('socialnetwork.vocab.practice.srsRateTitle') }}
-
-
-
-
-
@@ -162,7 +140,6 @@ const PRACTICE_MIN_EXPOSURES = 3;
const SRS_SESSION_STORAGE_VERSION = 2;
const HARD_REQUIRED_CONSECUTIVE_CORRECT = 5;
const MAX_DAILY_DUE = 50;
-const MAX_DAILY_HARD_VOCABS = 7;
export default {
name: 'VocabPracticeDialog',
@@ -171,7 +148,6 @@ export default {
return {
openParams: null, // { languageId, chapterId, lessonId, courseId }
onClose: null,
- onDailyHardLimitReached: null,
loading: false,
allVocabs: false,
srsMode: false,
@@ -258,9 +234,6 @@ export default {
showSkipButton() {
return !this.answered;
},
- showSrsRatingButtons() {
- return this.srsMode && this.answered && !this.locked;
- },
visibleCorrectAnswers() {
const answers = Array.isArray(this.acceptableAnswers) ? this.acceptableAnswers.filter(Boolean) : [];
if (answers.length > 0) {
@@ -272,37 +245,6 @@ export default {
const fallback = this.direction === 'L2R' ? this.current.reference : this.current.learning;
return this.expandAnswerVariants(fallback);
},
- srsRatingOptions() {
- if (!this.answered) {
- return [];
- }
- if (!this.lastCorrect) {
- return [
- {
- value: 'again',
- label: this.$t('socialnetwork.vocab.practice.srsAgain'),
- hint: this.$t('socialnetwork.vocab.practice.srsAgainHint')
- }
- ];
- }
- return [
- {
- value: 'hard',
- label: this.$t('socialnetwork.vocab.practice.srsHard'),
- hint: this.$t('socialnetwork.vocab.practice.srsHardHint')
- },
- {
- value: 'good',
- label: this.$t('socialnetwork.vocab.practice.srsGood'),
- hint: this.$t('socialnetwork.vocab.practice.srsGoodHint')
- },
- {
- value: 'easy',
- label: this.$t('socialnetwork.vocab.practice.srsEasy'),
- hint: this.$t('socialnetwork.vocab.practice.srsEasyHint')
- }
- ];
- },
srsTotalDue() {
return Number(this.srsSession?.initialTotalDue || 0) || 0;
},
@@ -446,21 +388,6 @@ export default {
this.hardMasteryByKey[key] = 0;
}
this.saveHardVocabMap();
- if (this.srsMode) {
- this.maybeStartDailyHardPractice();
- }
- },
- maybeStartDailyHardPractice() {
- if (this.hardCount < MAX_DAILY_HARD_VOCABS) return;
- const callback = this.onDailyHardLimitReached;
- this.close();
- this.$nextTick(() => {
- try {
- callback?.();
- } catch (_) {
- // A missing parent callback must not interrupt the dialog close.
- }
- });
},
unmarkCurrentAsHard() {
if (!this.current) return;
@@ -604,7 +531,7 @@ export default {
}
this.saveSrsSession();
},
- open({ languageId, chapterId, lessonId, courseId, initialPool = null, srsMode = false, closeOnHardCompletion = false, onDailyHardLimitReached = null, onClose = null }) {
+ open({ languageId, chapterId, lessonId, courseId, initialPool = null, initialHardPool = null, srsMode = false, closeOnHardCompletion = false, onClose = null }) {
if (this.autoAdvanceTimer) {
clearTimeout(this.autoAdvanceTimer);
this.autoAdvanceTimer = null;
@@ -615,7 +542,6 @@ export default {
console.debug('[VocabPracticeDialog] open called with', { languageId, chapterId, lessonId, courseId, srsMode, hasInitialPool: Array.isArray(initialPool) });
} catch (_) {}
this.onClose = typeof onClose === 'function' ? onClose : null;
- this.onDailyHardLimitReached = typeof onDailyHardLimitReached === 'function' ? onDailyHardLimitReached : null;
this.srsMode = Boolean(srsMode);
this.initialPool = Array.isArray(initialPool) ? initialPool : null;
this.closeOnHardCompletion = Boolean(closeOnHardCompletion);
@@ -639,9 +565,15 @@ export default {
this.$refs.dialog.open();
this.$nextTick(() => {
document.addEventListener('keydown', this.handleKeyDown);
- if (this.srsMode) this.maybeStartDailyHardPractice();
});
this.loadHardVocabMap();
+ if (Array.isArray(initialHardPool)) {
+ initialHardPool.forEach((item) => {
+ const key = this.getHardKey(item);
+ if (!key || !item?.learning || !item?.reference) return;
+ this.hardVocabMap[key] = { learning: item.learning, reference: item.reference, itemKey: item.itemKey || item.id || null };
+ });
+ }
this.reloadPool();
},
close() {
@@ -1198,7 +1130,7 @@ export default {
// ignore autoplay issues
}
},
- reportSrsReview(isCorrect, rating = null) {
+ reportSrsReview(isCorrect) {
if (!this.current || !this.openParams?.courseId) {
return Promise.resolve();
}
@@ -1209,8 +1141,7 @@ export default {
learning: this.current.learning,
reference: this.current.reference,
direction: this.direction,
- correct: Boolean(isCorrect),
- rating
+ correct: Boolean(isCorrect)
}).catch((error) => {
console.warn('[VocabPracticeDialog] SRS review could not be saved:', error);
});
@@ -1239,9 +1170,7 @@ export default {
};
}
}
- if (!this.srsMode) {
- this.reportSrsReview(isCorrect);
- }
+ if (this.srsMode) this.finishSrsAnswer(isCorrect);
const id = this.current?.id;
if (!id) return;
@@ -1259,6 +1188,7 @@ export default {
this.hardMasteryByKey[key] = Math.max(0, Number(this.hardMasteryByKey[key]) || 0) + 1;
const mapKey = this.findMatchingHardKey(this.current);
if ((Number(this.hardMasteryByKey[key]) || 0) >= HARD_REQUIRED_CONSECUTIVE_CORRECT && mapKey) {
+ this.clearSrsHardStatus(this.current);
const next = { ...this.hardVocabMap };
delete next[mapKey];
this.hardVocabMap = next;
@@ -1282,87 +1212,38 @@ export default {
this.lastIds.unshift(id);
this.lastIds = this.lastIds.slice(0, 3);
},
- async submitSrsRating(rating) {
- if (!this.srsMode || !this.answered || this.locked) {
- return;
- }
+ clearSrsHardStatus(item) {
+ const itemKey = String(item?.itemKey || item?.id || '').trim();
+ if (!itemKey || !this.openParams?.courseId) return;
+ apiClient.patch('/api/vocab/srs/hard', {
+ courseId: this.openParams.courseId,
+ itemKey
+ }).catch((error) => {
+ console.warn('[VocabPracticeDialog] hard status could not be cleared:', error);
+ });
+ },
+ async finishSrsAnswer(isCorrect) {
+ if (!this.srsMode || !this.answered || this.locked) return;
const id = this.current?.id;
- const treatAsAgain = rating === 'again' || !this.lastCorrect;
- const moveToHardPractice = treatAsAgain && this.isItemMarkedHard(this.current);
- const retryPendingIds = Array.isArray(this.srsSession?.retryPendingIds)
- ? this.srsSession.retryPendingIds
- : [];
- const needsFinalRepeat = this.lastCorrect && retryPendingIds.includes(id);
-
this.locked = true;
- // The immediate correction after a wrong answer is a rehearsal, not the
- // SRS result. Only the later repetition at the end is recorded as right.
- if (!needsFinalRepeat) {
- await this.reportSrsReview(this.lastCorrect, rating);
- }
+ if (!isCorrect) this.markCurrentAsHard();
+ await this.reportSrsReview(isCorrect);
if (id && this.srsSession) {
- if (treatAsAgain && !moveToHardPractice) {
- // Keep the card at the front of the queue. After the solution has
- // been shown, the user must type it correctly before moving on.
- if (Array.isArray(this.srsSession.doneIds)) {
- this.srsSession.doneIds = this.srsSession.doneIds.filter((x) => x !== id);
- }
- if (!retryPendingIds.includes(id)) {
- this.srsSession.retryPendingIds = [...retryPendingIds, id];
- }
- } else if (needsFinalRepeat) {
- // The correction was right, but the item must be recalled once more
- // after the rest of today's cards before it counts as complete.
- this.srsSession.retryPendingIds = retryPendingIds.filter((x) => x !== id);
- const finalReviewIds = Array.isArray(this.srsSession.finalReviewIds)
- ? this.srsSession.finalReviewIds
- : [];
- if (!finalReviewIds.includes(id)) {
- this.srsSession.finalReviewIds = [...finalReviewIds, id];
- }
- const remaining = Array.isArray(this.srsQueueIds)
- ? this.srsQueueIds.filter((x) => x !== id)
- : [];
- remaining.push(id);
- this.srsQueueIds = remaining;
- } else {
- // Hard cards leave the daily review after a wrong answer. They are
- // practiced separately through the hard-vocab drill.
- const done = Array.isArray(this.srsSession.doneIds) ? this.srsSession.doneIds : [];
- if (!done.includes(id)) {
- done.push(id);
- this.srsSession.doneIds = done;
- }
- this.srsSession.retryPendingIds = retryPendingIds.filter((x) => x !== id);
- this.srsSession.finalReviewIds = (Array.isArray(this.srsSession.finalReviewIds)
- ? this.srsSession.finalReviewIds
- : []).filter((x) => x !== id);
- if (Array.isArray(this.srsQueueIds) && this.srsQueueIds[0] === id) {
- this.srsQueueIds = this.srsQueueIds.slice(1);
- } else if (Array.isArray(this.srsQueueIds)) {
- this.srsQueueIds = this.srsQueueIds.filter((x) => x !== id);
- }
+ const done = Array.isArray(this.srsSession.doneIds) ? this.srsSession.doneIds : [];
+ if (!done.includes(id)) {
+ done.push(id);
+ this.srsSession.doneIds = done;
}
+ this.srsQueueIds = Array.isArray(this.srsQueueIds)
+ ? this.srsQueueIds.filter((x) => x !== id)
+ : [];
this.saveSrsSession();
}
-
- if (treatAsAgain && !moveToHardPractice) {
- const retryItem = this.pool.find((item) => item.id === id) || this.current;
- const retryDirection = this.direction;
- this.resetQuestion();
- this.current = retryItem;
- this.direction = retryDirection;
- const prompt = this.currentAnswerPrompt;
- this.acceptableAnswers = this.getAnswersForPrompt(prompt, this.direction);
- if (this.simpleMode) this.buildChoices();
- this.$nextTick(() => {
- if (!this.simpleMode) this.$refs.answerInput?.focus?.();
- });
- return;
- }
-
- this.next();
+ this.autoAdvanceTimer = setTimeout(() => {
+ this.autoAdvanceTimer = null;
+ this.next();
+ }, isCorrect ? 450 : 1300);
},
submitChoice(opt) {
if (this.locked) return;
diff --git a/frontend/src/i18n/locales/de/socialnetwork.json b/frontend/src/i18n/locales/de/socialnetwork.json
index a46e7fb..fdd5c33 100755
--- a/frontend/src/i18n/locales/de/socialnetwork.json
+++ b/frontend/src/i18n/locales/de/socialnetwork.json
@@ -870,10 +870,10 @@
"reviewTimeNow": "jetzt",
"reviewTimeTomorrow": "morgen",
"reviewTimeInDays": "in {count} Tagen",
- "srsDueStat": "SRS fällig: {count}",
+ "srsDueStat": "Tageswiederholung: {scheduled} von {total}",
"srsEyebrow": "Langzeitgedächtnis",
- "srsTitle": "{count} Begriffe sind heute fällig",
- "srsIntro": "Diese Wiederholung kommt aus dem SRS-Plan einzelner Begriffe. Sie hat Vorrang vor neuem Stoff, weil sie kurz vor dem Vergessen stabilisiert.",
+ "srsTitle": "Heute: {scheduled} von {total} fälligen Begriffen",
+ "srsIntro": "Diese Wiederholung kommt aus dem SRS-Plan einzelner Begriffe. Pro Tag werden höchstens 50 Begriffe eingeplant; weitere fällige Begriffe bleiben für die nächste Tageswiederholung erhalten.",
"srsStart": "Tageswiederholung starten",
"courseTodayPlanIntroSrs": "Didaktische Reihenfolge: Zuerst die fällige SRS-Tageswiederholung einzelner Begriffe. Danach kommen Kurz-Wiederholungen, Blockfortschritt und ggf. intensive Wiederholung. So wird altes Material stabilisiert, bevor neues Material dazukommt."
}
diff --git a/frontend/src/i18n/locales/en/socialnetwork.json b/frontend/src/i18n/locales/en/socialnetwork.json
index 349f605..935c5c1 100755
--- a/frontend/src/i18n/locales/en/socialnetwork.json
+++ b/frontend/src/i18n/locales/en/socialnetwork.json
@@ -870,10 +870,10 @@
"reviewTimeNow": "now",
"reviewTimeTomorrow": "tomorrow",
"reviewTimeInDays": "in {count} days",
- "srsDueStat": "SRS due: {count}",
+ "srsDueStat": "Daily review: {scheduled} of {total}",
"srsEyebrow": "Long-term memory",
- "srsTitle": "{count} terms are due today",
- "srsIntro": "This review comes from the SRS schedule of individual terms. It should come before new material because it stabilizes items close to forgetting.",
+ "srsTitle": "Today: {scheduled} of {total} due terms",
+ "srsIntro": "This review comes from the SRS schedule of individual terms. At most 50 terms are planned per day; any additional due terms remain for the next daily review.",
"srsStart": "Start daily review",
"courseTodayPlanIntroSrs": "Pedagogical order: start with the due SRS daily review for individual terms. Then quick reviews, block progress, and intensive review if needed. This stabilizes old material before new material is added."
}
diff --git a/frontend/src/views/social/VocabCourseView.vue b/frontend/src/views/social/VocabCourseView.vue
index fc28fed..c5c380f 100755
--- a/frontend/src/views/social/VocabCourseView.vue
+++ b/frontend/src/views/social/VocabCourseView.vue
@@ -46,13 +46,6 @@
{{ entry.learning }}
{{ entry.reference }}
-
@@ -85,7 +78,7 @@
{{ $t('socialnetwork.vocab.courses.courseFlowIntro') }}
- {{ $t('socialnetwork.vocab.courses.srsDueStat', { count: srsDueCount }) }}
+ {{ $t('socialnetwork.vocab.courses.srsDueStat', { scheduled: srsDailyCount, total: srsDueCount }) }}
{{ $t('socialnetwork.vocab.courses.courseFlowReviewStat', { count: dueReviewLessons.length }) }}
{{ $t('socialnetwork.vocab.courses.courseFlowBlockStat', { block: currentBlockNumber || '—' }) }}
@@ -94,7 +87,7 @@
{{ $t('socialnetwork.vocab.courses.srsEyebrow') }}
-
{{ $t('socialnetwork.vocab.courses.srsTitle', { count: srsDueCount }) }}
+
{{ $t('socialnetwork.vocab.courses.srsTitle', { scheduled: srsDailyCount, total: srsDueCount }) }}
{{ $t('socialnetwork.vocab.courses.srsIntro') }}
-
-
-
-