Skip to content

← Data and representation

How Does Romanized Nepali Typing Work?

Topic 24 · Grades 9–12 · Transliteration

You need lists and dictionaries (Grade 9).

What this builds on from class
In this unit 8 sections
  1. Try it
  2. How it works
  3. Program it
  4. What your program shows
  5. Going further still
  6. Challenges
  7. Questions
  8. Where to go next

You want to text your friend नमस्ते, but your phone shows English letters. So you switch on a Nepali keyboard that lets you type what you hear: n a m a s t e. The word नमस्ते appears. Seven English letters went in, and six Nepali codes were stored. Something on the phone cut your letters into pieces and swapped each piece for Nepali.

That something is a transliterator: a program that turns text written in one script into another script, by sound. The one below follows a small table of rules. Type a word, or pick one, and tap a piece to see how it was found.

Try it

Type in Roman letters

Type in small letters (capitals mean something special, as you will see). Each tile is one piece of what you typed, with what it adds to the Nepali.

नमस्ते
Letters typed
7
Pieces
7
Code points stored
6

7 letters, 7 pieces: नमस्ते, stored as 6 code points. Tap a piece to see how it was found.

At letter 1 the program looked up “nam” ✗, “na” ✗, “n” ✓. “n” is the consonant न. It says “na” by itself until a vowel sign or a halant changes it.

At letter 2 the program looked up “ama” ✗, “am” ✗, “a” ✓. “a” after a consonant adds nothing: the consonant already says “a”.

At letter 3 the program looked up “mas” ✗, “ma” ✗, “m” ✓. “m” is the consonant म. It says “ma” by itself until a vowel sign or a halant changes it.

At letter 4 the program looked up “ast” ✗, “as” ✗, “a” ✓. “a” after a consonant adds nothing: the consonant already says “a”.

At letter 5 the program looked up “ste” ✗, “st” ✗, “s” ✓. “s” is the consonant स. It says “sa” by itself until a vowel sign or a halant changes it.

At letter 6 the program looked up “te” ✗, “t” ✓. “t” is त. A consonant came just before it, so a halant ◌् goes first and joins the two: ◌्त.

At letter 7 the program looked up “e” ✓. “e” after a consonant is its vowel sign ◌े, stored after the consonant.

16 letters, 16 pieces: क्ष्क्ष्क्ष्क्ष्क्ष्क्ष्, stored as 32 code points. Tap a piece to see how it was found.

At letter 16 the program looked up “chh” ✗, “ch” ✗, “c” ✗. “ch” is च. A consonant came just before it, so a halant ◌् goes first and joins the two: ◌्च.

At letter 16 the program looked up “shh” ✗, “sh” ✗, “s” ✓. “sh” is the consonant ष. It says “sha” by itself until a vowel sign or a halant changes it.

Before you try it: the table has t, h and th in it. What do you think tha becomes?

Pick an answer to see why.

You need lists, strings and dictionaries in Python (Grade 9). Everything about how Nepali is stored is explained here.

Try it

Type shikshaa and watch the tiles. The program found sh, then i, then ksh (three letters, the longest piece in its table), then aa. Now type kh and chh: one tile each.

The order the program looks in matters. Switch the widget below to One letter at a time, and type tha, kh and bhaktapur again.

Try it

One piece at a time

Compare the two ways of cutting the same letters.

थ
Letters typed
3
Pieces
2
Code points stored
1

3 letters, 2 pieces: थ, stored as 1 code point. Tap a piece to see how it was found.

At letter 1 the program looked up “tha” ✗, “th” ✓. “th” is the consonant थ. It says “tha” by itself until a vowel sign or a halant changes it.

At letter 3 the program looked up “a” ✓. “a” after a consonant adds nothing: the consonant already says “a”.

16 letters, 16 pieces: क्ष्क्ष्क्ष्क्ष्क्ष्क्ष्, stored as 32 code points. Tap a piece to see how it was found.

At letter 16 the program looked up “chh” ✗, “ch” ✗, “c” ✗. “ch” is च. A consonant came just before it, so a halant ◌् goes first and joins the two: ◌्च.

At letter 16 the program looked up “shh” ✗, “sh” ✗, “s” ✓. “sh” is the consonant ष. It says “sha” by itself until a vowel sign or a halant changes it.

Now type nepal in the first widget.

nepal comes out as नेपल, not नेपाल. Why?

Pick an answer to see why.

How it works

What the phone stores

A transliterator’s output is Unicode, the same codes you met in Grade 7 and Grade 9 (How Nepali is stored, Characters and Unicode). Three facts about Devanagari in Unicode decide what it must produce:

  • Every consonant carries a vowel inside it. Unicode’s own description says that क stands for /ka/, not only /k/. This built-in vowel is called the inherent vowel.
  • A vowel sign (मात्रा), such as े or ा, replaces the inherent vowel. It is stored after its consonant, in the order you say it, even when it is drawn on the left (कि is क then ि).
  • The halant (◌्) takes the inherent vowel away. A consonant, a halant and another consonant make a conjunct: क, ◌्, ष is क्ष. Unicode has no single code for क्ष or ज्ञ. They are drawn as one shape by the font, but stored as three codes each.

