Span-level language detection for mixed-language text
Project description
polystring
Span-level language detection for mixed-language text.
Most language detection libraries return a single label for the whole string. polystring returns a labelled span for every part of the sentence, with character offsets, confidence scores, and special-token extraction baked in.
from polystring import analyze
result = analyze("je suis tellement tired, this week has been rough")
for span in result.spans:
print(f"[{span.language}] {span.text!r}")
# [fr] 'je suis tellement tired'
# [en] 'this week has been rough'
Why span-level detection matters
| Tool | What it returns for "hola I love this city, en serio" |
|---|---|
langdetect |
"es" (labels the whole string) |
lingua |
"es" (labels the whole string) |
langid |
"es" (labels the whole string) |
| polystring | [es] "hola" · [en] "I love this city" · [es] "en serio" |
Code-switching (mixing languages within a single sentence) is normal on social media, in diaspora communities, in customer support chats, and in any multilingual context. A single label for the whole input misses the structure entirely. polystring is built specifically for this problem.
Installation
pip install polystring
Optional extras:
pip install polystring[pandas] # enables result.to_dataframe()
Examples
Spanish / English (Spanglish)
result = analyze("no puedo creer how good this restaurant is, en serio")
for span in result.spans:
print(f"[{span.language}] {span.text!r}")
# [es] 'no puedo creer'
# [en] 'how good this restaurant is'
# [es] 'en serio'
French / English
result = analyze("je suis tellement tired lately, I need des vacances")
for span in result.spans:
print(f"[{span.language}] {span.text!r}")
# [fr] 'je suis tellement tired lately'
# [en] 'I need'
# [fr] 'des vacances'
Mixed with non-Latin scripts
result = analyze("this is great هذا رائع جداً I am very impressed")
for span in result.spans:
print(f"[{span.language}] {span.text!r}")
# [en] 'this is great'
# [ar] 'هذا رائع جداً'
# [en] 'I am very impressed'
Working with results
result = analyze("je suis tellement tired, this week has been rough")
result.dominant_language # 'fr'
result.is_mixed # True
result.languages # {'fr', 'en'}
result.confidence # 0.87 (mean confidence across linguistic spans)
Serialise to dict
result.to_dict()
# {
# 'text': 'je suis tellement tired, this week has been rough',
# 'spans': [{'text': 'je suis tellement tired', 'language': 'fr', ...}, ...],
# 'dominant_language': 'fr',
# 'is_mixed': True,
# 'confidence': 0.87,
# ...
# }
Serialise to DataFrame
# pip install polystring[pandas]
df = result.to_dataframe()
# text language token_type confidence start end is_foreign
# 0 je suis tellement tired fr text 0.91 0 23 False
# 1 this week has been rough en text 0.84 25 49 True
ANSI-coloured terminal output
print(result.highlight())
# [fr]je suis tellement tired [en]this week has been rough
# (each language rendered in a distinct colour)
Filter to linguistic spans only
result.linguistic_spans()
# Returns spans with token_type == "text" only (no URLs, emoji, mentions, etc.)
Span fields
| Field | Type | Description |
|---|---|---|
text |
str |
Text as it appears in the input |
language |
str |
ISO 639-1 code. "ur-Latn" for Roman Urdu, "und" for undetermined, "ne" for proper nouns |
token_type |
str |
"text", "url", "mention", "hashtag", "emoji", "num", or "ne" |
confidence |
float |
0.0 to 1.0. Non-text tokens are always 0.0 |
start / end |
int |
Character offsets into the original string |
is_foreign |
bool |
True if this span is not the dominant language |
ambiguous_candidates |
list[str] |
Populated when language == "und" due to a near-identical pair (e.g. ["es", "pt"]) |
Language coverage
polystring detects 75 languages via lingua. Non-Latin scripts (Arabic, Devanagari, CJK, Cyrillic, Thai, Hebrew, Korean, and more) are identified directly from Unicode ranges, no model call needed.
Nine languages have dedicated lexicons on top of the model, which significantly improves accuracy on short spans and code-switched text: Roman Urdu / Hinglish, Spanish, Portuguese, Italian, German, Turkish, Tagalog, Swahili
import polystring
print(polystring.supported_languages()) # full list of 75 ISO 639-1 codes
Options
analyze(
text,
languages=["es", "en"], # restrict to known language set (faster, fewer false positives)
granularity="token", # "span" (default) or "token" to get per-word data
min_confidence=0.70, # tokens below this threshold become "und"
low_accuracy_mode=False, # lexicon + script detection only, no model (very fast)
normalize=True, # NFC normalisation
custom_lexicon={"sw": ["mambo", "vipi"]}, # inject domain-specific words
)
granularity="token" gives per-word data
result = analyze("bonjour how are you doing", granularity="token")
for tok in result.tokens:
print(f"[{tok.language}] {tok.text!r} ({tok.confidence:.2f})")
# [fr] 'bonjour' (0.92)
# [en] 'how' (0.83)
# [en] 'are' (0.81)
# [en] 'you' (0.85)
# [en] 'doing' (0.88)
Contributing
git clone https://github.com/saadlohani/polystring
cd polystring
pip install -e ".[dev]"
pytest
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file polystring-0.1.0.tar.gz.
File metadata
- Download URL: polystring-0.1.0.tar.gz
- Upload date:
- Size: 147.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1d5e38a213ede282a229916f8d4ab8756a7467ed74291fdd117d5dfd1e68b9f3
|
|
| MD5 |
ef3f40fcea7dd4e91c6de2ad833ee3e5
|
|
| BLAKE2b-256 |
6a797c8ff1427403f24407c3c42d5ab24d668b7bc1cd4fa8d42f789b89e0cee3
|
Provenance
The following attestation bundles were made for polystring-0.1.0.tar.gz:
Publisher:
ci.yml on saadlohani/polystring
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
polystring-0.1.0.tar.gz -
Subject digest:
1d5e38a213ede282a229916f8d4ab8756a7467ed74291fdd117d5dfd1e68b9f3 - Sigstore transparency entry: 1807003505
- Sigstore integration time:
-
Permalink:
saadlohani/polystring@58fc0f791d1122b1b6bdf732c99ce0d26fc5b0fc -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/saadlohani
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@58fc0f791d1122b1b6bdf732c99ce0d26fc5b0fc -
Trigger Event:
push
-
Statement type:
File details
Details for the file polystring-0.1.0-py3-none-any.whl.
File metadata
- Download URL: polystring-0.1.0-py3-none-any.whl
- Upload date:
- Size: 145.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
74a376c3bd092c05766e52f563f6bd72c54aa800e48f979e63adacc9bcadcd70
|
|
| MD5 |
72846517eb7a6a987c92b647c664aed3
|
|
| BLAKE2b-256 |
2be1ffe1a6c05a3816f5f46824d7e82653fafe8b7002ea5de715af5aedc8e107
|
Provenance
The following attestation bundles were made for polystring-0.1.0-py3-none-any.whl:
Publisher:
ci.yml on saadlohani/polystring
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
polystring-0.1.0-py3-none-any.whl -
Subject digest:
74a376c3bd092c05766e52f563f6bd72c54aa800e48f979e63adacc9bcadcd70 - Sigstore transparency entry: 1807003920
- Sigstore integration time:
-
Permalink:
saadlohani/polystring@58fc0f791d1122b1b6bdf732c99ce0d26fc5b0fc -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/saadlohani
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@58fc0f791d1122b1b6bdf732c99ce0d26fc5b0fc -
Trigger Event:
push
-
Statement type: