Command Palette

Search for a command to run...

Hectal
PHASE 10Intermediate ~33 min· topic 7 of 8

Topic 10.7

Mobile, RTL and Languages

In one line

Most people meet your assistant on a phone, in many languages. This topic covers long words that break narrow layouts, text that runs right to left, an on-screen keyboard that hides the input box, and system messages that need translation and correct plural forms.

Think of it like this

A road sign. If it only works for people who read English, drive on the left and have a tall windscreen, some drivers will miss it. A chat screen built only for a wide English desktop misses a large share of its users in the same way.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

RTL / LTR
Right-to-left and left-to-right. The direction a script is read.
dir="auto"
An HTML attribute that lets the browser pick the direction from the first strong letter in the text.
Logical property
A CSS property named by start and end (margin-inline-start) instead of left and right, so it flips with the direction.
bdi
An HTML element that isolates a piece of text so its direction does not disturb the text around it.
overflow-wrap
A CSS property that decides whether an unbreakable string may be broken to fit. Values: normal, break-word, anywhere.
min-content
The smallest width a box can take without overflowing, the width of its longest unbreakable piece.
Visual viewport
The part of the page currently visible on the screen. It shrinks when the on-screen keyboard opens.
Locale
A language plus region, such as en-IN or ar. It decides number, date and plural formatting.
Intl.PluralRules
A browser API that returns the plural category (one, few, other…) for a number in a language.
Message catalog
An object that holds every user-facing string for a language, looked up by key.

Step by step

01Test with the ugliest string you can think of

Before reading about fixes, put an unbroken 86-character URL in a 120-pixel box. The first example does that four ways. Plain overflow-wrap: normal overflows. anywhere and break-word both fit in a plain box.

Then the same text in a flex row, which is how most chat rows are built: break-word overflows again. The last line shows that break-word plus min-width: 0 on the flex item fixes it. The log answers a yes-or-no question (does the content stick out of its box?), so it is the same on any screen.

02Let the browser guess direction, once, in the right place

The second example lists seven strings and prints the computed direction. Arabic and Hebrew are rtl. English and Hindi are ltr. "English first, then Arabic" is ltr and "Arabic first, then English" is rtl: the first strong letter decides. Digits and symbols alone default to ltr.

Then it measures where the user's bubble sits in an LTR page and an RTL page. With the side set on the bubble that also has dir="auto" it is on the right in both. With the side on a flex row it flips: right in LTR, left in RTL.

Let the browser guess direction, once, in the right placediagram
Rendering diagram…

03Plan for the keyboard

The third example decides from four numbers: the layout height, the visual height, how far the visual viewport is scrolled down, and the zoom scale. Under 15 percent covered it says closed (a browser toolbar is not a keyboard). Otherwise it says open and how many pixels to lift the composer. When pinch-zoomed it gives up and leaves the layout alone, because the numbers no longer compare.

In the browser you would call it from a resize listener on window.visualViewport. The browser also needs a fallback for when the API is missing, and the example shows that plan.

wiring (not run here)whole filejs
const vv = window.visualViewport;
function update() {
  const s = keyboardState({ layoutHeight: window.innerHeight, visualHeight: vv.height, visualTop: vv.offsetTop, scale: vv.scale });
  composer.style.transform = s.keyboard === "open" ? "translateY(-" + s.inset + "px)" : "";
}
vv?.addEventListener("resize", update);
vv?.addEventListener("scroll", update);

04Translate what is yours, with the right plural

The fourth example's t(lang, key, n) looks up a message, falls back to English if the language does not have it (the Hindi "offline" line is not translated yet and shows in English instead of blank), picks a plural form with Intl.PluralRules, and formats the number with Intl.NumberFormat. The log shows all six Arabic plural categories and the Arabic sentence chosen for each of 0, 1, 2, 5 and 11.

05Tell the model which language to answer in

The page language and the answer language are separate. A person using an English interface may write to Mitra in Hindi, and the reply should come in Hindi. That is a server instruction, not a CSS matter:

app/api/chat/route.ts (shape of the code)whole filets
const result = streamText({
  model, // your provider's model
  instructions: "You are Mitra for Chai & Co. Reply in the language the customer writes in.",
  messages: await convertToModelMessages(messages),
});

06Check it on a real phone

Browser developer tools simulate a phone, but the on-screen keyboard and the way a particular mobile browser handles the viewport are best seen on a device. Try one iPhone and one Android phone if you can, in portrait and landscape, with the keyboard open.

Try it yourself

  1. 1

    Add a Hebrew message with an order id

    In the second example add ["Hebrew with an id", "ההזמנה ORD-1042 נשלחה."] to SAMPLES and run.

    The sentence starts with Hebrew, so it is rtl. The id inside is left-to-right text, and the browser orders it by its own rules. To keep an id from disturbing its neighbours, wrap it in <bdi>, which gives it its own direction.

  2. 2

    Open the keyboard in a test

    In the third example add a case ["landscape, keyboard open", { layoutHeight: 360, visualHeight: 160 }] and read the lift amount.

    In landscape on a phone the keyboard can cover most of the screen, so the transcript may have almost no room. Decide what your product does then (for example hide the header while the keyboard is open).

  3. 3

    Translate one more message

    In the fourth example add fr: { stop: "Arrêter" } to the catalog, then log t("fr", "stop") and t("fr", "offline").

    The first line is French; the second falls back to English because offline has no French entry yet. A partial translation is shippable only because of the fallback.

Code & diagrams

A very long word: which wrapping rules break the layout?

Each line is a yes-or-no answer to "does the text stick out of its box?", which does not depend on the screen. The third line is the trap: break-word is fine in a plain box and still overflows inside a flex row. The fourth line fixes it with min-width: 0.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

normal       | plain box overflows: true  | inside a flex row overflows: true
anywhere     | plain box overflows: false | inside a flex row overflows: false
break        | plain box overflows: false | inside a flex row overflows: true
break-fixed  | plain box overflows: false | inside a flex row overflows: false
Right-to-left and mixed-direction messages

The first seven lines are the browser's own resolved direction for each sentence. The last two lines compare two ways to place the user's bubble: the wrong one leaves it on the right in both page directions, the right one mirrors. The result is a yes-or-no measure of position, not pixel numbers.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

English                        -> ltr
Arabic                         -> rtl
Hebrew                         -> rtl
Hindi (left to right script)   -> ltr
English first, then Arabic     -> ltr
Arabic first, then English     -> rtl
only digits and symbols        -> ltr
auto on the bubble:  LTR page -> right | RTL page -> right
row follows the page: LTR page -> right | RTL page -> left
Is the on-screen keyboard open? A decision, not pixels

The numbers are made up (an 800 px page with a 300 px keyboard), so you get the same lines on every machine. The 15 percent threshold is a choice of this example: browser toolbars and address bars also change the visual height a little, and they are not keyboards.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

phone, keyboard closed           | closed  | composer at the bottom of the page
phone, keyboard open             | open    | lift the composer by 300 px, scroll the transcript to the last message
phone, page scrolled while open  | open    | lift the composer by 260 px, scroll the transcript to the last message
small gap, a browser toolbar     | closed  | composer at the bottom of the page
pinch-zoomed                     | unknown | leave the layout alone
a window with visualViewport: true | a window without it: false
without it, the plan is: leave the layout alone
Translated messages, six Arabic plurals and a missing translation

The plural categories and the number formats come from the browser's own Intl data, not from the example. The Hindi and Arabic sentences are examples written for this course: have a native speaker review any translation before you ship it.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

en plural categories: one, other
hi plural categories: one, other
ar plural categories: zero, one, two, few, many, other
ar  0 -> zero  لا توجد أسئلة متبقية اليوم.
ar  1 -> one   لديك سؤال واحد متبقٍ اليوم.
ar  2 -> two   لديك سؤالان متبقيان اليوم.
ar  5 -> few   لديك 5 أسئلة متبقية اليوم.
ar 11 -> many  لديك 11 سؤالًا متبقيًا اليوم.
en  1 -> You have 1 question left today.
en  3 -> You have 3 questions left today.
hi  3 -> आज आपके पास 3 सवाल बचे हैं।
hi offline (not translated yet) -> You seem to be offline.
₹1,23,456.00
$123,456.00

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Use English plural rules for every language

