Skip to main content

tervatrix-rtl

Correct right-to-left text for Hebrew and Arabic, in one dependency-free module. Pure standard library, Python 3.9+, MIT.

It fixes the four bidi bugs that ship in almost every RTL application and are invisible in review, because they only appear once real data lands in the layout:

what the user sees why the fix
+972-4-000-0000 renders as 0000-000-4-972+ the dashes are neutral characters and inherit the paragraph's RTL direction prepare_outbound() wraps it in a left-to-right isolate
opening hours 09:00-18:00 render as 18:00-09:00, closing time first two number runs with a neutral between them get reordered as a unit the pattern treats a time range as one run
a whole Hebrew line flips to left-aligned because it starts with 2026 or WhatsApp the first-strong-character rule guesses the paragraph direction an invisible right-to-left mark is prefixed to the line
a prepared string no longer equals the string it came from the marks are invisible but real characters strip_bidi(), the exact inverse

Extracted from a WhatsApp agent answering Hebrew, Arabic and English customers in production.


Install

pip install tervatrix-rtl

No dependencies. Nothing to configure.


The 30-second version

from tervatrix_rtl import prepare_outbound

message = "שעות פתיחה: 09:00-18:00\nלשאלות: +972-4-000-0000"

send(prepare_outbound(message, "he"))

That one call wraps the time range and the phone number in left-to-right isolates and marks the line that starts with a digit. An English string is returned byte for byte untouched, so English never carries invisible control characters into your logs, your tests or your database.


API

prepare_outbound(text, language=None)

The single call to make on any string before you render or send it. Runs isolate_ltr_runs then force_rtl_paragraphs. Returns LTR languages unchanged.

Omit language and it is detected from the script the text is written in.

isolate_ltr_runs(text)

Wraps URLs, www. hosts, e-mail addresses, phone numbers, prices and time ranges in U+2066 … U+2069.

Idempotent, and composable: it processes only what lies outside any isolate that is already there. So you can wrap one value by hand with isolate(), drop it into a sentence, and still pass the whole sentence through prepare_outbound() — the hand-wrapped value survives untouched and everything around it is still protected.

force_rtl_paragraphs(text)

Prefixes U+200F to every line that does not already begin with a strong RTL character. Applied line by line, which is why an isolate is never allowed to cross a newline — a pair split over two lines would never balance.

isolate(value)

Wraps one value unconditionally. Use it when you already know what the value is and the pattern is deliberately too strict to catch it — a bare order id like A7, a two-digit table number.

f"הזמנה {isolate(order.id)} מוכנה"

format_time_range(start, end, separator="-")

format_time_range("09:00", "18:00") gives an isolated 09:00-18:00 that cannot be reordered.

detect_language(text)'he' | 'ar' | 'en'

Counts characters per script rather than stopping at the first hit, so one Latin brand name inside a Hebrew sentence does not switch the whole string to English. URLs, e-mail addresses and phone numbers are removed before counting - they are not evidence of a language, and a single link otherwise carries enough Latin letters to flip a short Hebrew message to English. Falls back to 'en' for digits-only, emoji-only and empty input.

is_rtl(language) and dir_for(language)

is_rtl accepts a bare subtag or a full tag: he, he-IL, ar_EG, fa-IR, ur. dir_for returns 'rtl' or 'ltr', ready to drop into an HTML dir attribute.

strip_bidi(text)

Removes every bidi control character, including the legacy embedding marks (U+202AU+202E) found in old translation files.

Call this before comparing strings. The marks are invisible, so an unstripped string fails an equality check for no visible reason — in a test assertion, a database WHERE, a de-duplication key. strip_bidi(prepare_outbound(x)) is exactly x.

LRI, PDI, RLM, RTL_LANGUAGES

The control characters (U+2066, U+2069, U+200F) and the tuple of RTL primary subtags, exported so you can assert on them in your own tests.


Where it belongs in a web app

Do the isolating once, on the way out, not scattered through templates:

# FastAPI / Flask / Django - one place, at the edge
from tervatrix_rtl import dir_for, prepare_outbound

@app.get("/order/{order_id}")
def order_page(order_id: str, lang: str = "he"):
    order = repo.get(order_id)
    return render(
        "order.html",
        dir=dir_for(lang),
        lang=lang,
        summary=prepare_outbound(order.summary, lang),
    )

And strip on the way in, before anything is compared or stored:

from tervatrix_rtl import strip_bidi

clean = strip_bidi(request.form["name"]).strip()

That second line matters more than it looks. Text pasted from a chat app, a PDF or a spreadsheet routinely carries U+200F and U+202AU+202E. Stored unstripped, the row never matches on lookup and the bug is invisible in every log and every database client.


Tests

No framework to install; unittest ships with Python.

python -m unittest discover -s tests -t .

They cover language detection, isolate placement, idempotency, isolate/newline balance, the round trip through strip_bidi, and a parity suite asserting byte-identical output with the JavaScript implementation.


Licence

MIT — see LICENSE. Use it commercially, fork it, ship it. No warranty.


Where this came from

This package is the RTL layer of a WhatsApp agent that answers Hebrew, Arabic and English customers on the official Meta Cloud API. Two more pieces of it are public and free:

  • @tervatrix/rtl-ui — the identical logic in JavaScript, plus logical-property CSS utilities and an optional React wrapper. Same function names, same behaviour, so a string prepared on either side of a project is byte-identical.
  • wa-agent-starter — a complete, working WhatsApp agent in FastAPI: Meta webhook, HMAC signature verification, language detection, keyword FAQ, this RTL handling, and 50 passing tests. MIT, self-hosted, no account with us.

If you need more than a keyword bot, wa-agent-kit is the paid version. It adds a Qdrant knowledge base so the agent answers questions nobody wrote a keyword for, multi-tenant support for many business numbers on one deployment, human handover, an admin panel, an AI-provider abstraction, and 12 months of updates.

Get wa-agent-kit

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tervatrix_rtl-1.0.0.tar.gz (16.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tervatrix_rtl-1.0.0-py3-none-any.whl (10.5 kB view details)

Uploaded Python 3

File details

Details for the file tervatrix_rtl-1.0.0.tar.gz.

File metadata

  • Download URL: tervatrix_rtl-1.0.0.tar.gz
  • Upload date:
  • Size: 16.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.11.9

File hashes

Hashes for tervatrix_rtl-1.0.0.tar.gz
Algorithm Hash digest
SHA256 74a66bb16ae6ca451cb41dc94e1cdfbba46aa85cf1f8acc8b46862cee0d089cb
MD5 d71f2825a8082f890aedcf5fac317426
BLAKE2b-256 6d4b5416063459a06d6bc510f7f1b1904a5690acef730f5f2cd4b4f025601a81

See more details on using hashes here.

File details

Details for the file tervatrix_rtl-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: tervatrix_rtl-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 10.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.11.9

File hashes

Hashes for tervatrix_rtl-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 bd3a95c4936f61c634e4c8eecbafd2fb9f1b9eb6bda4b62ecdccac3da4caa355
MD5 9dcdd546d59b4fbdf2393b4dad60afd8
BLAKE2b-256 0547e0ab4cf35b191110706155765ae1fa8e5ab9de8870870e099d0a284ed166

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page