Skip to main content

ukrainian-tn

CI PyPI version

Ukrainian text normalization, tokenization and sentence splitting in Rust, with optional native Python bindings.

The crate turns machine-readable spellings into the words a Ukrainian speaker would say — numbers, dates, times, ranges, units, currencies, abbreviations, identifiers and web addresses — and splits text into sentences and tokens.

[dependencies]
ukrainian-tn = "0.7"
use ukrainian_tn::{rozpodil, uktextnorm};

assert_eq!(uktextnorm::normalize("5 кг"), "п'ять кілограмів");
assert_eq!(uktextnorm::number_to_words(123), "сто двадцять три");

let sentences = rozpodil::split_sentences("Привіт! Це тест.");
assert_eq!(sentences.iter().map(|s| s.text).collect::<Vec<_>>(), ["Привіт!", "Це тест."]);

Python

The PyPI distribution is named ukrainian-tn and provides the ukrainian_tn import module:

pip install ukrainian-tn
import ukrainian_tn

assert ukrainian_tn.normalize("5 кг") == "п'ять кілограмів"

options = ukrainian_tn.NormalizeOptions("tts_friendly")
options.input_tolerance = "asr"
print(ukrainian_tn.normalize_with("5–7 кг", options))

for token in ukrainian_tn.tokenize("П'ять зв'язків."):
    print(token.text, token.start, token.stop)

To build the extension from this repository, create and activate a Python virtual environment, install maturin, then run:

maturin develop --release

The bindings use PyO3's stable ABI for Python 3.9 and newer. In Cargo, PyO3 is optional and is enabled only by the python feature; ordinary Rust builds do not compile or link Python support.

Normalization

normalize applies the default options. Use normalize_preset for a named bundle, or normalize_with for full control.

use ukrainian_tn::uktextnorm::{normalize, normalize_preset, normalize_with,
                               NormalizeOptions, NormalizePreset, RangeStyle};

assert_eq!(normalize("01.05.2024"), "перше травня дві тисячі двадцять четвертого року");

// Presets bundle the options for a use case.
let spoken = normalize_preset("5–7 кг", NormalizePreset::TtsFriendly);
assert_eq!(spoken, "від п'яти до семи кілограмів");

// Or start from a preset and adjust.
let options = NormalizeOptions {
    range_style: RangeStyle::Compact,
    ..NormalizeOptions::preset(NormalizePreset::TtsFriendly)
};
assert_eq!(normalize_with("5–7 кг", &options), "п'ять сім кілограмів");

The presets are Default, TtsFriendly (everything spelled out, for speech synthesis), Conservative (changes as little as possible) and SearchIndexing (keeps tokens searchable rather than speakable).

Ambiguity controls

NormalizeOptions keeps backwards-compatible defaults while letting callers resolve ambiguous input explicitly:

  • colon_style — contextual clock/ratio detection, forced clock, or forced ratio.
  • numeric_date_order — day-month-year, month-day-year, or preserving dates where both fields are at most 12.
  • currency_symbol_policy — assume the common currency for $ and ¥, or preserve those ambiguous symbols.

Reporting what had to be guessed

flag_uncertain reports every place the reading involved a judgement call. flag_uncertain_with takes options and omits the warnings they already resolve; invalid-value diagnostics always remain.

use ukrainian_tn::uktextnorm::flag_uncertain;

let spans = flag_uncertain("Дата 30.02.2024");
assert!(spans.iter().any(|s| s.text == "30.02.2024"));

UncertainSpan::start and stop are byte offsets, so &source[span.start..span.stop] == span.text.

Tolerating ASR-distorted input

