Skip to main content

ukrainian-tn

CI

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.6"
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.8 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

cargo test
cargo clippy --all-targets
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.

License

MIT. See LICENSE.

Release files for ukrainian-tn 0.6.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.6.0
File Size Uploaded
ukrainian_tn-0.6.0.tar.gz 200.1 kB Details

Built distributions (wheels)

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

Total release size: 8.5 MB

Release files / ukrainian_tn-0.6.0.tar.gz

Download URL ukrainian_tn-0.6.0.tar.gz
Size 200.1 kB
Tags Source
SHA-256 checksum
How to use checksums
1b2c723fd3cbd10e87cb0e54d877b89896d345dea7d0284821d1f52a125c6a84
BLAKE2b-256 checksum
How to use checksums
ebc6ece6d745bca0314ece7cfe5d659ee40780106e927412b4b15ba0f6cce008
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 Sep 21, 2026.

Transparency log

Release files / ukrainian_tn-0.6.0-cp38-abi3-win_amd64.whl

Download URL ukrainian_tn-0.6.0-cp38-abi3-win_amd64.whl
Size 1.5 MB
Tags CPython 3.8 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
82b4c8fb26915b87cee4890a36714aefe2069e620180f8d00632a976aa7b64d3
BLAKE2b-256 checksum
How to use checksums
0a44ed896985d4b6dacd05044c1c1bdbb7ddfb53c7a1915cdb45bcb79989dfda
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 Sep 21, 2026.

Transparency log

Release files / ukrainian_tn-0.6.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL ukrainian_tn-0.6.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 1.8 MB
Tags CPython 3.8 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
e67f7bf4b568568eba81588afdda94b96572b2c1c81e3e515c31ab3cad2a97e8
BLAKE2b-256 checksum
How to use checksums
05172e31e72667517c012442bcec8ef4acf758a7f914a9ca009ca00165a47c7d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 Sep 21, 2026.

Transparency log

Release files / ukrainian_tn-0.6.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL ukrainian_tn-0.6.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 1.7 MB
Tags CPython 3.8 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
66cd47ef43a950205a883707519f53b2935d38722c47bcfac2746a93009e2433
BLAKE2b-256 checksum
How to use checksums
f2c94098310fdbb787dd814aa72a62fcc113de824c9e8d5fab63cccdecfa3d81
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 Sep 21, 2026.

Transparency log

Release files / ukrainian_tn-0.6.0-cp38-abi3-macosx_11_0_arm64.whl

Download URL ukrainian_tn-0.6.0-cp38-abi3-macosx_11_0_arm64.whl
Size 1.6 MB
Tags CPython 3.8 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
c373ea7f1624db29b5d015835a2e9aeaa0d455884febbdaa93df5288b596ad84
BLAKE2b-256 checksum
How to use checksums
0a5c17787e839a57fb2ff677a06226cafcb365665b90c902db0bb07ea7ec1d14
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 Sep 21, 2026.

Transparency log

Release files / ukrainian_tn-0.6.0-cp38-abi3-macosx_10_12_x86_64.whl

Download URL ukrainian_tn-0.6.0-cp38-abi3-macosx_10_12_x86_64.whl
Size 1.7 MB
Tags CPython 3.8 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
3b468046bac936af905c60dcaa822ea7018e86441babf021d16d7bbd39083b9f
BLAKE2b-256 checksum
How to use checksums
21ed13e5f5fcd7d21ee85bddb19c3e772ec5f136bd9cb79d15252f2862ac60b0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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 Sep 21, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.6.0 This release

6 release 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