Skip to content

Arabic Quotation Marks and Why AI Gets the Direction Wrong

9 min read · updated August 11, 2026

A model produced Arabic prose with a quoted phrase in it, and on screen the closing quotation mark is at the start of the quote and the opening one at the end. The bidi algorithm ran correctly. The problem is that the characters chosen do not carry the property that would make them flip.

The symptom, precisely

In a right-to-left line, a quotation opens at the right and closes at the left. Whether that happens automatically depends on which characters are in the text:

  • With guillemets, U+00AB and U+00BB, you store the opener first and the renderer draws each one with the mirrored glyph in a right-to-left run. It looks correct with no intervention.
  • With curly quotation marks, U+201C and U+201D, you store the opener first, the renderer draws exactly the glyph you stored, and the mark whose shape says “open” ends up at the left-hand end of the quoted phrase — where an Arabic reader expects the close.
  • With straight ASCII quotes, U+0022, the two ends are the same glyph, so nothing looks wrong and nothing looks typographically right either.

Note what is not happening: the characters are not being reordered. In all three cases the storage is identical and the algorithm resolves them the same way. Only the drawn glyph differs.

What Arabic typography uses

The dominant convention in Arabic — and in Persian — for the outer level of quotation is the guillemet pair, U+00AB LEFT-POINTING DOUBLE ANGLE QUOTATION MARK and U+00BB RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK. The Unicode names describe the glyphs as drawn in a left-to-right context and are actively misleading here: in Arabic the character named “left-pointing” is the one you use to open, and it is drawn pointing the other way.

Nesting is where conventions diverge and where a house style earns its keep. Common practice is guillemets outside and either the curly pair or single guillemets U+2039 and U+203A inside. There is no single answer across Arabic-speaking countries or across publishers, so pick one, write it into the style guide, and enforce it in post-processing rather than hoping a model infers it.

Do not confuse any of these with U+0022 or with the apostrophe. And do not confuse the guillemets with the mathematical angle brackets U+27E8 and U+27E9 or with the ASCII less-than and greater-than signs, all of which look similar at small sizes and none of which is a quotation mark.

Bidi_Mirrored, and why it decides this

Unicode gives characters a boolean property, Bidi_Mirrored. When a mirrored character is laid out at an odd — right-to-left — embedding level, the renderer substitutes the glyph of its mirror partner. The partners are listed in BidiMirroring.txt in the Unicode Character Database, and the rule itself is L4 in Unicode Standard Annex #9.

Parentheses, square brackets, braces, angle brackets and the guillemets have this property. The curly quotation marks do not. That single difference is the whole of this page: brackets in Arabic text just work and quotation marks do not, and the reason is a per-character property rather than anything about the algorithm.

The reason for the asymmetry is that mirroring is defined for characters whose glyphs are geometric mirror images with a paired semantic role. The curly quotes are not a mirror pair — U+201D is not a reflection of U+201C, it is a different shape — and their use varies between languages, with German and Polish conventionally putting the “closing” shape at the start. Mirroring them would break those languages. So Unicode leaves them alone and the burden falls on whoever chooses the characters.

Mirroring is a rendering behaviour. Never pre-swap characters in your stored data to make a display look right: the renderer will mirror the swapped characters again in any correctly-directed context, and you will have data that is correct only in the one place you checked.

Why models emit the English marks

Three pressures, all pointing the same way. Instruction-tuned models are heavily trained on English text, where the curly pair is the typographically correct choice, and quotation marks are exactly the sort of low-information token that a model will produce from the dominant pattern rather than from the target language. Much Arabic web text itself uses ASCII or curly quotes, because most keyboard layouts and input methods make guillemets awkward to type — so the training data for Arabic also contains the wrong marks in quantity. And a system prompt or a schema written in English pulls the punctuation of the response toward English conventions even when the prose is Arabic.

Asking for the correct characters by code point works, and asking for them by name does not, because “Arabic quotation marks” is ambiguous even to a human. The same is true of the other Arabic-script punctuation, covered in Persian punctuation placement.

Fixing it at the right layer

Do it in post-processing rather than in the prompt if the output matters, because a prompt instruction is a probability and a substitution is a guarantee. The substitution is safe as long as you only touch balanced pairs, and only when the target language uses guillemets.

import re

# Only for languages whose house style is guillemets.
# Pairs only: an unbalanced mark is left alone rather than guessed.
CURLY = re.compile(r"\u201C([^\u201C\u201D]*)\u201D")
STRAIGHT = re.compile(r"\"([^\"]*)\"")

def guillemets(text: str) -> str:
    text = CURLY.sub("\u00AB\\1\u00BB", text)
    text = STRAIGHT.sub("\u00AB\\1\u00BB", text)
    return text

# Do not run this over code spans, URLs, or JSON embedded in the
# response - a quotation mark there is syntax, not typography.

The comment at the end is the part that bites. A response containing a JSON example, a shell command or a code block has quotation marks that are syntax, and rewriting them produces code that will not parse. Split the response into prose and non-prose regions before substituting, or run the substitution only on fields you know are prose. If you are normalising quotation marks generally rather than for Arabic specifically, the surrounding considerations are in normalising curly and straight quotes.

One last check: whatever you settle on, confirm the font you render in actually has the guillemet glyphs. Some Arabic webfonts ship a restricted Latin range, and a missing glyph falls back to a different font at a different weight, which looks like a bug in the text rather than in the font stack.