Try it

What the pieces store

Pick क्ष: three code points. Then त्ह: also three codes, which a font draws as a half त joined to ह. It is valid Unicode, but not a Nepali word.

क U+0915 ◌् U+094D ष U+0937
Characters (numbers stored)
3

Type something, or pick one of the buttons.

Every English letter, digit, space and punctuation mark has its own number. A capital and a small letter have different numbers.

Every character, of every script, is stored as its own number.

A Nepali syllable is stored as several numbers: a letter, then its vowel sign or a halant (्) and the next letter.

Each English character is a number under 128, so it fits in one byte.

English characters take 1 byte each, but every Devanagari letter or sign takes 3 bytes: its number is over 2,047.

An emoji’s number is over 65,535, so it takes 4 bytes. Nepali letters take 3, English ones 1.

The bigger a character’s number, the more bytes it takes: 1, 2, 3 or 4.

So typing is a translation from one list of sounds to another: Roman letters in, Unicode code points out. Notice that shikshaa is 8 letters and शिक्षा is 6 code points: sh and aa are two letters for one code each, while ksh is three letters for three codes.

Longest piece first

The program’s table is a Python dictionary: each key is a piece of Roman letters, each value what it gives in Nepali. Most pieces are one letter, some two (kh, th, aa, sh), a few three (chh, ksh). Capital letters are the retroflex sounds, said with the tongue curled back: T is ट, D ड, N ण, Sh ष. Small t is त.

The program reads the typed text from left to right. At each place it tries the longest pieces the table has, 3 letters, then 2, then 1, and takes the first one it finds. This rule is called longest match first (or maximal munch), and many programs that cut text into pieces use it, from transliterators to the part of Python that reads your programs: it reads **= as one piece, not as *, * and =.

Putting the pieces together

Once the text is cut into pieces, four rules turn them into Unicode, in order:

  • a.A consonant after another consonant gets a halant before it, so the two join: s then t is स्त.
  • b.A vowel after a consonant gives its vowel sign (e gives े), and a gives nothing at all.
  • c.A vowel anywhere else gives the vowel letter itself: aama begins with आ.
  • d.A consonant at the end of a word stays as it is, with its inherent vowel. Nepali writes नेपाल, not नेपाल्, though nobody says the last a.

Rule d. is a choice. Many words do end in a halant: छन् (they are), सन् (the year), विद्युत् (electricity). Our rules can’t type those; a real keyboard has a key for the halant, or learns them as words.

Using the four rules, what does ramro become?

Pick an answer to see why.

Program it

You will write the transliterator in two steps, test it on twenty words as people really type them, and then teach it to choose with a list of words.

Step 1: the table, and longest match first

pieces(text) gives back the pieces, longest first. The for loop tries 3, 2 and 1 letters; break stops it at the first piece in the table. If no piece fits (a space, or a letter like x that isn’t in the table), the loop ends with one letter, and that letter is kept as it is. Notice i = i + len(piece): near the end of the text, text[i:i + 3] may hold fewer than 3 letters.

VOWELS = {'a': ('अ', ''), 'aa': ('आ', 'ा'), 'i': ('इ', 'ि'), 'ee': ('ई', 'ी'),          'u': ('उ', 'ु'), 'oo': ('ऊ', 'ू'), 'e': ('ए', 'े'), 'ai': ('ऐ', 'ै'),          'o': ('ओ', 'ो'), 'au': ('औ', 'ौ')}CONSONANTS = {'k': 'क', 'kh': 'ख', 'g': 'ग', 'gh': 'घ', 'ch': 'च', 'chh': 'छ',              'j': 'ज', 'jh': 'झ', 'T': 'ट', 'Th': 'ठ', 'D': 'ड', 'Dh': 'ढ',              'N': 'ण', 't': 'त', 'th': 'थ', 'd': 'द', 'dh': 'ध', 'n': 'न',              'p': 'प', 'ph': 'फ', 'f': 'फ', 'b': 'ब', 'bh': 'भ', 'm': 'म',              'y': 'य', 'r': 'र', 'l': 'ल', 'w': 'व', 'v': 'व', 'sh': 'श',              'Sh': 'ष', 's': 'स', 'h': 'ह', 'ksh': 'क्ष', 'gy': 'ज्ञ'}SIGNS = {'M': 'ं', '~': 'ँ'}LONGEST = 3 def in_table(piece):    return piece in VOWELS or piece in CONSONANTS or piece in SIGNS def pieces(text):    found = []    i = 0    while i < len(text):        for size in range(LONGEST, 0, -1):            piece = text[i:i + size]            if in_table(piece):                break        found.append(piece)        i = i + len(piece)    return found print(pieces('namaste'))print(pieces('shiksha'))print(pieces('tha'))

Output

['n', 'a', 'm', 'a', 's', 't', 'e']['sh', 'i', 'ksh', 'a']['th', 'a']

Step 2: the four rules

join(parts) puts the pieces together by the four rules, remembering in after_consonant whether the last piece was a consonant. transliterate(text) does both steps.