In the fourth example change the line const category = new Intl.PluralRules(lang).select(n); to const category = n === 1 ? "one" : "other";.

terminal
$ The console, the Arabic lines for 2, 5 and 11
── what you'll see ──
ar 2 -> two لديك 2 سؤال متبقٍ اليوم.
ar 5 -> few لديك 5 سؤال متبقٍ اليوم.
ar 11 -> many لديك 11 سؤال متبقٍ اليوم.

Break #2

Put the side of the bubble on a dir="auto" element

In the second example, in Message, change <div style={{ display: "flex", justifyContent: "flex-end" }}> to a plain <div> and add marginInlineStart: "auto" to the <p>'s style.

terminal
$ The console, last line
── what you'll see ──
row follows the page: LTR page -> right | RTL page -> right

Myth vs fact

Myth

overflow-wrap: break-word and anywhere are the same.

Fact

They break words alike but differ in sizing: only anywhere lets a box shrink to the smallest break unit, so in a flex row break-word can still overflow.

Myth

dir="auto" detects the language.

Fact

It looks at the first strong letter. A message that starts with an English name and continues in Arabic is treated as left to right.

Myth

The keyboard resizes the page.

Fact

By default it resizes only the visual viewport. The layout stays the same size, so fixed bars can be hidden behind the keyboard.

Myth

Translating means replacing strings.

Fact

Plurals, number and money formats, text direction and text length all change. A German label can be much longer than the English one.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Keep a test string list with an 86-character URL, a long German-style compound word, an emoji family, Arabic, Hebrew and a Japanese sentence, and run every new message component against it. Add the list to your visual or snapshot tests.

  • ▸

    Do not translate the model's output on the client. Let the model answer in the user's language and keep your own strings in the catalog. Translating model text after the fact adds delay, cost and mistakes.

  • ▸

    Store the interface language as a user setting separate from the browser language, and put lang on the page (and on any piece in a different language) so screen readers choose the right voice.

  • ▸

    Fonts matter: pick a font stack that covers the scripts you expect. A missing glyph shows as an empty box in the middle of an answer.

