logo
Published on

Where Does a Vietnamese Word End? Building a Reading App Around That Question

Authors
  • avatar
    Name
    Alberto Montalesi
    Twitter
This article was written with the assistance of AI.

Most language apps make you drill sentences someone else chose. Langi takes the other route: you read stories at roughly your level, tap anything you don't know, and the words you tap come back later as flashcards. Reading is the lesson; the vocabulary system is downstream of it.

That design is not novel — it is the comprehensible input idea that LingQ and Migaku also build on. What turned out to be genuinely hard was something I had not budgeted for at all: deciding what counts as one word.

 

The problem: Vietnamese does not put spaces between words

English readers get word boundaries for free. Spaces separate words, so "tap a word to see its meaning" is an unambiguous instruction.

Vietnamese puts spaces between syllables, not words. A great many Vietnamese words are compounds of two or more syllables, and the individual syllables frequently mean something on their own too. So a string of syllables separated by spaces is not a sequence of words — it is a sequence of pieces that may or may not group into larger words, and the grouping depends on context.

For a reading app this is not an academic concern. It decides:

  • What is tappable. If the reader taps one syllable of a two-syllable word, showing them that syllable's standalone meaning is actively misleading.
  • What goes into the vocabulary system. A compound and its parts are not three things to learn. Treating them as three inflates the deck with entries the learner does not actually need.
  • What "words in this lesson" means. Every progress number downstream depends on the count being right.

 

What I built instead of a parser

The obvious engineering answer is automatic word segmentation, and there are real NLP tools for Vietnamese that do it. I went a different way, for a reason that is more editorial than technical: the lessons are written and reviewed by people, and a human who knows the sentence resolves ambiguity better than a model guessing at it — especially for proper nouns, loanwords, and fixed phrases that segmenters routinely get wrong.

So the transcript format carries a small markup language that lets whoever prepares a lesson say what the segmentation actually is:

MarkerMeaning
_join these syllables into a single word
~stop here — do not try to extend this into a compound
>leave this alone, do not translate it
*bold this in the rendered lesson

They combine, so a single token can be both joined and emphasised. The markers are stripped at render time; the reader never sees them. What they see is a sentence where the right spans are tappable and each one resolves to the meaning that is correct in that sentence.

The interesting property is that this is a human-in-the-loop system by design rather than by compromise. Ambiguity gets resolved once, at authoring time, by someone who knows the answer — rather than at read time, by a heuristic, over and over, for every learner.

 

Compounds still leak, so there is a second pass

Markup fixes the display. It does not by itself fix the vocabulary counts, because the dictionary is built up across the whole library: a syllable that is only ever part of a compound in one lesson may legitimately be a standalone word in another.

So there is a batch job that walks the lesson library and marks the cases where a syllable appears only as part of a compound, flagging it so it drops out of the lesson's word count and out of the review queue. It is unglamorous bookkeeping, and it is the difference between a vocabulary count the learner trusts and one they quietly stop believing.

If I were starting again I would still do the markup first and the batch pass second. Getting the display right is what learners notice; getting the counts right is what keeps them.

 

The rest of it

Once segmentation is settled, the remainder is more conventional:

  • Reading with audio. Every story has audio, so the same lesson works for reading and listening.
  • Tap-to-save vocabulary, feeding an SM-2-style scheduler — each card carries an ease factor and an interval that move based on how you rate your recall, so words you find hard come back sooner.
  • Three live languages — Vietnamese, Italian and German — off one content pipeline.
  • Progress tracking: streaks, saved lessons, and known-word counts.

The stack is a Next.js and React front end on Vercel with PostgreSQL behind it, TanStack Query and Zustand for state, NextAuth for accounts, Paddle for payments, and Sentry for error tracking. It is a monorepo: the learner-facing client, an admin app for preparing and reviewing lessons, and shared packages between them.

Worth being clear about that admin app. In a content-driven product, tooling for whoever prepares the content is not a side project — the markup language above is only usable because there is a decent interface for applying it. I underestimated that at the start and paid for it later.

 

What I would tell someone building something similar

The lesson I keep coming back to is that the hard problem was not where I expected it. I assumed the reading interface would be the interesting part. The reading interface was a week. Deciding what a word is, and building the editorial tooling to record that decision, shaped the data model, the admin app and every progress number in the product.

If you are building anything that segments text a learner will interact with — in any language where boundaries are not free — settle that question before you design the schema. It is very expensive to change afterwards.

You can try Langi at langi.app.

Did you find this useful?

0 readers found this helpful