fix(curator): contractions are words of their own

Wiktionary tags a contraction as an abbreviation and an alternative of
its first word (im: in + dem), so German's im, zum and ins, which every
subtitle has, were neither entries nor found as forms. A contraction is
now an entry of its own, as text writes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-14 08:02:54 +02:00
commit 8f4131a138
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
4 changed files with 22 additions and 7 deletions

View file

@ -178,10 +178,15 @@ Future<List<File>> wiktionaryDictionaries(
continue;
}
final senses = _maps(json['senses']);
// A contraction (im, of in and dem; don't) is a word of its own,
// which text writes as it is.
bool contraction(Map<String, Object?> sense) =>
_strings(sense['tags']).contains('contraction');
// Senses saying the word is a form of another ("dative plural of
// Haus") make it a key of that one.
for (final sense in senses) {
if (_strings(sense['tags']).any(_shortForms.contains)) {
if (_strings(sense['tags']).any(_shortForms.contains) ||
contraction(sense)) {
continue;
}
for (final target in [
@ -195,8 +200,9 @@ Future<List<File>> wiktionaryDictionaries(
}
final own = [
for (final sense in senses)
if (_maps(sense['form_of']).isEmpty &&
_maps(sense['alt_of']).isEmpty &&
if ((contraction(sense) ||
_maps(sense['form_of']).isEmpty &&
_maps(sense['alt_of']).isEmpty) &&
_strings(sense['glosses']).isNotEmpty)
sense,
];

View file

@ -30,9 +30,9 @@ declarations of the entities they use: JMdict is © the Electronic Dictionary
Research and Development Group, used under its licence, CC BY-SA 4.0
(<https://www.edrdg.org/edrdg/licence.html>).
`wiktionary-sample.jsonl` holds 29 lines of kaikki.org's Wiktextract data of the
`wiktionary-sample.jsonl` holds 30 lines of kaikki.org's Wiktextract data of the
English Wiktionary (the dump of 2026-10-03, read per word from kaikki.org): the
German Haus, Häuser, Häusern, essen, aß, gegessen and Straße, the Greek σπίτι,
German Haus, Häuser, Häusern, essen, aß, gegessen, Straße and im, the Greek σπίτι,
σπιτιού and γράφω, the English house, houses, go and went, the French maison,
and the Korean 먹다, 집 and 학생, each shortened to the fields the curator reads (no categories,
etymologies or related words; at most six senses, two examples, 40 forms, two

View file

@ -27,3 +27,4 @@
{"senses": [{"examples": [{"text": "집에 돌아오다", "bold_text_offsets": [[0, 1]], "roman": "jib-e doraoda", "bold_roman_offsets": [[0, 3]], "translation": "to return home", "english": "to return home", "bold_translation_offsets": [[10, 14]], "type": "example"}, {"text": "집에 온 이후부터", "bold_text_offsets": [[0, 1]], "roman": "jib-e on ihubuteo", "bold_roman_offsets": [[0, 3]], "translation": "since I got home", "english": "since I got home", "bold_translation_offsets": [[12, 16]], "type": "example"}], "glosses": ["home, house"]}, {"synonyms": [{"word": "댁", "alt": "宅"}], "glosses": ["household"]}, {"glosses": ["sheath, case"]}], "pos": "noun", "head_templates": [{"name": "ko-noun", "args": {}, "expansion": "집 • (jip)"}], "forms": [{"form": "jip", "tags": ["romanization"]}], "etymology_number": "1", "sounds": [{"tags": ["SK-Standard", "Seoul"], "ipa": "[t͡ɕip̚]"}], "word": "집", "lang": "Korean", "lang_code": "ko"}
{"senses": [{"examples": [{"text": "작품(作品) (jakpum, “works”) + 집(集) (jip, “collection”) → 작품집(作品集) (jakpumjip, “collected works”)", "type": "example"}], "glosses": ["collection of bound works; anthology, collected works"], "tags": ["morpheme"]}], "pos": "suffix", "head_templates": [{"name": "ko-suffix", "args": {"hanja": "集"}, "expansion": "—집 • (-jip) (hanja 集)"}], "forms": [{"form": "—집", "tags": ["canonical"]}, {"form": "-jip", "tags": ["romanization"]}, {"form": "集", "tags": ["hanja"]}], "etymology_number": "2", "word": "집", "lang": "Korean", "lang_code": "ko"}
{"senses": [{"examples": [{"text": "최근에 박사 과정을 시작했는데 다시 학생이 돼서 기분이 좋습니다.", "bold_text_offsets": [[20, 23]], "roman": "Choegeun-e baksa gwajeong-eul sijakhaenneunde dasi haksaeng-i dwaeseo gibun-i joseumnida.", "bold_roman_offsets": [[51, 61]], "translation": "I recently started a PhD program, and it feels good to be a student again.", "english": "I recently started a PhD program, and it feels good to be a student again.", "bold_translation_offsets": [[60, 67]], "type": "example"}], "glosses": ["student, scholar, school pupil"]}], "pos": "noun", "head_templates": [{"name": "ko-noun", "args": {"hanja": "學生", "count": "명(名)"}, "expansion": "학생 • (haksaeng) (counter 명(名), hanja 學生)"}], "forms": [{"form": "haksaeng", "tags": ["romanization"]}, {"form": "명", "tags": ["counter", "error-unknown-tag"]}, {"form": "學生", "tags": ["hanja"]}], "sounds": [{"tags": ["SK-Standard", "Seoul"], "ipa": "[ha̠ks͈ɛŋ]"}, {"tags": ["SK-Standard", "Seoul"], "ipa": "[ha̠ks͈e̞ŋ]"}], "word": "학생", "lang": "Korean", "lang_code": "ko"}
{"senses": [{"glosses": ["contraction of in + dem; in the"], "tags": ["abbreviation", "alt-of", "contraction"], "alt_of": [{"word": "in", "extra": "+ dem; in the"}]}], "pos": "contraction", "head_templates": [{"name": "head", "args": {"1": "de", "2": "contraction"}, "expansion": "im"}], "forms": [{"form": "no-table-tags", "source": "declension", "tags": ["table-tags"]}, {"form": "l", "source": "declension", "tags": ["inflection-template"]}, {"form": "in den", "tags": ["accusative", "masculine"], "source": "declension"}, {"form": "ins", "tags": ["accusative", "neuter"], "source": "declension"}, {"form": "in die", "tags": ["accusative", "feminine"], "source": "declension"}, {"form": "in die", "tags": ["accusative", "plural"], "source": "declension"}, {"form": "im", "tags": ["dative", "masculine"], "source": "declension"}, {"form": "im", "tags": ["dative", "neuter"], "source": "declension"}, {"form": "in der", "tags": ["dative", "feminine"], "source": "declension"}, {"form": "in den", "tags": ["dative", "plural"], "source": "declension"}], "sounds": [{"ipa": "/ɪm/"}, {"audio": "De-im.ogg", "ogg_url": "https://upload.wikimedia.org/wikipedia/commons/7/71/De-im.ogg", "mp3_url": "https://upload.wikimedia.org/wikipedia/commons/transcoded/7/71/De-im.ogg/De-im.ogg.mp3"}, {"audio": "De-im2.ogg", "tags": ["Germany", "Berlin"], "ogg_url": "https://upload.wikimedia.org/wikipedia/commons/4/44/De-im2.ogg", "mp3_url": "https://upload.wikimedia.org/wikipedia/commons/transcoded/4/44/De-im2.ogg/De-im2.ogg.mp3"}], "word": "im", "lang": "German", "lang_code": "de"}

View file

@ -140,6 +140,8 @@ void main() {
(essen['textKeys']! as List).cast<String>(),
containsAll(['isst', 'aß', 'gegessen']),
);
// A contraction is a word of its own: im, of in and dem.
expect(ids, contains('im/contraction'));
// Typed with `ss`.
expect(
(entryOf('wiktionary-de-en.jsonl.gz', 'Straße/noun')['looseKeys']!
@ -246,8 +248,14 @@ void main() {
for (final line in linesOf('wiktionary-de-en.jsonl.gz').skip(1))
(line['headword'], line['rank']! as int, line['labels']),
]..sort((a, b) => a.$2.compareTo(b.$2));
expect(ranked.first.$1, 'Haus');
expect(ranked.first.$3, ['common']);
expect(
[
for (final (word, _, _) in ranked)
if (['im', 'Haus', 'Straße'].contains(word)) word,
],
['im', 'Haus', 'Straße'],
);
expect(ranked.firstWhere((each) => each.$1 == 'Haus').$3, ['common']);
});
group('folding', () {