Remember this

  1. 1

    The problem this solves: Mitra's answers are written by a model, in any language, with URLs, code and names of any length. The screen is a phone with a keyboard covering half of it. The surrounding messages ("Stop", "You have 2 questions left") are yours to translate. Four things break first: very long words, text direction, the keyboard, and plurals. Each one has a small, testable fix.

  2. 2

    Long words and wrapping. A URL or an order id has no spaces, so the browser has nowhere to break the line and the text pushes out of its bubble. The CSS property overflow-wrap controls this. The first example puts an 86-character URL into a 120-pixel box: with the default (normal) it sticks out; with break-word or anywhere it breaks. Then it shows the trap: in a flex row (the usual layout for a chat row) break-word still overflows, because MDN says soft wrap opportunities from break-word are not counted when the browser works out a box's minimum content size, so the flex item refuses to shrink. anywhere counts them and works. Setting min-width: 0 on the flex item also works. Use overflow-wrap: anywhere on message text and test with an unbroken string.

  3. 3

    Right-to-left text. Arabic, Hebrew, Persian and Urdu read from right to left. A model reply can be in any of them, and you do not know in advance. The HTML attribute dir="auto" lets the browser guess the direction from the first strong letter. MDN is clear that it is a heuristic, not language detection: a comment that starts with an English name and continues in Arabic is treated as left to right. The second example prints the computed direction for seven sentences. Put dir="auto" on the element that holds the text.

  4. 4

    Layout direction versus text direction. Which side a bubble sits on belongs to the page, not to the language of one message. The second example shows the trap. When dir="auto" and margin-inline-start: auto are on the same bubble, an English message in an Arabic (right-to-left) page stays on the right, because "start" now follows the message. Put the side on a row element that follows the page (display: flex; justify-content: flex-end puts the user's bubble at the end side in both directions) and dir="auto" only on the text.

  5. 5

    Write direction-neutral CSS. Use logical properties: margin-inline-start, padding-inline-end, text-align: start, inset-inline-end, instead of left and right. Set dir="rtl" and lang on the <html> element (or on a wrapper) for a right-to-left interface, and the layout flips by itself. Wrap a piece of text whose direction is unknown, such as an order id or a user name inside a sentence of another direction, in <bdi>, which isolates it as if it had dir="auto".

  6. 6

    The on-screen keyboard. On a phone the keyboard covers the lower part of the page. By default the keyboard resizes only the visual viewport (what you can see); the layout viewport keeps its size, so a fixed input bar may end up behind the keyboard. MDN documents a viewport meta option, interactive-widget, with resizes-visual (the default), resizes-content (the layout adapts) and overlays-content (nothing resizes). Browser support varies, so check MDN's compatibility table before relying on it. The reliable approach is to compare window.innerHeight with window.visualViewport.height and lift the composer by the difference. The third example turns that into a pure function with five cases, including pinch-zoom (where the sizes cannot be compared) and a small gap from a browser toolbar.

  7. 7

    Viewport values cannot be printed, so print decisions. The example does not log pixel sizes of your device. It passes numbers in and prints the plan ("lift the composer by 300 px"). Do the same in tests: the function that decides is easy to check, and the code that reads visualViewport is a thin wrapper.

  8. 8

    Reflow and touch targets. WCAG 1.4.10 Reflow (Level AA) asks for content to work at 320 CSS pixels wide without scrolling in two directions, which is what a phone or a zoomed desktop page is. Test the chat at that width and at 400% zoom. Make buttons easy to tap, with space between Send and Stop so they are not hit by mistake.

  9. 9

    Your messages versus the model's. The words the model writes follow the user's language if you tell it to. On the server that is one line in the system prompt (the option is named instructions in AI SDK 7). The words your code writes ("Stop", "Could not save your feedback", "You have 2 questions left today") are yours and must be translated. Keep them in a catalog, one object per language, and fall back to English when a key is missing, so the page never shows a blank.

  10. 10

    Plurals are not "add an s". English has two forms (one, other). Hindi has the same two categories in Intl.PluralRules. Arabic has six: zero, one, two, few, many, other. The fourth example prints them, picks the form with Intl.PluralRules, and formats the number with Intl.NumberFormat. Money is formatted for the reader too: 123456 rupees is ₹1,23,456.00 in en-IN and $123,456.00 in en-US with US dollars. Never build these strings with n === 1 ? ... : ... for languages you do not read.

Explain it without notes

01

Why does a long URL break a chat bubble, and what is the difference between break-word and anywhere?

02

How should the side of the user's bubble be decided so the chat works in a right-to-left page?

03

How do you handle a fixed composer on a phone with the on-screen keyboard open?

04

Why do you need Intl.PluralRules for translated messages?

Practice

01

Add a copied message to the catalog for en and hi (no plural), and make t return English for a language that is not in the catalog at all (such as "xx"). Log both cases.

02

Write a function bubbleSide(pageDir) that returns where the user's bubble should be ("left" or "right") and log it for "ltr" and "rtl".

Trade-offs

  • ↔

    Logical CSS properties make a right-to-left version almost free, but older code that uses left and right has to be converted, which takes care and visual testing.

  • ↔

    Supporting many languages costs translation and review time for every new message. A catalog with an English fallback lets you ship early and translate in stages.

Done when you can

  • I can test a layout with an unbroken 86-character string and fix it with overflow-wrap: anywhere or min-width: 0.

  • I can use dir="auto" on text and keep the bubble side on a parent that follows the page direction.

  • I can use logical CSS properties instead of left and right.

  • I can decide from visualViewport whether the keyboard covers the composer.

  • I can translate my own messages with the right plural form and a fallback.