Adding Language Support to SqueakyCleanText¶
This guide covers how to add new language support to SqueakyCleanText, both as a user (via config) and as a maintainer (adding built-in support).
Overview¶
SqueakyCleanText has 4 language-dependent subsystems:
| Subsystem | What it does | Default languages |
|---|---|---|
| Lingua detection | Auto-detects input language | All 75 Lingua languages (detect_all_languages=True; False = EN, NL, DE, ES + extras) |
| Stopwords | Removes common words for statistical model | EN, NL, DE, ES |
| Date patterns | Regex + fuzzy matching for month names | EN, NL, DE, ES, FR, PT, IT |
| NER models | Named entity recognition (ONNX models) | EN, NL, DE, ES + Multilingual |
Each subsystem can be extended independently. A language doesn't need support in all 4 to be useful.
v0.6.0 Notes¶
- PII mode (
ner_mode='pii') applies to all supported languages — PII labels are language-agnostic - Replacement modes (
replacement_mode='placeholder'|'synthetic'|'reversible') work for all languages - ModernBERT is available as an alternative English-only backend with 8192-token context (vs 512 for XLM-RoBERTa). Set via
ner_models={**DEFAULT_NER_MODELS, 'ENGLISH': 'rhnfzl/modernbert-base-ner-conll03-english-onnx'}once exported (the dict must nameENGLISHandMULTILINGUAL) - GLiClass document classification is language-independent (zero-shot, works with any input language)
How to Specify Languages¶
All language parameters accept any of these formats (case-insensitive):
| Format | Example | Resolves to |
|---|---|---|
| Lingua name | 'ENGLISH', 'english' |
'ENGLISH' |
| ISO 639-1 | 'en', 'EN' |
'ENGLISH' |
| ISO 639-3 | 'eng', 'ENG' |
'ENGLISH' |
This applies to: language, extra_languages, custom_stopwords keys, and custom_month_names keys.
Single language vs. tuple of languages¶
from sct import TextCleanerConfig
# Pin to one language (skip auto-detection)
cfg = TextCleanerConfig(language='en') # → detects nothing, always ENGLISH
# Restrict detection to listed languages (auto-detect among them)
cfg = TextCleanerConfig(language=('en', 'nl')) # → detect per text, restricted to EN/NL
# Full auto-detection (default — all supported languages)
cfg = TextCleanerConfig(language=None) # → detect among all 75 Lingua languages
# (detect_all_languages=False: EN, NL, DE, ES + extras)
Option A: User-Level Extension (via TextCleanerConfig)¶
Users can add language support without modifying source code:
from sct.config import TextCleanerConfig
from sct.sct import TextCleaner
cfg = TextCleanerConfig(
# 1. Extend Lingua detection to include Polish (ISO codes also work: 'pl')
extra_languages=('POLISH',),
# 2. Custom stopwords (optional — auto-detected from stop-words package if available)
custom_stopwords={
'POLISH': frozenset({'i', 'w', 'na', 'z', 'do', 'nie', 'to', 'jest'}),
},
# 3. Custom month names for date detection (optional)
custom_month_names={
'POLISH': (
'styczen', 'luty', 'marzec', 'kwiecien', 'maj', 'czerwiec',
'lipiec', 'sierpien', 'wrzesien', 'pazdziernik', 'listopad', 'grudzien',
),
},
# 4. Custom NER model (optional — falls back to MULTILINGUAL if not provided)
ner_models={
'ENGLISH': 'rhnfzl/xlm-roberta-large-conll03-english-onnx',
'MULTILINGUAL': 'rhnfzl/wikineural-multilingual-ner-onnx',
# 'POLISH': 'your-org/polish-ner-onnx', # placeholder: your own ONNX NER repo; without it POLISH runs MULTILINGUAL
},
)
cleaner = TextCleaner(cfg=cfg)
result = cleaner.process("Spotkanie z Janem Kowalskim 15 styczen 2025 w Warszawie")
What each field does¶
extra_languages: Tuple of language identifiers (e.g. 'POLISH', 'pl', 'pol'). Accepts Lingua names, ISO 639-1, or ISO 639-3 codes. They join supported_languages, which sets the stopword lists loaded up front and the languages legacy warmup() loads. Unified processing rejects warmup() because providers initialize per worker on first use. With the default detect_all_languages=True, Lingua already considers all 75 languages, so the detector does not change. Only with detect_all_languages=False does extra_languages add these languages to the detector.
custom_stopwords: Dict mapping language name to frozenset of stopwords. If provided for a language, replaces (not merges with) any auto-detected stopwords. If not provided, the stop-words package is queried using the language's ISO 639-1 code.
custom_month_names: Dict mapping language name to tuple of 12 month names (full form, in order Jan-Dec). These are compiled into the date regex at config time, enabling date detection for that language.
ner_models: Dict mapping language name to HuggingFace ONNX model repo ID. ENGLISH and MULTILINGUAL are always required. Languages without a dedicated NER model fall back to the MULTILINGUAL model.
Language discovery¶
The supported_languages field is automatically computed as the union of:
- Default 4: {ENGLISH, DUTCH, GERMAN, SPANISH}
- extra_languages keys
- ner_models keys (minus MULTILINGUAL)
- custom_stopwords keys
- custom_month_names keys
You don't need to add a language to extra_languages if it's already declared in any other field.
Minimal examples¶
Just detection (no stopwords/dates/NER for the new language):
from sct import TextCleanerConfig
cfg = TextCleanerConfig(
extra_languages=('FRENCH',),
check_ner_process=False,
)
Detection + stopwords (auto-loaded from stop-words package):
from sct import TextCleanerConfig
cfg = TextCleanerConfig(
extra_languages=('FRENCH',), # stop-words has French built-in
check_ner_process=False,
)
Pin language (skip auto-detection — ISO codes accepted):
from sct import TextCleanerConfig
cfg = TextCleanerConfig(
language='fr', # ISO 639-1 → resolves to 'FRENCH'; any Lingua language works
check_ner_process=False,
)
Option B: Adding Built-In Language Support (Maintainer Guide)¶
There are two tiers of built-in support:
| Tier | What it means | Steps required |
|---|---|---|
| Opt-in | Detected by default (all 75 Lingua languages); users add it to the detect_all_languages=False detector via extra_languages=('FRENCH',) |
Steps 1, 3 (NER only), 7 |
| Full default | In DEFAULT_LANGUAGES, so also in the detect_all_languages=False detector |
Steps 1, 2, 3, 7 |
With the default detect_all_languages=True, every Lingua language is detected, so FR/PT/IT and Polish need no setting. The tiers only differ when a user sets detect_all_languages=False: then only supported_languages are detected, and an opt-in language needs extra_languages=('FRENCH',). build_detector() handles any Lingua language name.
To permanently add a new language (e.g. French) to the package defaults, follow these steps in dependency order.
Step 1: sct/utils/constants.py — Add month names¶
Add the language's month names to MONTH_NAMES_MULTILINGUAL:
MONTH_NAMES_MULTILINGUAL = {
# ... existing entries ...
# French (accented characters are supported — keys are lowercase canonical forms)
"janvier": frozenset({"fr"}), "février": frozenset({"fr"}),
"mars": frozenset({"fr"}), # French March — NOT shared with English "march"
"avril": frozenset({"fr"}), "juin": frozenset({"fr"}),
"juillet": frozenset({"fr"}), "août": frozenset({"fr"}),
"septembre": frozenset({"fr"}), "octobre": frozenset({"fr"}),
"novembre": frozenset({"fr", "it"}), # shared: FR novembre = IT novembre
"décembre": frozenset({"fr"}),
# Update existing entries when month names are shared:
"mai": frozenset({"de", "fr"}), # German Mai = French mai (both = May)
}
Shared month names — check these before adding new entries:
- "agosto" (ES/PT/IT August): add new ISO codes to the existing frozenset
- "marzo" (ES/IT March): already tagged {"es", "it"}
- "abril" (ES/PT April): already tagged {"es", "pt"}
- "mai" (DE/FR May): already tagged {"de", "fr"}
- "novembre" (FR/IT November): already tagged {"fr", "it"}
Rules:
- Keys must be lowercase (include accented characters as they appear in text)
- Values are frozensets of ISO 639-1 codes
- If a month name string is identical across languages, add all ISO codes to one entry
- Abbreviations (≤ 4 chars) are caught by exact DATE_REGEX; include unambiguous ones only
- Full names (> 4 chars) are used by fuzzy matching via FUZZY_MONTH_VOCABULARY_BY_LANG
Update _LANG_TO_VOCAB:
_LANG_TO_VOCAB = {
"ENGLISH": "en",
"DUTCH": "nl",
"GERMAN": "de",
"SPANISH": "es",
"FRENCH": "fr", # ADD
"PORTUGUESE": "pt", # ADD
"ITALIAN": "it", # ADD
}
The module-level DATE_REGEX, FUZZY_MONTH_VOCABULARY, and FUZZY_MONTH_VOCABULARY_BY_LANG are automatically recomputed from MONTH_NAMES_MULTILINGUAL at import time. No further changes to datetime.py needed.
Step 2: sct/utils/resources.py — Update default languages (full-default tier only)¶
Skip this step for opt-in languages —
build_detector()dynamically handles any language insupported_languages. Only needed to add the language to the detector used withdetect_all_languages=False; the default detector already covers all 75 Lingua languages.
Add the language to DEFAULT_LANGUAGES:
Step 3: sct/config.py, update model routes¶
Add the language to the matching checkpoint record in _ONNX_CHECKPOINTS and _TORCH_CHECKPOINTS.
Each record keeps its language keys, model ID, and pinned revision together.
The exported DEFAULT_* maps are derived from those records.
A language without a dedicated model belongs to the multilingual checkpoint record:
(('FRENCH', 'PORTUGUESE', 'ITALIAN', 'MULTILINGUAL'),
'rhnfzl/wikineural-multilingual-ner-onnx', '74191432b21c956b5b04b4778c0e92e569b1f80d'),
LANG_KEYS defines the default model routes. Add opt-in routes to the checkpoint records without changing that default set.
Step 3.5: scripts/export_onnx_models.py — Create dedicated ONNX model (optional)¶
FR/PT/IT (v0.5.0+) use
wikineural-multilingual-ner-onnxas their NER model. This is the accepted design — per-language ONNX models for these languages are hard to source (few CoNLL/WikiNER-style XLM-RoBERTa models exist for FR/PT/IT on HuggingFace). The multilingual model provides adequate PER/LOC/ORG/MISC coverage across all three languages.
This step is only needed if you specifically want a dedicated ONNX model for a language and have identified a suitable source model. Skip this step entirely if the MULTILINGUAL fallback is acceptable (it is for FR/PT/IT).
1. Find a suitable source model on HuggingFace:
For languages with CoNLL-02/03 coverage (EN, NL, DE, ES), the canonical source models are the FacebookAI/xlm-roberta-large-finetuned-conll0{2,3}-* series. For other languages, search HuggingFace for XLM-RoBERTa-based NER models fine-tuned on a standard NER corpus (WikiNER, Evalita, HAREM, etc.):
# Search HuggingFace for French NER models
# Filter: task=token-classification, architecture=XLM-RoBERTa
# Prefer: high downloads, standard NER labels (PER/LOC/ORG/MISC), CoNLL-style evaluation
2. Add the source → target mapping to MODEL_MAP:
# scripts/export_onnx_models.py
MODEL_MAP = {
# ... existing entries ...
# Add when a suitable source model is identified:
"your-org/source-french-ner-model": "rhnfzl/xlm-roberta-large-ner-french-onnx",
"your-org/source-portuguese-ner-model": "rhnfzl/xlm-roberta-large-ner-portuguese-onnx",
"your-org/source-italian-ner-model": "rhnfzl/xlm-roberta-large-ner-italian-onnx",
}
3. Export and upload the model:
# Install export dependencies (separate from runtime deps)
pip install torch transformers "optimum[onnxruntime]" huggingface_hub
# Authenticate to HuggingFace Hub
huggingface-cli login
# Export a single language model
python scripts/export_onnx_models.py --models your-org/source-french-ner-model
# Or export all models in MODEL_MAP (skips already-uploaded ones)
python scripts/export_onnx_models.py
# Preview without uploading
python scripts/export_onnx_models.py --dry-run
The script handles: ONNX export via Optimum CLI, verification of required files, repo creation on HuggingFace Hub, file upload, and cleanup.
4. Update config.py with the new ONNX repo ID:
After upload, move the language from the multilingual record to a dedicated record in _ONNX_CHECKPOINTS.
Record the uploaded model ID and its resolved revision together.
Step 4: sct/utils/stopwords.py — No changes needed¶
The stop-words package supports 33 languages. If the new language is among them, stopwords are auto-loaded via language_to_iso(). Check coverage:
from stop_words import StopWordError, get_stop_words
print('le' in get_stop_words('fr'))
# -> True
try:
get_stop_words('th')
except StopWordError:
print('Thai: not supported, SCT uses no stopwords')
# -> Thai: not supported, SCT uses no stopwords
If the package doesn't support the language, users can provide custom_stopwords, or you can add a built-in set.
Step 5: sct/utils/datetime.py — No changes needed¶
Date regex is auto-built from MONTH_NAMES_MULTILINGUAL (Step 1). Fuzzy vocabulary is auto-built from the same data. No code changes needed.
Step 6: sct/utils/ner.py — No changes needed¶
NER models are loaded from the ner_models dict. If a default model was added in Step 3, it will be available automatically. Languages without dedicated models fall back to MULTILINGUAL.
Step 7: tests/test_sct.py — Add language-specific tests¶
Add date regex tests:
def test_date_regex_french(self):
dt = datetime.ProcessDateTime()
cases = [
("Reunion le 15 janvier 2024 a Paris", "15 janvier 2024"),
("Le 3 mars 2023 debut du projet", "3 mars 2023"),
]
for input_text, date_str in cases:
result = dt.replace_dates(input_text, "<DATE>")
self.assertIn("<DATE>", result, f"Failed for French date: {date_str}")
Add NER test (if dedicated model added):
@requires_ner
def test_ner_process_french(self):
processed = self.ner.ner_process(
"Emmanuel Macron habite a Paris.",
positional_tags=['PER', 'LOC'],
ner_confidence_threshold=0.85,
language='FRENCH',
)
self.assertNotIn("Emmanuel Macron", processed)
self.assertNotIn("Paris", processed)
Step 8: Version bump¶
Bump version in sct/__init__.py (single source of truth for version).
Creating ONNX NER Models for New Languages¶
Use scripts/export_onnx_models.py to convert a PyTorch NER model from HuggingFace to ONNX format and upload it to the rhnfzl organization. This script is the standard tool for all ONNX model creation in this project.
Prerequisites¶
# Install export dependencies (not in the runtime requirements)
pip install torch transformers "optimum[onnxruntime]" huggingface_hub
# Log in to HuggingFace Hub (requires write access to rhnfzl org)
huggingface-cli login
Workflow¶
Step 1: Identify a suitable source model
For languages with CoNLL-02/03 data, prefer the FacebookAI/xlm-roberta-large-finetuned-* series (consistent architecture with existing EN/NL/DE/ES models). For other languages:
- Search HuggingFace for XLM-RoBERTa models fine-tuned on a standard NER corpus
- Prefer: high download counts, CoNLL-style labels (PER/LOC/ORG/MISC), public evaluation results
- CoNLL-02 covers Spanish, Dutch; CoNLL-03 covers English, German; other languages typically use WikiNER, Evalita (Italian), HAREM (Portuguese), QUAERO (French), etc.
Step 2: Add the mapping to MODEL_MAP
Edit scripts/export_onnx_models.py:
MODEL_MAP = {
# Existing entries (CoNLL-02/03):
"FacebookAI/xlm-roberta-large-finetuned-conll03-english": "rhnfzl/xlm-roberta-large-conll03-english-onnx",
# ... other existing entries ...
# New language — add your chosen source model:
"your-org/xlm-roberta-large-french-ner": "rhnfzl/xlm-roberta-large-ner-french-onnx",
}
Target repo IDs follow the pattern rhnfzl/xlm-roberta-large-ner-{language}-onnx.
Step 3: Export and upload
# Export a specific model only
python scripts/export_onnx_models.py --models your-org/xlm-roberta-large-french-ner
# Dry run first (exports but doesn't upload)
python scripts/export_onnx_models.py --models your-org/xlm-roberta-large-french-ner --dry-run
# Keep the export directory for inspection
python scripts/export_onnx_models.py --models your-org/xlm-roberta-large-french-ner --keep-exports
The script exports via optimum.exporters.onnx (token-classification task), verifies required files (model.onnx, config.json, tokenizer.json, tokenizer_config.json), creates the HuggingFace repo if it doesn't exist, uploads all files, and cleans up the local export directory.
Step 4: Update config.py
After upload, move the language from the multilingual checkpoint record to its own _ONNX_CHECKPOINTS record.
Use the uploaded model ID and its pinned revision.
The DEFAULT_NER_MODELS and DEFAULT_NER_REVISIONS maps then update together.
The ONNX pipeline (sct/utils/onnx_pipeline.py) handles download, caching, and inference automatically. INT8 dynamic quantization is also available at load time via the quantize=True parameter (install SqueakyCleanText[quantize] first), reducing model size ~4× with a small accuracy trade-off.
Language Support Matrix¶
Current built-in support (0.8.0). "Yes (auto)" means detected with the default detect_all_languages=True; the last column is only needed with detect_all_languages=False:
| Language | Lingua (default) | Stopwords | Date Regex | Fuzzy Dates | NER Model | Opt-in via |
|---|---|---|---|---|---|---|
| English | Yes (auto) | Yes | Yes | Yes | rhnfzl/xlm-roberta-large-conll03-english-onnx |
— |
| Dutch | Yes (auto) | Yes | Yes | Yes | rhnfzl/xlm-roberta-large-conll02-dutch-onnx |
— |
| German | Yes (auto) | Yes | Yes | Yes | rhnfzl/xlm-roberta-large-conll03-german-onnx |
— |
| Spanish | Yes (auto) | Yes | Yes | Yes | rhnfzl/xlm-roberta-large-conll02-spanish-onnx |
— |
| French | Yes (auto) | Yes (stop-words) | Yes | Yes | rhnfzl/wikineural-multilingual-ner-onnx (multilingual) |
extra_languages=('FRENCH',) |
| Portuguese | Yes (auto) | Yes (stop-words) | Yes | Yes | rhnfzl/wikineural-multilingual-ner-onnx (multilingual) |
extra_languages=('PORTUGUESE',) |
| Italian | Yes (auto) | Yes (stop-words) | Yes | Yes | rhnfzl/wikineural-multilingual-ner-onnx (multilingual) |
extra_languages=('ITALIAN',) |
User-extensible via TextCleanerConfig (no source changes needed):
| Feature | How to enable | Auto-detected? |
|---|---|---|
| Lingua detection | Default (all 75 languages); with detect_all_languages=False, extra_languages=('POLISH',) |
Yes |
| Stopwords | Auto from stop-words pkg |
Yes (33 languages) |
| Custom stopwords | custom_stopwords={'POLISH': frozenset({...})} |
No |
| Date regex | custom_month_names={'POLISH': (...)} |
No |
| NER model | ner_models={**DEFAULT_NER_MODELS, 'POLISH': 'model-id'} |
Falls back to MULTILINGUAL |
Languages supported by the stop-words package¶
Arabic, Bulgarian, Catalan, Chinese, Czech, Danish, Dutch, English, Finnish, French, German, Greek, Gujarati, Hebrew, Hindi, Hungarian, Indonesian, Italian, Japanese, Korean, Malay, Norwegian, Persian, Polish, Portuguese, Romanian, Russian, Slovak, Spanish, Swedish, Turkish, Ukrainian, Vietnamese (33, from the package's languages.json).
Checklist for Adding a New Built-In Language¶
Opt-in tier (auto-detected by default; extra_languages only for detect_all_languages=False)¶
- [ ] Add month names to
MONTH_NAMES_MULTILINGUALinconstants.py(check for shared names!) - [ ] Add ISO code to
_LANG_TO_VOCABinconstants.py - [ ] Add a language key to
_ONNX_CHECKPOINTS(dedicated model or multilingual fallback) inconfig.py - [ ] Add a language key to
_TORCH_CHECKPOINTS(dedicated model or multilingual fallback) inconfig.py - [ ] Create dedicated ONNX model (optional — see Step 3.5; skip if MULTILINGUAL fallback is acceptable):
- [ ] Find a suitable source model on HuggingFace (XLM-RoBERTa-based, CoNLL/WikiNER-style labels)
- [ ] Add
"source-model": "rhnfzl/xlm-roberta-large-ner-{lang}-onnx"toMODEL_MAPinscripts/export_onnx_models.py - [ ] Run
python scripts/export_onnx_models.py --models <source-model>to export and upload to rhnfzl Hub - [ ] Move the language to its dedicated
_ONNX_CHECKPOINTSrecord with the new ONNX repo ID and revision - [ ] Add date regex tests in
test_sct.py - [ ] (Optional) Add NER tests if dedicated model added
- [ ] Verify:
ruff check sct/ tests/clean - [ ] Verify:
pytest tests/ -vall pass - [ ] Bump
__version__insct/__init__.py(pyproject.tomlreads it)
Full-default tier (also in the detect_all_languages=False detector)¶
All of the above, plus:
- [ ] Add to DEFAULT_LANGUAGES in resources.py
Default routing¶
Keep LANG_KEYS unchanged when adding an opt-in route. Changing it expands the default model and language set.