Speech recognition rarely hands the normalizer a clean token: the same word arrives with a missing apostrophe (дев'ятнадцятого → девятнадцятого), glued or split (ю ес бі → юесбі), or in a surzhyk / phonetic variant. Under the default InputTolerance::Strict such a token misses the exact lexicon lookups and passes through unchanged. InputTolerance::Asr adds a fallback that runs only after an exact lookup misses, resolving the token against the closed lexicon in two cheap, deterministic stages — a canonical key that folds separators and confusable spellings, then a bounded edit-distance match to the single closest entry (ties are left unresolved rather than guessed).

use ukrainian_tn::uktextnorm::{
    flag_uncertain_with, normalize_with, InputTolerance, NormalizeOptions, UncertaintyCategory,
};

let options = NormalizeOptions { input_tolerance: InputTolerance::Asr, ..Default::default() };

// A recognizer typo still reaches its reading.
assert_eq!(normalize_with("spotifay", &options), "спотіфай");

// Every approximate reading is reported, never silently guessed.
let spans = flag_uncertain_with("spotifay", &options);
assert!(spans.iter().any(|s| s.category == UncertaintyCategory::ApproximateMatch));

The fallback never runs on the hot path for clean text, and the search space is always a closed lexicon (hundreds of entries), never free text, so the behaviour is deterministic and testable by the golden corpora.

The same fallback also runs over Cyrillic input, where ASR distorts the reading itself rather than a Latin spelling:

use ukrainian_tn::uktextnorm::{normalize_with, InputTolerance, NormalizeOptions};

let options = NormalizeOptions { input_tolerance: InputTolerance::Asr, ..Default::default() };

// "ватсап" is a one-edit distortion of the canonical reading "вотсап".
assert_eq!(normalize_with("ватсап", &options), "вотсап");
// A lowercased or phonetically-spelled acronym is restored, then expanded.
assert!(normalize_with("сума пдв", &options).contains("додану вартість"));
assert!(normalize_with("сума педеве", &options).contains("додану вартість"));
// Everyday Ukrainian prose is never dragged onto a reading or acronym.
assert_eq!(normalize_with("сьогодні я пив каву", &options), "сьогодні я пив каву");

The closed target sets are foreign-shaped by design — brand/English readings and acronym keys (plus their phonetic letter-name spellings, so педеве folds back to ПДВ). The canonical key is phonetic: it folds the confusions a Ukrainian recognizer actually makes (і/ї/и, е/є, я→а, ю→у, ґ→г, the soft sign, doublings, and Russian/surzhyk carry-over), so near-homophones collapse before any edit-distance step.

Inflected ordinary words are not repaired by default, because fuzzy-matching open prose against itself would corrupt it. The universal extension point is asr_vocabulary: hand the normalizer any list of canonical Ukrainian words — a domain glossary or a full lexicon — and the same phonetic-key and bounded-edit rules repair distorted tokens against it.

use ukrainian_tn::uktextnorm::{normalize_with, InputTolerance, NormalizeOptions};

let options = NormalizeOptions {
    input_tolerance: InputTolerance::Asr,
    asr_vocabulary: vec!["автентифікація".to_owned(), "ідентифікатор".to_owned()],
    ..Default::default()
};

assert_eq!(normalize_with("автентіфікація", &options), "автентифікація");

Load the list from a one-column word TSV with load_asr_vocabulary_tsv.

A recognizer also splits or glues multi-word targets (вай фай / вайфай for вай-фай). Because the phonetic key drops separators, a split window of tokens and its glued form share one key, so a sliding-window pass rejoins either shape to the canonical target — including a multi-word entry supplied via asr_vocabulary:

# use ukrainian_tn::uktextnorm::{normalize_with, InputTolerance, NormalizeOptions};
let options = NormalizeOptions {
    input_tolerance: InputTolerance::Asr,
    asr_vocabulary: vec!["вай-фай".to_owned()],
    ..Default::default()
};
assert!(normalize_with("увімкни вай фай", &options).contains("вай-фай")); // split
assert!(normalize_with("увімкни вайфай", &options).contains("вай-фай")); // glued

Numbers

use ukrainian_tn::uktextnorm::{number_to_ordinal_words, number_to_words,
                               number_to_words_case, number_to_words_digit_by_digit,
                               GrammaticalCase, OrdinalForm};

// A bare "одна" before "тисяча" is dropped, as Ukrainian usage requires.
assert_eq!(number_to_words(1_234), "тисяча двісті тридцять чотири");
assert_eq!(number_to_words(2_234), "дві тисячі двісті тридцять чотири");
assert_eq!(number_to_ordinal_words(21, OrdinalForm::NomF), "двадцять перша");
assert_eq!(number_to_words_case(500, GrammaticalCase::Genitive), "п'ятисот");
assert_eq!(number_to_words_digit_by_digit("007"), "нуль нуль сім");

Values above MAX_SPELLED_NUMBER (999_999_999_999_999_999) are read digit by digit instead of spelled out.

Segmentation

Both entry points borrow from the input and report byte offsets, so &text[span.start..span.stop] == span.text always holds.

use ukrainian_tn::rozpodil::{split_sentences, tokenize};

let text = "м. Київ, вул. Хрещатик, 1. Зустріч о 10:30.";
assert_eq!(split_sentences(text).len(), 2);
assert_eq!(tokenize("П'ять зв'язків.").len(), 3);

Custom vocabulary

Pass a map of preferred readings to override the built-in brand and English-word lexicons for a single call. The normalize_english_words switch also controls custom readings.

use ukrainian_tn::uktextnorm::{normalize_with, NormalizeOptions};

let options = NormalizeOptions {
    vocabulary: [("google", "гуголь"), ("acme", "акме")]
        .into_iter()
        .map(|(k, v)| (k.to_owned(), v.to_owned()))
        .collect(),
    ..NormalizeOptions::default()
};
assert_eq!(normalize_with("Google і Acme", &options), "гуголь і акме");

Readings can also be loaded from a UTF-8 TSV file with the same columns as data/lexicons/brands.tsv:

latin	cyrillic
Acme	акме
Google	гуголь
use ukrainian_tn::uktextnorm::load_vocabulary_tsv;

let words = load_vocabulary_tsv("my_words.tsv")?;
# Ok::<(), ukrainian_tn::uktextnorm::VocabularyError>(())

Latin keys are single ASCII words, matched without regard to case.

Currency and cryptocurrency coverage

Normalization covers 178 ISO 4217 List One codes from the 2026-01-01 data snapshot, including their 0-, 2-, 3- and 4-digit minor-unit rules. More than 70 common cryptocurrency and finance tickers have natural Ukrainian readings. Other 2–10 character uppercase alphanumeric tickers are spelled out after amounts and when paired with a known asset, so newly introduced assets do not require an immediate release. Prefix and suffix amounts, localized thousands separators, signs, decimals and the ₿ symbol are all supported.

Lexicons

The lexicons under data/lexicons/ are the source of truth. They are embedded at compile time with include_str! and parsed once on first use, so editing a TSV and rebuilding is all that is needed to change a reading. cargo test checks each table for duplicate keys, out-of-range values and empty columns.

Development

Requires Rust 1.99 or newer (edition 2024).

cargo test
cargo clippy --all-targets --all-features -- -W clippy::pedantic -D warnings
cargo fmt --all

The test suite has four parts:

  • tests/conformance.rs — the reference assertion suite, reporting every failure at once rather than stopping at the first.
  • tests/golden.rs — the TSV corpora under tests/data/, including an idempotence check on the sentence corpus.
  • tests/robustness.rs — awkward input must not panic, empty the text, or produce spans that disagree with the source.
  • tests/rozpodil.rs and tests/vocabulary.rs — segmentation and vocabulary loading.

Benchmarks

Run the Criterion suite in the optimized bench profile:

cargo +1.99.0 bench --locked --bench text_processing -- --noplot

Baseline measured on 2026-11-02 with Rust 1.99.0, Criterion 0.8.2, fancy-regex 0.20.0, default features, and the system allocator on Linux x86_64 (Intel Core i9-9900K @ 3.60 GHz, 8 cores / 16 threads). Each benchmark uses 100 samples, a 3-second warm-up and a 5-second target measurement period. Times below are arithmetic means per call with 95% confidence intervals.

Benchmark Input Mean 95% confidence interval
normalize/mixed 193 bytes 14.119 ms 14.080–14.161 ms
normalize/asr 77 bytes 10.553 ms 10.526–10.579 ms
segmentation/tokenize 6176 bytes 65.585 µs 65.323–65.851 µs
segmentation/sentences 6176 bytes 229.882 µs 229.338–230.500 µs
number_to_words 123_456_789 819.517 ns 817.267–821.792 ns

normalize/mixed covers an address, date, time, currency, measurement, temperature and brands. normalize/asr covers distorted brand/acronym spellings and split words with ASR tolerance enabled. Segmentation processes 32 copies of the mixed input. Inputs and options are constructed outside the timed loops, normalization regexes and lexicons are initialized before timing, and std::hint::black_box protects inputs from optimization. Output allocation and destruction are included. These are local steady-state measurements; results vary with hardware and system load and exclude cold-start costs. Criterion stores raw samples and estimates under target/criterion/.

License

MIT. See LICENSE.

Metadata

Release files for ukrainian-tn 0.7.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ukrainian-tn 0.7.0
File Size Uploaded Provenance Actions
ukrainian_tn-0.7.0.tar.gz 207.0 kB With provenance Details for ukrainian_tn-0.7.0.tar.gz

Built distributions (wheels)

Table of built distributions (wheels) for ukrainian-tn 0.7.0
File Actions
ukrainian_tn-0.7.0-cp39-abi3-win_amd64.whl CPython 3.9 abi3 Windows x86-64 Details for ukrainian_tn-0.7.0-cp39-abi3-win_amd64.whl
ukrainian_tn-0.7.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl CPython 3.9 abi3 Linux glibc 2.17+ x86-64 Details for ukrainian_tn-0.7.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
ukrainian_tn-0.7.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl CPython 3.9 abi3 Linux glibc 2.17+ ARM64 Details for ukrainian_tn-0.7.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
ukrainian_tn-0.7.0-cp39-abi3-macosx_11_0_arm64.whl CPython 3.9 abi3 macOS 11.0+ ARM64 Details for ukrainian_tn-0.7.0-cp39-abi3-macosx_11_0_arm64.whl
ukrainian_tn-0.7.0-cp39-abi3-macosx_10_12_x86_64.whl CPython 3.9 abi3 macOS 10.12+ x86-64 Details for ukrainian_tn-0.7.0-cp39-abi3-macosx_10_12_x86_64.whl

Total release size: 8.7 MB

Release files / ukrainian_tn-0.7.0.tar.gz

Download URL ukrainian_tn-0.7.0.tar.gz
Size 207.0 kB
Tags Source
SHA-256 checksum
How to use checksums
3143b5cbaa1651729472cd2f6e16baf771f0e3ce146ee95e05ea61d911997a44
BLAKE2b-256 checksum
How to use checksums
ccacbdb91422ea19ee6dd2d65a7eb013f1a11a14c6487e048f015a975352fa99
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.

Transparency log

Release files / ukrainian_tn-0.7.0-cp39-abi3-win_amd64.whl

Download URL ukrainian_tn-0.7.0-cp39-abi3-win_amd64.whl
Size 1.6 MB
Tags CPython 3.9 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
3841171176426ffca418b8dfde090dedc76a5dd4cb5aedda294eda8cc98cadb2
BLAKE2b-256 checksum
How to use checksums
873335a7bb6d7bdfcd9828339aa032276e25b76074de53508dc9f83111220fe3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.

Transparency log

Release files / ukrainian_tn-0.7.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl

Download URL ukrainian_tn-0.7.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl
Size 1.8 MB
Tags CPython 3.9 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
e49e9eb566e799e8f88c9fb2754fa4d7125f3dcfd0b32ab07f4afb38c37f5df4
BLAKE2b-256 checksum
How to use checksums
b28a4b11d9a3430dfb37830f2cddb112a61fa76659f079f69f549d2838746e57
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.

Transparency log

Release files / ukrainian_tn-0.7.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl

Download URL ukrainian_tn-0.7.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl
Size 1.8 MB
Tags CPython 3.9 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
d3594e071eaef826157f164f8eacc6a5c5b6e67ecf540b645aaea0e488e6d7bb
BLAKE2b-256 checksum
How to use checksums
d05c284b55dd60c55fbbbc380ca7e10df92ca4a5cb6d3c4fd37b0f5bf0c387ff
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.

Transparency log

Release files / ukrainian_tn-0.7.0-cp39-abi3-macosx_11_0_arm64.whl

Download URL ukrainian_tn-0.7.0-cp39-abi3-macosx_11_0_arm64.whl
Size 1.6 MB
Tags CPython 3.9 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
3b268ac00122f9ecc63128d3e8eadd6065e11509219205dfd190262300125e07
BLAKE2b-256 checksum
How to use checksums
78dcbf1d589b5925c0213309939b2b3a47cfa84dbfaf36c33c70fd373e6991e9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.

Transparency log

Release files / ukrainian_tn-0.7.0-cp39-abi3-macosx_10_12_x86_64.whl

Download URL ukrainian_tn-0.7.0-cp39-abi3-macosx_10_12_x86_64.whl
Size 1.7 MB
Tags CPython 3.9 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
8fb7f8d9d4b2c2cd45934baa104632f48d86df548ad59b02eefc18749d0220cb
BLAKE2b-256 checksum
How to use checksums
98e730ed40f5246c64cab5b7724ef873855d80ed547e7c133d6638e56d3fb352
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.

Transparency log

Security for ukrainian-tn 0.7.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 6 files in this release have been uploaded with provenance metadata.

Select from 6 files

Attestation for ukrainian_tn-0.7.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.7.0 This release

6 release files Full provenance

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