VOWELS = {'a': ('अ', ''), 'aa': ('आ', 'ा'), 'i': ('इ', 'ि'), 'ee': ('ई', 'ी'),          'u': ('उ', 'ु'), 'oo': ('ऊ', 'ू'), 'e': ('ए', 'े'), 'ai': ('ऐ', 'ै'),          'o': ('ओ', 'ो'), 'au': ('औ', 'ौ')}CONSONANTS = {'k': 'क', 'kh': 'ख', 'g': 'ग', 'gh': 'घ', 'ch': 'च', 'chh': 'छ',              'j': 'ज', 'jh': 'झ', 'T': 'ट', 'Th': 'ठ', 'D': 'ड', 'Dh': 'ढ',              'N': 'ण', 't': 'त', 'th': 'थ', 'd': 'द', 'dh': 'ध', 'n': 'न',              'p': 'प', 'ph': 'फ', 'f': 'फ', 'b': 'ब', 'bh': 'भ', 'm': 'म',              'y': 'य', 'r': 'र', 'l': 'ल', 'w': 'व', 'v': 'व', 'sh': 'श',              'Sh': 'ष', 's': 'स', 'h': 'ह', 'ksh': 'क्ष', 'gy': 'ज्ञ'}SIGNS = {'M': 'ं', '~': 'ँ'}LONGEST = 3 def in_table(piece):    return piece in VOWELS or piece in CONSONANTS or piece in SIGNS def pieces(text):    found = []    i = 0    while i < len(text):        for size in range(LONGEST, 0, -1):            piece = text[i:i + size]            if in_table(piece):                break        found.append(piece)        i = i + len(piece)    return found def join(parts):    out = ''    after_consonant = False    for piece in parts:        if piece in CONSONANTS:            if after_consonant:                out = out + '्'            out = out + CONSONANTS[piece]            after_consonant = True        elif piece in VOWELS:            letter, sign = VOWELS[piece]            if after_consonant:                out = out + sign            else:                out = out + letter            after_consonant = False        else:            out = out + SIGNS.get(piece, piece)            after_consonant = False    return out def transliterate(text):    return join(pieces(text)) for word in ['namaste', 'bhaktapur', 'shikshaa', 'nepal']:    nepali = transliterate(word)    print(word, '→', nepali, len(nepali), 'code points')

Output

namaste → नमस्ते 6 code pointsbhaktapur → भक्तपुर 7 code pointsshikshaa → शिक्षा 6 code pointsnepal → नेपल 4 code points

The board below takes transliterate from the program just above, as you have it (press Edit above to change it), and calls it with whatever you type. Change a rule, say, leave out the halant, and see what happens to namaste.

Try it

Your program beside the page’s

The tiles and the big word are the page’s own program. Your program’s answer is under them.

नमस्ते
Letters typed
7
Pieces
7
Code points stored
6

7 letters, 7 pieces: नमस्ते, stored as 6 code points. Tap a piece to see how it was found.

At letter 1 the program looked up “nam” ✗, “na” ✗, “n” ✓. “n” is the consonant न. It says “na” by itself until a vowel sign or a halant changes it.

At letter 2 the program looked up “ama” ✗, “am” ✗, “a” ✓. “a” after a consonant adds nothing: the consonant already says “a”.

At letter 3 the program looked up “mas” ✗, “ma” ✗, “m” ✓. “m” is the consonant म. It says “ma” by itself until a vowel sign or a halant changes it.

At letter 4 the program looked up “ast” ✗, “as” ✗, “a” ✓. “a” after a consonant adds nothing: the consonant already says “a”.

At letter 5 the program looked up “ste” ✗, “st” ✗, “s” ✓. “s” is the consonant स. It says “sa” by itself until a vowel sign or a halant changes it.

At letter 6 the program looked up “te” ✗, “t” ✓. “t” is त. A consonant came just before it, so a halant ◌् goes first and joins the two: ◌्त.

At letter 7 the program looked up “e” ✓. “e” after a consonant is its vowel sign ◌े, stored after the consonant.

16 letters, 16 pieces: क्ष्क्ष्क्ष्क्ष्क्ष्क्ष्, stored as 32 code points. Tap a piece to see how it was found.

At letter 16 the program looked up “chh” ✗, “ch” ✗, “c” ✗. “ch” is च. A consonant came just before it, so a halant ◌् goes first and joins the two: ◌्च.

At letter 16 the program looked up “shh” ✗, “sh” ✗, “s” ✓. “sh” is the consonant ष. It says “sha” by itself until a vowel sign or a halant changes it.

Your transliterate: —

Press Ask your program: the page calls your transliterate with what is typed above and shows what it gives back.

Loading Python (about 6 MB, only the first time)…

Asking your transliterate…

Your transliterate gives the same as the page’s. Try other words: it follows your typing now.

Your transliterate gives something other than the page’s नमस्ते. Compare them piece by piece.

No program above this widget defines transliterate. Write one (Edit), then press Ask your program.

Your program ran, but it has no function called transliterate.

Your program stopped before it could be asked. Its message is below.

Your transliterate stopped with an error. Python’s message is below.

Your transliterate took too long and was stopped: is a loop never moving on?

Your transliterate should give back a string of Nepali letters.

What your transliterate prints, and any error, appears here.

Step 3: twenty words, as people type them

People don’t type nepaal or dhanyawaad. They type nepal and dhanyabad, with one a for both vowels, t for both त and ट, b where Nepali writes व. Here are twenty everyday words, each typed the way most people type it, and the word they mean.

VOWELS = {'a': ('अ', ''), 'aa': ('आ', 'ा'), 'i': ('इ', 'ि'), 'ee': ('ई', 'ी'),          'u': ('उ', 'ु'), 'oo': ('ऊ', 'ू'), 'e': ('ए', 'े'), 'ai': ('ऐ', 'ै'),          'o': ('ओ', 'ो'), 'au': ('औ', 'ौ')}CONSONANTS = {'k': 'क', 'kh': 'ख', 'g': 'ग', 'gh': 'घ', 'ch': 'च', 'chh': 'छ',              'j': 'ज', 'jh': 'झ', 'T': 'ट', 'Th': 'ठ', 'D': 'ड', 'Dh': 'ढ',              'N': 'ण', 't': 'त', 'th': 'थ', 'd': 'द', 'dh': 'ध', 'n': 'न',              'p': 'प', 'ph': 'फ', 'f': 'फ', 'b': 'ब', 'bh': 'भ', 'm': 'म',              'y': 'य', 'r': 'र', 'l': 'ल', 'w': 'व', 'v': 'व', 'sh': 'श',              'Sh': 'ष', 's': 'स', 'h': 'ह', 'ksh': 'क्ष', 'gy': 'ज्ञ'}SIGNS = {'M': 'ं', '~': 'ँ'}LONGEST = 3 def in_table(piece):    return piece in VOWELS or piece in CONSONANTS or piece in SIGNS def pieces(text):    found = []    i = 0    while i < len(text):        for size in range(LONGEST, 0, -1):            piece = text[i:i + size]            if in_table(piece):                break        found.append(piece)        i = i + len(piece)    return found def join(parts):    out = ''    after_consonant = False    for piece in parts:        if piece in CONSONANTS:            if after_consonant:                out = out + '्'            out = out + CONSONANTS[piece]            after_consonant = True        elif piece in VOWELS:            letter, sign = VOWELS[piece]            if after_consonant:                out = out + sign            else:                out = out + letter            after_consonant = False        else:            out = out + SIGNS.get(piece, piece)            after_consonant = False    return out def transliterate(text):    return join(pieces(text)) TESTS = {'nepal': 'नेपाल', 'pani': 'पानी', 'bidyalaya': 'विद्यालय',         'dhanyabad': 'धन्यवाद', 'shanti': 'शान्ति', 'bhaktapur': 'भक्तपुर',         'kathmandu': 'काठमाडौँ', 'himal': 'हिमाल', 'kitab': 'किताब',         'bato': 'बाटो', 'ramro': 'राम्रो', 'ghar': 'घर', 'timi': 'तिमी',         'chiya': 'चिया', 'shiksha': 'शिक्षा', 'aama': 'आमा', 'buba': 'बुबा',         'sathi': 'साथी', 'khana': 'खाना', 'gaun': 'गाउँ'} right = 0for typed, wanted in TESTS.items():    got = transliterate(typed)    if got == wanted:        right = right + 1    else:        print(typed, '→', got, 'but people mean', wanted)print('Right:', right, 'of', len(TESTS))

Output

nepal → नेपल but people mean नेपालpani → पनि but people mean पानीbidyalaya → बिद्यलय but people mean विद्यालयdhanyabad → धन्यबद but people mean धन्यवादshanti → शन्ति but people mean शान्तिkathmandu → कथ्मन्दु but people mean काठमाडौँhimal → हिमल but people mean हिमालkitab → कितब but people mean किताबbato → बतो but people mean बाटोramro → रम्रो but people mean राम्रोtimi → तिमि but people mean तिमीchiya → चिय but people mean चियाshiksha → शिक्ष but people mean शिक्षाaama → आम but people mean आमाbuba → बुब but people mean बुबाsathi → सथि but people mean साथीkhana → खन but people mean खानाgaun → गौन but people mean गाउँRight: 2 of 20

The rules alone get only 2 of 20 right: bhaktapur and ghar. Everything else needs a vowel the typist didn’t write as our table wants, or a letter the typist couldn’t mark at all.

Step 4: let the words decide

The letters nepal could mean several spellings: न or ण, then े, then प, then nothing or ा, then ल. A keyboard can make all of them and keep only the ones that are real words. That needs a word list. words.py holds the 1,000 most common words in Nepal’s school textbooks (Grades 1 to 12, in Nepali), with how many times the books use each.

CHOICES lists the pieces people type for more than one thing: a for अ or आ, i for इ or ई, t for त or ट, n for न, ण or a nasal sign, s and sh for स, श or ष, b for ब or व, and th, d and dh for both kinds of each. Python’s itertools.product gives every way of choosing one option for each piece.

