Skip to content

Replace fasttext with lingua and overhaul wrong-language detection - #1

Merged
UHHHHHHHHHHHHHH merged 4 commits into
mainfrom
2091-swap-fasttext-for-lingua
Sep 17, 2026
Merged

UHHHHHHHHHHHHHH merged 4 commits into
mainfrom
2091-swap-fasttext-for-lingua

Conversation

@UHHHHHHHHHHHHHH

Copy link
Copy Markdown
Collaborator

Restores installability on Python 3.13/3.14 (pescheck/dashboard#2091 on GitLab): fasttext-wheel tops out at cp312 wheels and pins numpy<2. lingua 2.2 ships cp312-cp314 wheels, has zero dependencies, and bundles its models in the wheel, so nothing is downloaded at runtime.

The fasttext-era tuning (carrier phrases, confused-merge tables, 0.5 absolute threshold) measured counterproductive on lingua, so wrong-language detection is rebuilt around lingua's full confidence distribution:

  • Detector restricted to the locales present in the linted dirs plus the source language
  • Relative rule: flag only when the expected language scores below expected_confidence_max (0.1) while another tops confidence_threshold (0.7)
  • Script filtering: tokens in a script the locale does not use are stripped before detection
  • Source token stripping: msgstr tokens copied verbatim from the msgid (loan words, product nouns, quoted terms) are dropped, so only the actually translated words are judged

Verification

Benchmarked against the full PESCHECK translation corpus (27,011 entries, 507 .po files) plus 6,800 planted contaminations (real msgstrs judged as another locale) and synthetic stress suites:

corpus false positives: 0 (was 13 with the ported fasttext pipeline)
planted recall: 6065/6800 = 89.2% (uk<-ru 25/25, nl<-de 24/25, it<-es 22/25)
loan-word stress (English tokens injected at 20-80%): 0/1920 flagged
full corpus lint: 16.6s

Both real contaminations in the corpus are found: Swedish in examplecheck's sw locale, Spanish in idcheck's it locale (the latter at expected_confidence_max 0.05). Requires Python 3.12+.

Niels Ekkelenkamp and others added 4 commits September 16, 2026 17:16
fasttext-wheel has no wheels past cp312 and pins numpy<2, which blocked
installing on Python 3.13+ (dashboard#2091). lingua 2.x is a Rust engine
with cp312-cp314 wheels and zero dependencies, and its models ship in
the wheel so nothing is downloaded at runtime.

The detection pipeline (carrier phrases, confused merges, thresholds)
carries over unchanged apart from two new directional Cyrillic merges
lingua needs: Ukrainian text with dotted i reads as Kazakh, and short
Russian phrases read as Bulgarian. Bokmal and Nynorsk confidences are
folded into 'no' since lingua has no Norwegian macrolanguage, and
locales lingua has no model for are skipped instead of flagged.

--compact-model now maps to lingua's low accuracy mode. Requires
Python 3.12+ (lingua's wheel floor).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion

The carrier phrases, confused-merge tables, and 0.5 absolute threshold were
all tuned to fasttext's top-k output. Benchmarked against the full PESCHECK
translation corpus (27k entries) plus 6800 planted contaminations, they were
counterproductive on lingua: the merge tables made whole sibling pairs
undetectable (0/25 planted recall on pt<-es and da<-sv) while still letting
mixed-script brand strings flag as exotic languages.

Replaced with three mechanisms measured to give zero false positives on the
corpus at 88.7% planted recall:

- The detector is restricted to the locales present in the linted dirs plus
  the source language, instead of all 70+ spoken languages. Contamination
  comes from neighbouring rows of the same translation batch, and the
  restriction keeps confidence mass on languages that can actually occur.
- A relative rule replaces the absolute threshold: flag only when the
  expected language scores below expected_confidence_max (0.05) while
  another language tops confidence_threshold (0.7). This asks 'is this
  clearly NOT the expected language' instead of 'what is the argmax'.
- Tokens in a script the locale does not use are stripped before detection.
  Lingua only considers the dominant script's words, so quoted product
  nouns ('Скопіюйте Application ID та Client Secret') otherwise drown out
  the native text entirely. All-foreign-script contamination stays caught
  by check_wrong_script, which runs first.

Carrier phrases and confused merges are deleted. New expected_confidence_max
config knob and --expected-confidence-max CLI flag; confidence_threshold
default moves 0.5 -> 0.7. Full-corpus lint runs in ~16s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A loan word, product noun, or quoted term in a translation is a token
carried over verbatim from the source string, so the msgid (already in
the is_wrong_language signature, previously unused) is the exact list of
words that say nothing about the translation's language. Dropping msgstr
tokens whose words all appear in the msgid isolates the actually
translated words; a string that is mostly untranslatable jargon falls
under the minimum detection length and is skipped.

Benchmarked against the corpus plus synthetic stress suites (English
tech tokens injected into correct translations at 20-80% of tokens, and
into planted contamination at 30%): zero false positives at every
injection ratio, including the quoted-Dutch-law-name lines that
previously needed a .po-lint-ignore entry, which is now redundant. With
the filter in place expected_confidence_max goes back to 0.1, restoring
sibling-pair recall (planted 89.2%).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Test matrix 3.12-3.14 to match requires-python (lingua's wheel floor),
regenerated uv.lock, and dropped PO_LINT_COMPACT_MODEL from the test
run: it exists to skip the old 126MB fasttext download, but lingua's
models ship in the wheel, and the thresholds are tuned for high
accuracy mode.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@UHHHHHHHHHHHHHH
UHHHHHHHHHHHHHH merged commit 01505af into main Sep 17, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant