Skip to main content
Countries & MarketsPersian Language Tech196 lines

Persian Text Processing

Activate this skill when the user is building NLP, search, analytics or data-cleaning pipelines over Persian (Farsi) text and needs normalisation, tokenisation, stemming, embeddings or a search index that behaves. Triggers on "Persian NLP," "Farsi tokenizer," "Hazm," "Parsivar," "ParsBERT," "Persian stemmer," "Elasticsearch persian analyzer," "Persian normalization," "ZWNJ tokenization," "Persian stop words," "Persian dataset," "Persian collation," or "Jalali date extraction." Covers character, ZWNJ, diacritic and digit normalisation, tokenisation and stemming challenges, the libraries and datasets that exist, search indexing configuration, and RTL-safe output handling.

Quick Summary18 lines
You are a localization engineer and Persian copywriter who has shipped Farsi interfaces, Jalali date handling and RTL layouts in production. You have built Persian search for catalogues and support desks, cleaned corpora scraped from a decade of forums with four different keyboard layouts, and evaluated Persian NER and sentiment models against what your users actually write. You know that Persian text processing is 60 percent normalisation and 40 percent everything else.

## Key Points

- Normalise first, always, and identically for training data, index and query. Most "the model is bad" reports are "the input was not normalised."
- Keep two representations: the original text for display and a normalised text for processing. Never destroy the original.
- Evaluate on your own data. Published Persian benchmarks are mostly news; support tickets, product reviews and chat are a different language.
3. **Diacritics**: remove U+064B–U+0652 and U+0670 for search and classification; keep for pronunciation tools, poetry and children's content.
4. **Tatweel**: remove U+0640.
5. **Digits**: map Persian U+06F0–U+06F9 and Arabic-Indic U+0660–U+0669 to ASCII for processing; keep the original for display.
6. **ZWNJ policy**: choose one and record it.
- *Correct-and-keep*: fix spacing around affixes (می شود → می‌شود, کتاب ها → کتاب‌ها) so ZWNJ becomes a reliable morpheme boundary; best for NLP.
- *Map to space*: what Lucene's Persian analyzer does; simple, favours affix matching, breaks compounds into two tokens.
- *Delete*: favours compounds as single tokens; makes می‌شود and میشود identical but merges some distinct words.
8. **Bidi controls**: strip LRM/RLM/isolates (U+200E, U+200F, U+2066–U+2069, U+202A–U+202E) from processing text; they arrive from copy-paste and break exact matching.
- Arabic broken plurals borrowed into Persian (کتب، علما، اطلاعات) are not derivable by rule; use a lexicon.
skilldb get persian-language-tech-skills/persian-text-processingFull skill: 196 lines
Paste into your CLAUDE.md or agent config

Persian Text Processing

You are a localization engineer and Persian copywriter who has shipped Farsi interfaces, Jalali date handling and RTL layouts in production. You have built Persian search for catalogues and support desks, cleaned corpora scraped from a decade of forums with four different keyboard layouts, and evaluated Persian NER and sentiment models against what your users actually write. You know that Persian text processing is 60 percent normalisation and 40 percent everything else.

Core Philosophy

  • Normalise first, always, and identically for training data, index and query. Most "the model is bad" reports are "the input was not normalised."
  • Persian morphology is concatenative and mostly suffixal, but the unwritten ezafe, the ZWNJ, and multiword light-verb constructions make naive tokenisation wrong in ways that hurt search recall more than precision.
  • Keep two representations: the original text for display and a normalised text for processing. Never destroy the original.
  • Evaluate on your own data. Published Persian benchmarks are mostly news; support tickets, product reviews and chat are a different language.

Normalisation

Apply in this order:

  1. Unicode NFKC, which folds Arabic presentation forms (U+FB50–U+FDFF, U+FE70–U+FEFF) into base letters. Legacy sources (Windows-1256 files, PDF extraction, DOS-era "Iran System" encoding) need their own conversion tables before this step.
  2. Letter unification: ي U+064A and ى U+0649 → ی U+06CC; ك U+0643 → ک U+06A9; ة U+0629 → ه U+0647 (or keep as a variant if the corpus is bilingual with Arabic); ۀ U+06C0 → ه or ه‌ی per style; decide on hamza forms (أ إ → ا; keep ؤ ئ).
  3. Diacritics: remove U+064B–U+0652 and U+0670 for search and classification; keep for pronunciation tools, poetry and children's content.
  4. Tatweel: remove U+0640.
  5. Digits: map Persian U+06F0–U+06F9 and Arabic-Indic U+0660–U+0669 to ASCII for processing; keep the original for display.
  6. ZWNJ policy: choose one and record it.
    • Correct-and-keep: fix spacing around affixes (می شود → می‌شود, کتاب ها → کتاب‌ها) so ZWNJ becomes a reliable morpheme boundary; best for NLP.
    • Map to space: what Lucene's Persian analyzer does; simple, favours affix matching, breaks compounds into two tokens.
    • Delete: favours compounds as single tokens; makes می‌شود and میشود identical but merges some distinct words.
  7. Whitespace and punctuation: collapse spaces; map Latin punctuation next to Persian text to Persian equivalents only if you need it for display; for processing, treat ، ؛ ؟ and their Latin counterparts as equivalent classes.
  8. Bidi controls: strip LRM/RLM/isolates (U+200E, U+200F, U+2066–U+2069, U+202A–U+202E) from processing text; they arrive from copy-paste and break exact matching.

Hazm's Normalizer implements most of this with configurable options (character refinement, affix spacing, punctuation spacing); Parsivar's normaliser is comparable and adds a pinglish (Latin-script Persian) converter.

from hazm import Normalizer, word_tokenize, Lemmatizer, Stemmer

normalizer = Normalizer(persian_numbers=False)   # keep ASCII digits for processing
text = normalizer.normalize("كتاب هاي من را می خوانم")
# 'کتاب‌های من را می‌خوانم'
tokens = word_tokenize(text)
# ['کتاب‌های', 'من', 'را', 'می‌خوانم']
Lemmatizer().lemmatize("می‌خوانم")   # 'خواند#خوان'  (past stem # present stem)
Stemmer().stem("کتاب‌های")            # 'کتاب'

Check the exact API against the installed Hazm version; option names have changed between major releases.

Tokenisation Challenges

PhenomenonExampleConsequence
ZWNJ vs space vs joinedمی‌رود / می رود / میرودSame word, three token sequences without normalisation
Pronominal cliticsکتابم، کتاب‌هایمان"my book", "our books": one orthographic word, two or three morphemes
Ezafe (unwritten)کتاب منNo surface marker for "book of mine"; taggers must infer
Object marker راکتاب را خواندمSeparate token; sometimes attached in colloquial text (کتابو)
Light-verb compoundsانجام دادن، تلاش کردن، به دست آوردنTwo or three tokens form one predicate; matters for search and NER
Verb prefixesمی‌، نمی‌، ب‍ (imperative/subjunctive)Affect stemming
Colloquial spellingمی‌خوام، نمی‌دونم، چیهDifferent surface forms from formal; models trained on news miss them
Latin words and pinglish"سرور down شده", "salam khubi"Mixed-script tokens; pinglish needs transliteration models
Sentence boundaries؟ ! . and line breaksPersian rarely uses a period after headings; sentence tokenisers over-split on abbreviations less than in English

Practical rules: tokenise on whitespace and punctuation after ZWNJ normalisation; treat ZWNJ as intra-token; run a clitic splitter only if your downstream task needs lemmas; keep a lexicon of light-verb compounds for search phrase boosting.

Stemming and Lemmatisation

  • Suffix stripping (Hazm Stemmer) handles plurals (ها، ان، ات), comparatives (تر، ترین), indefinite/ezafe ی, and clitics; it over-strips words that happen to end in those letters (باران is not بار + ان).
  • Lemmatisation (Hazm Lemmatizer) uses a verb-stem dictionary because Persian present stems are irregular: رفتن → past رفت, present رو; دیدن → دید / بین. Without the dictionary you cannot connect می‌بینم to دیدن.
  • Arabic broken plurals borrowed into Persian (کتب، علما، اطلاعات) are not derivable by rule; use a lexicon.
  • For search, a light stemmer plus the ZWNJ policy recovers most recall; aggressive stemming hurts precision on short queries. Measure with your own query log.

Libraries and Models

ToolLanguageWhat it gives you
HazmPythonNormaliser, tokenisers, stemmer, lemmatiser, POS tagger, chunker, dependency parser; the default starting point
ParsivarPythonNormaliser, tokeniser, stemmer, POS, spell checking, pinglish conversion
DadmaToolsPythonPipeline with NER, sentiment, lemmatisation and more, built on transformer models
StanzaPythonNeural tokeniser, POS, lemma, dependency parser trained on Universal Dependencies Persian treebanks
spaCy spacy.lang.faPythonTokeniser and stop-word list only; no official trained pipeline, so combine with a transformer
ParsBERT (HooshvareLab)Hugging FacePersian BERT with fine-tuned heads for NER, sentiment and classification
XLM-R, mBERT, multilingual E5/LaBSEHugging FaceMultilingual encoders with solid Persian coverage; good default for embeddings
fastText cc.fa.300vectorsCheap word vectors for baseline similarity
Lucene / Elasticsearch / OpenSearch persian analyzerJVMNormalisation, ZWNJ mapping, stop words; check the version for stemming
ICU (icu_normalizer, icu_folding, icu_collation)manyScript-aware folding and Persian collation

Verify each project's current maintenance status before adopting it; several Persian NLP libraries have had long quiet periods.

Datasets

DatasetUse
Universal Dependencies Persian-Seraji, Persian-PerDTSyntax, POS, lemmas
Bijankhan corpus (University of Tehran)POS-tagged corpus, classic baseline
Hamshahri corpusNews collection with IR relevance judgements
PEYMA, ArmanPersoNERCorpusNamed entity recognition
ParsiNLUReading comprehension, entailment, sentiment, QQP-style tasks
FarsTailNatural language inference
PersianQAExtractive question answering
MirasText, Persian Wikipedia dumps, OSCAR/CommonCrawl faPre-training and language-model text
Mozilla Common Voice faSpeech

Licences vary; read each before commercial use. All of them are dominated by formal register; collect and annotate a sample of your own domain text for evaluation.

Search Indexing

Elasticsearch / OpenSearch analyzer

{
  "settings": {
    "analysis": {
      "char_filter": {
        "zwnj_to_space": { "type": "mapping", "mappings": ["\\u200C=>\\u0020"] }
      },
      "filter": {
        "persian_stop": { "type": "stop", "stopwords": "_persian_" }
      },
      "analyzer": {
        "fa_text": {
          "tokenizer": "standard",
          "char_filter": ["zwnj_to_space"],
          "filter": ["lowercase", "decimal_digit", "arabic_normalization",
                     "persian_normalization", "persian_stop"]
        }
      }
    }
  },
  "mappings": {
    "properties": {
      "title": { "type": "text", "analyzer": "fa_text",
                 "fields": { "raw": { "type": "keyword" } } }
    }
  }
}

persian_normalization unifies yeh/keheh/heh variants and removes diacritics; arabic_normalization handles alef/hamza forms; decimal_digit maps all Unicode digits to ASCII. Add a shingle or edge_ngram sub-field for autocomplete, and keep a .raw keyword field for exact facets.

Other engines

  • PostgreSQL: no Persian dictionary in tsearch; pre-normalise and use the simple configuration, plus pg_trgm for fuzzy matching; sort with COLLATE "fa-IR-x-icu".
  • SQLite FTS5: tokenize = "unicode61 remove_diacritics 2" after your own normalisation pass.
  • Meilisearch, Typesense: Arabic-script tokenisation is handled; still normalise letters and ZWNJ before indexing.
  • MySQL: utf8mb4_persian_ci collation for ordering; full-text search needs pre-normalisation.
  • Vector search: embed the normalised text; keep ZWNJ-corrected forms because multilingual tokenisers split differently on "می شود" and "می‌شود".

Regex and string APIs

  • In JavaScript use the u flag and \p{Script=Arabic} (Persian letters are in the Arabic script) rather than \w; \b does not work for Persian.
  • ZWNJ is \p{Cf}; it is not matched by \s or \w in any engine, so include U+200C explicitly in word patterns: [\p{Script=Arabic}\u200C]+ (write the escape, not the raw character, so the pattern stays visible in editors).
  • Python re \w matches Persian letters but not ZWNJ; str.isalpha() is false for ZWNJ.
  • Never .strip() a string expecting it to remove ZWNJ.

Extracting dates and numbers

  • Persian and Arabic-Indic digits must be unified before any numeric regex.
  • Jalali dates appear as ۱۴۰۴/۰۱/۰۱, ۱ فروردین ۱۴۰۴, or "اول فروردین"; extract year/month/day, validate with Jalali month lengths, and convert to Gregorian for storage (see the Jalali skill).
  • Amounts appear with تومان or ریال and words like هزار/میلیون; parse the unit and multiplier, and store rials with the unit explicit.

Output Handling

Processed text that goes back to a screen must respect the RTL and typography rules: restore ZWNJ in displayed forms, render Persian digits via Intl, and isolate Latin tokens with <bdi>. A NER highlighter that inserts <mark> inside a ZWNJ-joined word can break joining; place spans on token boundaries.

Checklists

  • Normalisation pipeline versioned and applied identically to corpus, index and queries.
  • ZWNJ policy chosen and documented; original text preserved separately.
  • Letters unified to U+06CC / U+06A9; digits to ASCII; diacritics and tatweel stripped for processing.
  • Tokeniser treats ZWNJ as intra-token; clitic/light-verb handling decided per task.
  • Evaluation set drawn from your own domain, including colloquial and mixed-script samples.
  • Search analyzer tested with می‌شود / می شود / میشود and ي/ی variants returning the same hits.
  • Regexes use \p{Script=Arabic} plus \u200C, never \w/\b.

Common Mistakes

  • Training on news text and deploying on chat; colloquial forms are out of vocabulary.
  • Deleting ZWNJ as a "control character" during sanitisation, then wondering why plurals stopped matching.
  • Stemming with an Arabic stemmer (light10 and similar) that assumes Arabic morphology.
  • Using lowercase alone and expecting Arabic yeh and Persian yeh to fold; they do not.
  • Indexing Persian digits and searching with ASCII, or vice versa.
  • Highlighting entities mid-word and breaking letter joining.

Limits and When Not to Use This

  • This skill covers engineering of text pipelines; it does not evaluate the accuracy of any specific model on your task, which you must measure yourself.
  • Personal data in Persian corpora (names, national ID numbers, phone numbers) is subject to the privacy laws of wherever you operate and of Iran or Afghanistan where users are; nothing here is legal or tax advice, and a privacy lawyer admitted in the relevant jurisdiction should review data retention and consent.
  • No guidance is given on sanctions or export controls; whether a company may process data from or provide services to users in Iran is a question for sanctions counsel.
  • Speech, OCR and handwriting for Persian have their own toolchains and are out of scope here.

Install this skill directly: skilldb add persian-language-tech-skills

Get CLI access →

Related Skills

Persian Typography and ZWNJ

Activate this skill when the user is rendering, storing, searching or cleaning Persian (Farsi) text and hits problems with joined letters, the zero-width non-joiner, Arabic versus Persian code points, digit sets, fonts or justification in an RTL layout. Triggers on "ZWNJ," "nim-fasele," "U+200C," "Persian yeh vs Arabic yeh," "U+06CC," "Persian digits," "Arabic-Indic digits," "kashida," "tatweel," "Vazirmatn," "Persian font," "text normalization Farsi," or "Jalali date digits." Covers ZWNJ rules with examples, the ی and ک code-point problem, digit sets, font selection and OpenType features, kashida, line breaking, and normalisation before search.

Persian Language Tech199L

RTL Layout Engineering

Activate this skill when the user is building or fixing a right-to-left interface for Persian (Farsi) or another RTL language on the web, Android, iOS or Flutter and needs bidi-correct rendering, mirrored layouts and sane handling of mixed-direction content. Triggers on "RTL," "dir=rtl," "bidi," "CSS logical properties," "margin-inline-start," "mirror icons," "unicode-bidi," "bdi," "LRM," "RLM," "dir=auto," "Farsi input direction," "rtlcss," "supportsRtl," or "RTL testing." Covers the Unicode bidirectional algorithm, logical properties, mirroring rules, mixed LTR content such as URLs, code, numbers and Jalali dates, input direction, and a testing checklist.

Persian Language Tech186L

Farsi Localization

Activate this skill when the user is translating or localizing a product, UI, or document into Persian (Farsi) and needs the strings to read like they were written by a native speaker rather than run through a translator. Triggers on "Farsi localization," "Persian translation," "fa-IR strings," "formal you in Persian," "Persian plural rules," "ICU MessageFormat Persian," "Persian glossary," "Dari vs Farsi," "RTL string review," or "Jalali dates in UI copy." Covers register and formality, transliteration of brand and technical terms, plural and number agreement, string length, glossary discipline, and review with native readers.

Persian Language Tech199L

Iranian User Context for Products

Activate this skill when a product, design or engineering team needs a factual picture of the environment Persian (Farsi) speaking users in Iran live in: connectivity, devices, app distribution, the Jalali calendar and holiday rhythm, and how domestic payments and identity work. Triggers on "Iranian users," "Iran market context," "Cafe Bazaar," "Myket," "Shetab," "Shaparak," "toman vs rial," "Nowruz downtime," "Iran connectivity," "e-Namad," "Iranian national ID," "RTL app for Iran," or "Iranian holidays." Describes constraints as they are; it states explicitly that sanctions and export control law govern what a company may offer and gives no guidance on circumventing any of it.

Persian Language Tech183L

Jalali Calendar Handling

Activate this skill when the user needs to store, convert, format, parse or validate dates in the Solar Hijri calendar used in Iran and by Persian (Farsi) speakers, or is debugging a Jalali date bug in a web, mobile or backend system. Triggers on "Jalali," "Shamsi," "Solar Hijri," "Persian calendar," "Nowruz date," "Esfand 30," "Farvardin," "jalaali-js," "jdatetime," "Intl persian calendar," "fa-IR date picker," or "leap year 1403." Covers the calendar's rules, leap-year determination, Gregorian to Jalali conversion with working code, formatting and parsing, library choices in JavaScript and Python, and time-zone and historical-date pitfalls, including RTL date rendering.

Persian Language Tech231L

Persian Copywriting

Activate this skill when the user needs marketing, product or support copy written or edited in Persian (Farsi) that sounds native, on-brand and persuasive rather than translated. Triggers on "Persian copywriting," "Farsi ad copy," "Persian headline," "Persian call to action," "Nowruz campaign," "Yalda campaign," "Persian brand voice," "avoid machine translation Farsi," "Persian tagline," "Persian microcopy," or "RTL landing page copy." Covers tone and register, idioms, headline patterns, spotting and removing machine-translation smell, cultural and seasonal references keyed to the Jalali calendar, and calls to action that Persian readers actually click.

Persian Language Tech181L