from itertools import productfrom words import WORDS VOWELS = {'a': ('अ', ''), 'aa': ('आ', 'ा'), 'i': ('इ', 'ि'), 'ee': ('ई', 'ी'),          'u': ('उ', 'ु'), 'oo': ('ऊ', 'ू'), 'e': ('ए', 'े'), 'ai': ('ऐ', 'ै'),          'o': ('ओ', 'ो'), 'au': ('औ', 'ौ')}CONSONANTS = {'k': 'क', 'kh': 'ख', 'g': 'ग', 'gh': 'घ', 'ch': 'च', 'chh': 'छ',              'j': 'ज', 'jh': 'झ', 'T': 'ट', 'Th': 'ठ', 'D': 'ड', 'Dh': 'ढ',              'N': 'ण', 't': 'त', 'th': 'थ', 'd': 'द', 'dh': 'ध', 'n': 'न',              'p': 'प', 'ph': 'फ', 'f': 'फ', 'b': 'ब', 'bh': 'भ', 'm': 'म',              'y': 'य', 'r': 'र', 'l': 'ल', 'w': 'व', 'v': 'व', 'sh': 'श',              'Sh': 'ष', 's': 'स', 'h': 'ह', 'ksh': 'क्ष', 'gy': 'ज्ञ'}SIGNS = {'M': 'ं', '~': 'ँ'}LONGEST = 3 def in_table(piece):    return piece in VOWELS or piece in CONSONANTS or piece in SIGNS def pieces(text):    found = []    i = 0    while i < len(text):        for size in range(LONGEST, 0, -1):            piece = text[i:i + size]            if in_table(piece):                break        found.append(piece)        i = i + len(piece)    return found def join(parts):    out = ''    after_consonant = False    for piece in parts:        if piece in CONSONANTS:            if after_consonant:                out = out + '्'            out = out + CONSONANTS[piece]            after_consonant = True        elif piece in VOWELS:            letter, sign = VOWELS[piece]            if after_consonant:                out = out + sign            else:                out = out + letter            after_consonant = False        else:            out = out + SIGNS.get(piece, piece)            after_consonant = False    return out def transliterate(text):    return join(pieces(text)) CHOICES = {'a': ['a', 'aa'], 'i': ['i', 'ee'], 'u': ['u', 'oo'],           't': ['t', 'T'], 'th': ['th', 'Th'], 'd': ['d', 'D'], 'dh': ['dh', 'Dh'],           'n': ['n', 'N', 'M', '~'], 's': ['s', 'sh', 'Sh'], 'sh': ['sh', 'Sh', 's'],           'b': ['b', 'w'], 'w': ['w', 'b'], 'v': ['v', 'b']} def spellings(text):    options = [CHOICES.get(piece, [piece]) for piece in pieces(text)]    found = []    for parts in product(*options):        if parts[0] in SIGNS:          # a nasal sign can't start a word            continue        word = join(parts)        if word not in found:            found.append(word)    return found for typed in ['nepal', 'pani', 'dhanyabad']:    every = spellings(typed)    words = [w for w in every if w in WORDS]    print(typed, ':', len(every), 'spellings;', 'words:', words)

Output

nepal : 4 spellings; words: ['नेपाल']pani : 16 spellings; words: ['पनि', 'पानी']dhanyabad : 256 spellings; words: ['धन्यवाद']

Try it

Every spelling, and which are words

Type a word, or pick one, and see every spelling the letters could mean, with the ones that are words and how often the textbooks use each.

The rules alone give पनि
पनि 16,806
पानी 2,787
—
—
—

Not words: पनी · पणि · पणी · पंइ · पंई · पँइ …

Spellings the letters could mean
16
Of them, words in the list
2

2 of the 16 spellings are words. The most common, पनि, is shown first; the others are offered as choices.

None of the 50,000 spellings is in the list, so the keyboard can only give the rules’ own spelling, क्ष्क्ष्क्ष्क्ष्क्ष्क्ष्.

The list: the 1,000 most common words of Nepal’s school textbooks in Nepali, Grades 1–12, with how many times the books use each.

Four spellings for nepal, and only one is a word. dhanyabad has 256 spellings, and again only one is a word. pani has two real words, पनि and पानी, so the keyboard needs a way to choose. The simplest is to take the word the textbooks use most, the one with the biggest count:

from itertools import productfrom words import WORDS VOWELS = {'a': ('अ', ''), 'aa': ('आ', 'ा'), 'i': ('इ', 'ि'), 'ee': ('ई', 'ी'),          'u': ('उ', 'ु'), 'oo': ('ऊ', 'ू'), 'e': ('ए', 'े'), 'ai': ('ऐ', 'ै'),          'o': ('ओ', 'ो'), 'au': ('औ', 'ौ')}CONSONANTS = {'k': 'क', 'kh': 'ख', 'g': 'ग', 'gh': 'घ', 'ch': 'च', 'chh': 'छ',              'j': 'ज', 'jh': 'झ', 'T': 'ट', 'Th': 'ठ', 'D': 'ड', 'Dh': 'ढ',              'N': 'ण', 't': 'त', 'th': 'थ', 'd': 'द', 'dh': 'ध', 'n': 'न',              'p': 'प', 'ph': 'फ', 'f': 'फ', 'b': 'ब', 'bh': 'भ', 'm': 'म',              'y': 'य', 'r': 'र', 'l': 'ल', 'w': 'व', 'v': 'व', 'sh': 'श',              'Sh': 'ष', 's': 'स', 'h': 'ह', 'ksh': 'क्ष', 'gy': 'ज्ञ'}SIGNS = {'M': 'ं', '~': 'ँ'}LONGEST = 3 def in_table(piece):    return piece in VOWELS or piece in CONSONANTS or piece in SIGNS def pieces(text):    found = []    i = 0    while i < len(text):        for size in range(LONGEST, 0, -1):            piece = text[i:i + size]            if in_table(piece):                break        found.append(piece)        i = i + len(piece)    return found def join(parts):    out = ''    after_consonant = False    for piece in parts:        if piece in CONSONANTS:            if after_consonant:                out = out + '्'            out = out + CONSONANTS[piece]            after_consonant = True        elif piece in VOWELS:            letter, sign = VOWELS[piece]            if after_consonant:                out = out + sign            else:                out = out + letter            after_consonant = False        else:            out = out + SIGNS.get(piece, piece)            after_consonant = False    return out def transliterate(text):    return join(pieces(text)) CHOICES = {'a': ['a', 'aa'], 'i': ['i', 'ee'], 'u': ['u', 'oo'],           't': ['t', 'T'], 'th': ['th', 'Th'], 'd': ['d', 'D'], 'dh': ['dh', 'Dh'],           'n': ['n', 'N', 'M', '~'], 's': ['s', 'sh', 'Sh'], 'sh': ['sh', 'Sh', 's'],           'b': ['b', 'w'], 'w': ['w', 'b'], 'v': ['v', 'b']} def spellings(text):    options = [CHOICES.get(piece, [piece]) for piece in pieces(text)]    found = []    for parts in product(*options):        if parts[0] in SIGNS:          # a nasal sign can't start a word            continue        word = join(parts)        if word not in found:            found.append(word)    return found def best(text):    words = [w for w in spellings(text) if w in WORDS]    if not words:        return transliterate(text)    return max(words, key=WORDS.get) TESTS = {'nepal': 'नेपाल', 'pani': 'पानी', 'bidyalaya': 'विद्यालय',         'dhanyabad': 'धन्यवाद', 'shanti': 'शान्ति', 'bhaktapur': 'भक्तपुर',         'kathmandu': 'काठमाडौँ', 'himal': 'हिमाल', 'kitab': 'किताब',         'bato': 'बाटो', 'ramro': 'राम्रो', 'ghar': 'घर', 'timi': 'तिमी',         'chiya': 'चिया', 'shiksha': 'शिक्षा', 'aama': 'आमा', 'buba': 'बुबा',         'sathi': 'साथी', 'khana': 'खाना', 'gaun': 'गाउँ'} print('nepal →', best('nepal')) right = 0for typed, wanted in TESTS.items():    got = best(typed)    if got == wanted:        right = right + 1    else:        print(typed, '→', got, 'but people mean', wanted)print('Right:', right, 'of', len(TESTS))

Output

nepal → नेपालpani → पनि but people mean पानीkathmandu → कथ्मन्दु but people mean काठमाडौँgaun → गौन but people mean गाउँRight: 17 of 20

What your program shows

Type nepal, and your program now gives नेपाल, as people want. With the same rules and a list of 1,000 words, it goes from 2 of 20 to 17 of 20. The rules do the cutting and joining; the word list does the choosing. We tried a list of 5,000 words too: it gets the same 17. A longer list lets it type rarer words, but the three it misses need something else.

The three it still gets wrong show the next ideas, and what real keyboards do:

  • pani: both पनि and पानी are words. पनि is six times as common in the textbooks, so the program always gives पनि, even in chiso pani (चिसो पानी, cold water). To choose well it must look at the words around it. That is Challenge ★★★.
  • gaun: the cut is wrong before the list is asked. Longest match takes au as one piece (औ), so no spelling it makes is गाउँ, which needs aa, u and the sign ँ. A list can only choose among the spellings it is given.
  • kathmandu: people type the city’s English name, not its Nepali spelling. The n stands for the sign ँ at the end, and the u for औ. No rule about single letters predicts that; it has to be learned from how people really type.

That is what the big keyboards do. A phone keyboard keeps a list of many thousands of words, counts of which words come together, and a model, learned from examples, of how people write each word in Roman letters. It weighs all of them at once to rank its guesses, shows the best, and offers the rest as choices above the keys, as your program’s list of words did.

Going further still

The letters our table can’t type

Our table has no ङ or ञ, so it can’t type सङ्ख्या or सञ्चार, and no ऋ or ृ, so कृषि is out of reach too. Adding ng for ङ looks easy, but think about ganga: longest match would take ng and give गङ, when the word is गङ्गा. Try adding the pieces and see which of the twenty words change.

Signs with no consonant

join puts ँ after anything, even at the start of a word. Our spellings skips a sign at the start, but not after a space. Change join so that a sign after a space, or at the very start, is left out.

An open question: there is a way to write Nepali in Roman letters with no doubt at all. Scholars mark long vowels and curled-tongue sounds with marks above and below the letter (ā, ṭ, ṣ), as the international standard ISO 15919 does: śikṣā for शिक्षा, with ś for श and ṣ for ष. A phone keyboard has no such keys, and nobody types them in a message. Could a keyboard be designed for Nepali that is as quick to learn as Roman letters, and never leaves the computer guessing?

Challenges

Three challenges, each checked on the page: write your function in the program above the checker (press Edit), then press Check my program. The examples show what your function should give; there are more tests you can’t see.

★ Cut a word into syllables

Write syllables(word): the word’s akshars (syllables as they are written), each a string of the code points it is stored as, as a list. A vowel sign, a nasal sign, a visarga or a halant joins the akshar before it, and so does a consonant that comes after a halant. So नमस्ते is ['न', 'म', 'स्ते'].

MARKS = 'ािीुूृेैोौंँः्'   # vowel signs, nasal signs, the visarga and the halant def syllables(word):    # Give back the word's akshars, as a list of strings.    return [] print(syllables('नमस्ते'))

Try it

★ Cut a word into syllables

  1. नमस्ते syllables('नमस्ते') Should give ['न', 'म', 'स्ते'] Yours —
  2. विद्यालय syllables('विद्यालय') Should give ['वि', 'द्या', 'ल', 'य'] Yours —
Tests passed
— / 8
Challenge
★ solved

Write your syllables in the program above (Edit), then check it.

A test your function gets wrong, anything it prints, and any error appear here.

Hint
Answer

★★ Back to Roman letters

Write romanize(word), the other direction: Nepali to Roman letters, in our table’s spelling, so that transliterate(romanize(word)) gives word back. Use BACK, the first key the table has for each consonant (so व is w and फ is ph). A consonant gets an a after it unless a halant or a vowel sign follows it, or it ends the word. So नमस्ते is 'namaste' and नेपाल is 'nepaal'.

VOWELS = {'a': ('अ', ''), 'aa': ('आ', 'ा'), 'i': ('इ', 'ि'), 'ee': ('ई', 'ी'),          'u': ('उ', 'ु'), 'oo': ('ऊ', 'ू'), 'e': ('ए', 'े'), 'ai': ('ऐ', 'ै'),          'o': ('ओ', 'ो'), 'au': ('औ', 'ौ')}CONSONANTS = {'k': 'क', 'kh': 'ख', 'g': 'ग', 'gh': 'घ', 'ch': 'च', 'chh': 'छ',              'j': 'ज', 'jh': 'झ', 'T': 'ट', 'Th': 'ठ', 'D': 'ड', 'Dh': 'ढ',              'N': 'ण', 't': 'त', 'th': 'थ', 'd': 'द', 'dh': 'ध', 'n': 'न',              'p': 'प', 'ph': 'फ', 'f': 'फ', 'b': 'ब', 'bh': 'भ', 'm': 'म',              'y': 'य', 'r': 'र', 'l': 'ल', 'w': 'व', 'v': 'व', 'sh': 'श',              'Sh': 'ष', 's': 'स', 'h': 'ह', 'ksh': 'क्ष', 'gy': 'ज्ञ'}SIGNS = {'M': 'ं', '~': 'ँ'}LONGEST = 3 def in_table(piece):    return piece in VOWELS or piece in CONSONANTS or piece in SIGNS def pieces(text):    found = []    i = 0    while i < len(text):        for size in range(LONGEST, 0, -1):            piece = text[i:i + size]            if in_table(piece):                break        found.append(piece)        i = i + len(piece)    return found def join(parts):    out = ''    after_consonant = False    for piece in parts:        if piece in CONSONANTS:            if after_consonant:                out = out + '्'            out = out + CONSONANTS[piece]            after_consonant = True        elif piece in VOWELS:            letter, sign = VOWELS[piece]            if after_consonant:                out = out + sign            else:                out = out + letter            after_consonant = False        else:            out = out + SIGNS.get(piece, piece)            after_consonant = False    return out def transliterate(text):    return join(pieces(text)) BACK = {}                      # each consonant → its first key in the tablefor roman, nepali in CONSONANTS.items():    if len(nepali) == 1 and nepali not in BACK:        BACK[nepali] = romanLETTERS = {letter: roman for roman, (letter, sign) in VOWELS.items()}SIGN_OF = {sign: roman for roman, (letter, sign) in VOWELS.items() if sign}NASAL = {'ं': 'M', 'ँ': '~'} def romanize(word):    # Give back the word in Roman letters, in the table's spelling.    return '' print(romanize('नमस्ते'), transliterate(romanize('नमस्ते')))

Try it

★★ Back to Roman letters

  1. नमस्ते romanize('नमस्ते') Should give 'namaste' Yours —
  2. नेपाल romanize('नेपाल') Should give 'nepaal' Yours —
Tests passed
— / 9
Challenge
★★ solved

Write your romanize in the program above (Edit), then check it.

A test your function gets wrong, anything it prints, and any error appear here.

Hint
Another hint
Answer

★★★ Choose by the word before

Write choose(previous, typed): like Step 4’s best, but for a word typed after another word, previous (in Nepali). Of the spellings that are words in COUNTS, take the one that comes after previous most often in the textbooks (PAIRS); if none of them ever does, the most common one. If no spelling is a word, the rules’ own spelling. So choose('चिसो', 'pani') is पानी, and choose('कुनै', 'pani') is पनि.

from itertools import product VOWELS = {'a': ('अ', ''), 'aa': ('आ', 'ा'), 'i': ('इ', 'ि'), 'ee': ('ई', 'ी'),          'u': ('उ', 'ु'), 'oo': ('ऊ', 'ू'), 'e': ('ए', 'े'), 'ai': ('ऐ', 'ै'),          'o': ('ओ', 'ो'), 'au': ('औ', 'ौ')}CONSONANTS = {'k': 'क', 'kh': 'ख', 'g': 'ग', 'gh': 'घ', 'ch': 'च', 'chh': 'छ',              'j': 'ज', 'jh': 'झ', 'T': 'ट', 'Th': 'ठ', 'D': 'ड', 'Dh': 'ढ',              'N': 'ण', 't': 'त', 'th': 'थ', 'd': 'द', 'dh': 'ध', 'n': 'न',              'p': 'प', 'ph': 'फ', 'f': 'फ', 'b': 'ब', 'bh': 'भ', 'm': 'म',              'y': 'य', 'r': 'र', 'l': 'ल', 'w': 'व', 'v': 'व', 'sh': 'श',              'Sh': 'ष', 's': 'स', 'h': 'ह', 'ksh': 'क्ष', 'gy': 'ज्ञ'}SIGNS = {'M': 'ं', '~': 'ँ'}LONGEST = 3 def in_table(piece):    return piece in VOWELS or piece in CONSONANTS or piece in SIGNS def pieces(text):    found = []    i = 0    while i < len(text):        for size in range(LONGEST, 0, -1):            piece = text[i:i + size]            if in_table(piece):                break        found.append(piece)        i = i + len(piece)    return found def join(parts):    out = ''    after_consonant = False    for piece in parts:        if piece in CONSONANTS:            if after_consonant:                out = out + '्'            out = out + CONSONANTS[piece]            after_consonant = True        elif piece in VOWELS:            letter, sign = VOWELS[piece]            if after_consonant:                out = out + sign            else:                out = out + letter            after_consonant = False        else:            out = out + SIGNS.get(piece, piece)            after_consonant = False    return out def transliterate(text):    return join(pieces(text)) CHOICES = {'a': ['a', 'aa'], 'i': ['i', 'ee'], 'u': ['u', 'oo'],           't': ['t', 'T'], 'th': ['th', 'Th'], 'd': ['d', 'D'], 'dh': ['dh', 'Dh'],           'n': ['n', 'N', 'M', '~'], 's': ['s', 'sh', 'Sh'], 'sh': ['sh', 'Sh', 's'],           'b': ['b', 'w'], 'w': ['w', 'b'], 'v': ['v', 'b']} def spellings(text):    options = [CHOICES.get(piece, [piece]) for piece in pieces(text)]    found = []    for parts in product(*options):        if parts[0] in SIGNS:          # a nasal sign can't start a word            continue        word = join(parts)        if word not in found:            found.append(word)    return found # How often the textbooks use each of these words, and how often one comes# straight after another (only the pairs this challenge needs)COUNTS = {'पानी': 2787, 'पनि': 16806, 'काम': 3423, 'कम': 2070}PAIRS = {('चिसो', 'पानी'): 25, ('चिसो', 'कम'): 1, ('तातो', 'पानी'): 18, ('कुनै', 'पनि'): 1466, ('कुनै', 'काम'): 80, ('म', 'पानी'): 8, ('म', 'पनि'): 121, ('म', 'काम'): 3, ('राम्रो', 'पनि'): 1, ('राम्रो', 'काम'): 31, ('भन्दा', 'पनि'): 17, ('भन्दा', 'काम'): 2, ('भन्दा', 'कम'): 190, ('धेरै', 'पानी'): 20, ('धेरै', 'पनि'): 1, ('धेरै', 'काम'): 18, ('धेरै', 'कम'): 13} def choose(previous, typed):    # Give back the spelling of typed that fits best after previous.    return '' print(choose('चिसो', 'pani'), choose('कुनै', 'pani'))

Try it

★★★ Choose by the word before

  1. Cold water choose('चिसो', 'pani') Should give 'पानी' Yours —
  2. Any … at all choose('कुनै', 'pani') Should give 'पनि' Yours —
Tests passed
— / 9
Challenge
★★★ solved

Write your choose in the program above (Edit), then check it.

A test your function gets wrong, anything it prints, and any error appear here.

Hint
Answer

Questions

Why does a transliterator try the longest piece first?

Pick an answer to see why.

How many code points is क्षि stored as?

Pick an answer to see why.

a.

Your Step 4 program types khana as खाना (food), but someone meant खान (to eat, as in खान जानु). Both are words in the list. What could a keyboard do about it?

Answer
b.

Ram types Nepali with the Romanized keyboard layout (Grade 6), and Sita with a transliterating keyboard on her phone. Both type nepal. What does each get, and why?

Answer
c.

A shop’s old price list was typed in the Preeti font. Explain why a search for चिया on the computer finds nothing, and why turning it into Unicode is harder than looking up each key.

Answer
d.

spellings('dhanyabad') makes 256 spellings. Explain where 256 comes from, and why a much longer word could make a keyboard slow.

Answer

Where to go next