made vocabulary exercise just a bit more tolerant

This commit is contained in:
dvorakmortonxd 2026-09-11 22:08:45 +02:00
commit a57c4923fe
6 changed files with 75 additions and 11 deletions

View file

@ -155,6 +155,39 @@ gboolean answer_accepts(const char *sel_norm, const char *ans) {
return ok;
}
/* True when the typed answer matches the beginning of an accepted answer but
* is missing one or more trailing words (e.g. "jmenovat" for "jmenovat se").
* Used to warn about an incomplete answer before counting it wrong. */
gboolean answer_is_incomplete(const char *sel_norm, const char *ans) {
gchar **parts = g_strsplit(ans, "|", -1);
gboolean incomplete = FALSE;
for (int i = 0; parts[i] != NULL && !incomplete; i++) {
gchar *a = normalize_answer(parts[i]);
gchar **sel_w = g_strsplit(sel_norm, " ", -1);
gchar **ans_w = g_strsplit(a, " ", -1);
int nsel = g_strv_length(sel_w);
int nans = g_strv_length(ans_w);
int k;
if (sel_norm[0] && nsel > 0 && nsel < nans) {
incomplete = TRUE;
for (k = 0; k < nsel; k++) {
if (g_strcmp0(sel_w[k], ans_w[k]) != 0) {
incomplete = FALSE;
break;
}
}
}
g_strfreev(sel_w);
g_strfreev(ans_w);
g_free(a);
}
g_strfreev(parts);
return incomplete;
}
void combo_list_check(GtkButton *button, gpointer data) {
ComboListCtx *ctx = data;
guint total = ctx->combos->len;

View file

@ -1062,7 +1062,7 @@ const TypedQ trans_p13[] = {
{"das Alter", "věk|age", "věk"},
{"auch", "také|taky|also|too", "také"},
{"aus", "z|ze|from", "z"},
{"die Band, -s", "skupina|kapela|band", "(hudební) skupina"},
{"die Band, -s", "skupina|hudební skupina|kapela|hudební kapela|band", "(hudební) skupina"},
{"chatten (Infinitiv)", "chatovat|chatuje|to chat", "chatovat"},
{"das Computerspiel, -e", "počítačová hra|computer game", "počítačová hra"},
{"die/das E-Mail, -s", "e-mail|email", "e-mail"},
@ -1079,7 +1079,7 @@ const TypedQ trans_p13[] = {
"Je fotbalový fanatik."},
{"gehen (Infinitiv)", "jít|jde|chodit|to go", "jít"},
{"Ich gehe gern auf Konzerte.",
"rád chodím na koncerty|ráda chodím na koncerty|i like going to concerts",
"rád chodím na koncerty|ráda chodím na koncerty|chodím rád na koncerty|chodím ráda na koncerty|i like going to concerts",
"Rád(a) chodím na koncerty."},
{"gern", "rád|ráda|gladly", "rád/a"},
{"die Gitarre, -n", "kytara|guitar", "kytara"},
@ -1096,7 +1096,7 @@ const TypedQ trans_p13[] = {
{"kein/keine/kein", "žádný|žádná|žádné|no|none", "žádný/žádná/žádné"},
{"das Klavier, -e", "klavír|piano", "klavír"},
{"kommen (Infinitiv, aus)",
"přicházet|přijít|přichází|pochází|to come|to come from",
"přicházet|přijít|přichází|pocházet|pochází|to come|to come from",
"přicházet, pocházet (z)"},
{"Er kommt aus Wien.",
"pochází z vídně|he comes from vienna|he is from vienna",
@ -1119,30 +1119,30 @@ const TypedQ trans_p13[] = {
};
const TypedQ trans_p14[] = {
{"falsch", "nesprávně|chybně|špatně|wrong|incorrect", "nesprávně, chybně"},
{"falsch", "nesprávně|chybně|špatně|nepravda|wrong|incorrect", "nesprávně, chybně"},
{"die Flöte, -n", "flétna|flute", "flétna"},
{"haben (Infinitiv)", "mít|má|to have", "mít"},
{"das Instrument, -e", "nástroj|instrument", "(hudební) nástroj"},
{"das Instrument, -e", "nástroj|hudební nástroj|instrument", "(hudební) nástroj"},
{"jetzt", "teď|nyní|now", "teď"},
{"machen (Infinitiv)", "dělat|dělá|to do|to make", "dělat"},
{"richtig", "správně|správný|correct|right", "správně"},
{"der Sport", "sport", "sport"},
{"Sport treiben (Infinitiv)",
"sportovat|sportuje|provozovat sport|to do sport", "sportovat"},
"sportovat|sportuje|provozovat sport|dělat sport|to do sport", "sportovat"},
{"das Squash", "squash", "squash"},
{"das/der Yoga", "jóga|yoga", "jóga"},
};
const TypedQ trans_p15[] = {
{"bald", "brzy|soon", "brzy"},
{"Bis bald!", "brzy na shledanou|brzy se uvidíme|see you soon",
{"bald", "brzy|brzo|soon", "brzy"},
{"Bis bald!", "brzy na shledanou|brzo na shledanou|brzy se uvidíme|brzo se uvidíme|see you soon",
"Brzy na shledanou!"},
{"bis", "do|až|until", "do"},
{"Bis zum nächsten Mal!", "do příštího setkání|do příště|until next time",
"Do příštího setkání!"},
{"die Frau, -en", "paní|žena|woman|mrs", "paní"},
{"die Frau, -en", "paní|žena|manželka|woman|mrs|wife", "paní"},
{"der Herr, -en", "pan|muž|man|mr", "pan"},
{"Mach's gut!", "měj se hezky|měj se|take care", "Měj se hezky!"},
{"Mach's gut!", "měj se hezky|měj se|mějte se hezky|mějte se|take care", "Měj se hezky!"},
{"das Museum, Museen", "muzeum|museum", "muzeum"},
{"Sie gehen ins Museum.",
"jdou do muzea|they are going to the museum|they go to the museum",
@ -1186,6 +1186,7 @@ typedef struct {
int done;
int *order; /* queue of word indices, order[0] = current */
int qn; /* words still in the queue */
gboolean warned; /* current word already got an "incomplete" warning */
} TransCtx;
/* Canonical translation of a word, in the current UI language. */
@ -1203,11 +1204,13 @@ static void trans_update_progress(TransCtx *ctx) {
}
static void trans_show(TransCtx *ctx) {
ctx->warned = FALSE;
if (ctx->qn <= 0) {
gtk_label_set_text(GTK_LABEL(ctx->header), "");
gtk_label_set_text(GTK_LABEL(ctx->prompt), tr("trans_done"));
gtk_widget_set_visible(ctx->entry, FALSE);
gtk_widget_set_sensitive(ctx->check, FALSE);
gtk_widget_set_visible(ctx->check, FALSE);
set_feedback(ctx->feedback, TRUE, tr("feedback_ok"));
trans_update_progress(ctx);
mark_done(ctx->unit, ctx->ex_num);
@ -1255,6 +1258,16 @@ static void translate_check(GtkButton *button, gpointer data) {
txt = gtk_editable_get_text(GTK_EDITABLE(ctx->entry));
norm = normalize_answer(txt ? txt : "");
good = txt && txt[0] && answer_accepts(norm, q->answers);
/* Nearly there (e.g. "jmenovat" for "jmenovat se"): warn in yellow and let
* the student finish the answer. A second Check on the same word counts. */
if (!good && !ctx->warned && txt && txt[0] &&
answer_is_incomplete(norm, q->answers)) {
g_free(norm);
ctx->warned = TRUE;
set_feedback_warn(ctx->feedback, tr("trans_incomplete"));
return;
}
g_free(norm);
/* Pop the current word off the front of the queue. */

1
i18n.c
View file

@ -80,6 +80,7 @@ const TrEntry tr_ui[] = {
"Přeložte slovo. Špatná odpověď se vrátí na konec.",
"Translate the word. Wrong answers come back at the end."},
{"trans_wrong", "Špatně. Správně: %s", "Wrong. Correct: %s"},
{"trans_incomplete", "Nepřesné – něco chybí.", "Almost – something is missing."},
{"trans_progress", "Hotovo: %d / %d", "Done: %d / %d"},
{"trans_done", "Hotovo! Zvládl(a) jsi všechna slova.",
"Done! You mastered all the words."},

View file

@ -743,6 +743,7 @@ void shuffle_indices(int *arr, int n);
void flow_clear(GtkFlowBox *fb);
void answer_mark(GtkWidget *widget, gboolean ok);
void set_feedback(GtkWidget *label, gboolean ok, const char *text);
void set_feedback_warn(GtkWidget *label, const char *text);
gboolean text_has_german_umlaut(const char *s);
void add_umlaut_note(GtkWidget *body);
void unit_save_progress(UnitCtx *u);
@ -852,6 +853,7 @@ void combo_list_add(ComboListCtx *ctx, GtkWidget *combo, const char *ans);
void combo_list_add_trans(ComboListCtx *ctx, GtkWidget *label);
GtkWidget *make_word_combo(const char **words, int n);
gboolean answer_accepts(const char *sel_norm, const char *ans);
gboolean answer_is_incomplete(const char *sel_norm, const char *ans);
void combo_list_check(GtkButton *button, gpointer data);
void sent_rebuild(SentBuilder *sb);
GdkContentProvider *asm_drag_prepare(GtkDragSource *source,

View file

@ -610,6 +610,11 @@ entry.answer-wrong {
font-size: 15px;
font-weight: 700;
}
.feedback-warn {
color: @warning;
font-size: 15px;
font-weight: 700;
}
.stats-page {
padding: 8px 28px 24px 28px;
}

10
util.c
View file

@ -92,10 +92,20 @@ void answer_mark(GtkWidget *widget, gboolean ok) {
void set_feedback(GtkWidget *label, gboolean ok, const char *text) {
gtk_widget_remove_css_class(label, "feedback-ok");
gtk_widget_remove_css_class(label, "feedback-err");
gtk_widget_remove_css_class(label, "feedback-warn");
gtk_widget_add_css_class(label, ok ? "feedback-ok" : "feedback-err");
gtk_label_set_text(GTK_LABEL(label), text);
}
/* Yellow "almost there" note: close to a correct answer but incomplete. */
void set_feedback_warn(GtkWidget *label, const char *text) {
gtk_widget_remove_css_class(label, "feedback-ok");
gtk_widget_remove_css_class(label, "feedback-err");
gtk_widget_remove_css_class(label, "feedback-warn");
gtk_widget_add_css_class(label, "feedback-warn");
gtk_label_set_text(GTK_LABEL(label), text);
}
/* True if `s` contains any German special letter (ä, ö, ü or ß). */
gboolean text_has_german_umlaut(const char *s) {
const char *p = s ? s : "";