feat(localization): add difficult vocabulary marking features in multiple languages
All checks were successful
Deploy to production / deploy (push) Successful in 1m58s

- Introduced new localization strings for marking vocabulary as difficult, including options to save and remove difficult marks.
- Enhanced user experience by providing feedback on the status of difficult vocabulary in Cebuano, Spanish, and French.
- Updated existing localization files to ensure consistency across languages, supporting the recent vocabulary management features.
This commit is contained in:
Torsten Schulz (local)
2026-04-22 11:12:26 +02:00
parent 44b40d5a46
commit d3aad6e7ef
4 changed files with 133 additions and 3 deletions

View File

@@ -199,6 +199,9 @@ export default {
locked: false,
autoAdvanceTimer: null,
hardVocabMap: {}, // { [normalizedPairKey]: { learning, reference, markedAt } }
hardPhaseActive: false,
hardMasteryByKey: {}, // { [hardKey]: consecutiveCorrect }
cycleAskedIds: [],
};
},
computed: {
@@ -294,6 +297,17 @@ export default {
isCurrentMarkedHard() {
if (!this.current) return false;
return Boolean(this.hardVocabMap[this.getHardKey(this.current)]);
},
hardPoolItems() {
if (!Array.isArray(this.pool) || this.pool.length === 0) return [];
return this.pool.filter((item) => Boolean(this.hardVocabMap[this.getHardKey(item)]));
},
hardRemainingCount() {
const requiredConsecutiveCorrect = 2;
return this.hardPoolItems.filter((item) => {
const key = this.getHardKey(item);
return (Number(this.hardMasteryByKey[key]) || 0) < requiredConsecutiveCorrect;
}).length;
}
},
methods: {
@@ -341,6 +355,9 @@ export default {
markedAt: new Date().toISOString()
}
};
if (this.hardMasteryByKey[key] == null) {
this.hardMasteryByKey[key] = 0;
}
this.saveHardVocabMap();
},
unmarkCurrentAsHard() {
@@ -350,8 +367,33 @@ export default {
const next = { ...this.hardVocabMap };
delete next[key];
this.hardVocabMap = next;
if (this.hardMasteryByKey[key] != null) {
const nextMastery = { ...this.hardMasteryByKey };
delete nextMastery[key];
this.hardMasteryByKey = nextMastery;
}
if (this.hardPhaseActive && this.hardRemainingCount <= 0) {
this.hardPhaseActive = false;
}
this.saveHardVocabMap();
},
addCurrentToCycle() {
if (!this.current?.id) return;
if (this.cycleAskedIds.includes(this.current.id)) return;
this.cycleAskedIds = [...this.cycleAskedIds, this.current.id];
},
maybeStartHardPhase() {
if (this.srsMode || this.hardPhaseActive) return;
if (!this.pool.length || !this.hardPoolItems.length) return;
if (this.cycleAskedIds.length < this.pool.length) return;
this.hardPhaseActive = true;
},
maybeFinishHardPhase() {
if (!this.hardPhaseActive) return;
if (this.hardRemainingCount > 0) return;
this.hardPhaseActive = false;
this.cycleAskedIds = [];
},
getLocalDateKey() {
const d = new Date();
const y = d.getFullYear();
@@ -455,14 +497,17 @@ export default {
this.pendingRetry = null;
this.pool = [];
this.hardVocabMap = {};
this.hardPhaseActive = false;
this.hardMasteryByKey = {};
this.cycleAskedIds = [];
this.locked = false;
this.resetQuestion();
this.$refs.dialog.open();
this.$nextTick(() => {
document.addEventListener('keydown', this.handleKeyDown);
});
this.reloadPool();
this.loadHardVocabMap();
this.reloadPool();
},
close() {
if (this.autoAdvanceTimer) {
@@ -736,6 +781,36 @@ export default {
if (!nextId) return null;
return items.find((it) => it.id === nextId) || null;
}
if (this.hardPhaseActive) {
const requiredConsecutiveCorrect = 2;
const hardItems = this.hardPoolItems.filter((item) => {
const key = this.getHardKey(item);
return (Number(this.hardMasteryByKey[key]) || 0) < requiredConsecutiveCorrect;
});
if (hardItems.length === 0) {
this.hardPhaseActive = false;
return null;
}
const rankedHard = hardItems
.map((item) => {
const key = this.getHardKey(item);
const mastery = Number(this.hardMasteryByKey[key]) || 0;
const st = this.perId[item.id] || { c: 0, w: 0, streak: 0, lastAsked: 0 };
return {
item,
mastery,
wrong: Number(st.w) || 0,
attempts: (Number(st.c) || 0) + (Number(st.w) || 0),
};
})
.sort((a, b) => {
if (a.mastery !== b.mastery) return a.mastery - b.mastery;
if (a.wrong !== b.wrong) return b.wrong - a.wrong;
if (a.attempts !== b.attempts) return a.attempts - b.attempts;
return Math.random() - 0.5;
});
return rankedHard[0]?.item || hardItems[Math.floor(Math.random() * hardItems.length)];
}
if (this.pendingRetry?.id) {
const retryItem = items.find((it) => it.id === this.pendingRetry.id);
if (retryItem) {
@@ -861,9 +936,23 @@ export default {
if (isCorrect) {
st.c += 1;
st.streak = st.streak >= 0 ? st.streak + 1 : 1;
if (this.hardPhaseActive && this.current) {
const key = this.getHardKey(this.current);
this.hardMasteryByKey[key] = Math.max(0, Number(this.hardMasteryByKey[key]) || 0) + 1;
if ((Number(this.hardMasteryByKey[key]) || 0) >= 2 && this.hardVocabMap[key]) {
const next = { ...this.hardVocabMap };
delete next[key];
this.hardVocabMap = next;
this.saveHardVocabMap();
}
}
} else {
st.w += 1;
st.streak = st.streak <= 0 ? st.streak - 1 : -1;
if (this.hardPhaseActive && this.current) {
const key = this.getHardKey(this.current);
this.hardMasteryByKey[key] = 0;
}
}
st.lastAsked = Date.now();
this.perId[id] = st;
@@ -943,12 +1032,23 @@ export default {
this.resetQuestion();
return;
}
if (this.current?.id) {
this.addCurrentToCycle();
}
this.maybeStartHardPhase();
this.maybeFinishHardPhase();
const retryDirection = this.pendingRetry?.direction || null;
this.resetQuestion();
if (retryDirection) {
this.direction = retryDirection;
}
this.current = this.pickNextItem();
if (!this.current) {
this.maybeFinishHardPhase();
if (!this.hardPhaseActive) {
this.current = this.pickNextItem();
}
}
if (!this.current) return;
const prompt = this.currentPrompt;
this.acceptableAnswers = this.getAnswersForPrompt(prompt, this.direction);