[{"data":1,"prerenderedAt":631},["ShallowReactive",2],{"guide-unicode-bidi-text-reversal":3},{"id":4,"title":5,"body":6,"date":623,"description":624,"extension":625,"meta":626,"navigation":324,"path":627,"readingTime":343,"seo":628,"stem":629,"__hash__":630},"guides\u002Fguides\u002Funicode-bidi-text-reversal.md","Text Reversal and Bidirectional Unicode",{"type":7,"value":8,"toc":612},"minimark",[9,13,18,26,104,117,121,124,140,148,158,161,166,169,308,401,412,416,419,430,436,439,443,446,510,513,517,567,570,574,595,599,608],[10,11,12],"p",{},"Reversing a string sounds trivial — swap characters from both ends until you reach the middle. But the moment your text contains emojis, combining marks, or right-to-left scripts, the simple approach breaks. What you see as a single character on screen might be multiple code points under the hood, and naïve reversal scrambles them into gibberish.",[14,15,17],"h2",{"id":16},"the-problem-with-naïve-reversal","The Problem with Naïve Reversal",[10,19,20,21,25],{},"Consider this string: ",[22,23,24],"code",{},"\"noe\\u0308l\""," — the word \"noël\" written with a combining diaeresis on the e. It contains five code points:",[27,28,29,45],"table",{},[30,31,32],"thead",{},[33,34,35,39,42],"tr",{},[36,37,38],"th",{},"Index",[36,40,41],{},"Code Point",[36,43,44],{},"Character",[46,47,48,60,71,82,93],"tbody",{},[33,49,50,54,57],{},[51,52,53],"td",{},"0",[51,55,56],{},"U+006E",[51,58,59],{},"n",[33,61,62,65,68],{},[51,63,64],{},"1",[51,66,67],{},"U+006F",[51,69,70],{},"o",[33,72,73,76,79],{},[51,74,75],{},"2",[51,77,78],{},"U+0065",[51,80,81],{},"e",[33,83,84,87,90],{},[51,85,86],{},"3",[51,88,89],{},"U+0308",[51,91,92],{},"◌̈ (combining diaeresis)",[33,94,95,98,101],{},[51,96,97],{},"4",[51,99,100],{},"U+006C",[51,102,103],{},"l",[10,105,106,107,110,111,113,114,116],{},"A naïve reversal that operates on code points produces ",[22,108,109],{},"\"l\\u0308eon\""," — the combining mark now attaches to the ",[22,112,103],{}," instead of the ",[22,115,81],{},", rendering as \"l̈eon\". The diaeresis hopped to the wrong letter because the algorithm did not respect grapheme boundaries.",[14,118,120],{"id":119},"grapheme-clusters","Grapheme Clusters",[10,122,123],{},"A grapheme cluster is the smallest unit of text that a user perceives as a single character. It may consist of:",[125,126,127,134,137],"ul",{},[128,129,130,131,133],"li",{},"A base character (e.g., ",[22,132,81],{},")",[128,135,136],{},"Zero or more combining marks (e.g., diaeresis, accent, cedilla)",[128,138,139],{},"Zero or more variation selectors or joiners",[10,141,142,143,147],{},"The family emoji 👨‍👩‍👧‍👦 illustrates this dramatically. It is a single grapheme cluster composed of ",[144,145,146],"strong",{},"seven"," code points:",[149,150,155],"pre",{"className":151,"code":153,"language":154},[152],"language-text","U+1F468  👨  Man\nU+200D        Zero-Width Joiner\nU+1F469  👩  Woman\nU+200D        Zero-Width Joiner\nU+1F467  👧  Girl\nU+200D        Zero-Width Joiner\nU+1F466  👦  Boy\n","text",[22,156,153],{"__ignoreMap":157},"",[10,159,160],{},"Reversing these code points individually splits the joiner sequences, producing a string of disconnected emoji that no longer renders as a family.",[162,163,165],"h3",{"id":164},"proper-reversal-with-grapheme-clusters","Proper Reversal with Grapheme Clusters",[10,167,168],{},"To reverse text correctly, you must segment it into grapheme clusters first, then reverse the cluster order:",[149,170,174],{"className":171,"code":172,"language":173,"meta":157,"style":157},"language-javascript shiki shiki-themes github-light github-dark","function reverseGraphemes(str) {\n  const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });\n  const clusters = [...segmenter.segment(str)].map(s => s.segment);\n  return clusters.reverse().join('');\n}\n","javascript",[22,175,176,200,237,276,302],{"__ignoreMap":157},[177,178,181,185,189,193,197],"span",{"class":179,"line":180},"line",1,[177,182,184],{"class":183},"szBVR","function",[177,186,188],{"class":187},"sScJk"," reverseGraphemes",[177,190,192],{"class":191},"sVt8B","(",[177,194,196],{"class":195},"s4XuR","str",[177,198,199],{"class":191},") {\n",[177,201,203,206,210,213,216,219,222,224,228,231,234],{"class":179,"line":202},2,[177,204,205],{"class":183},"  const",[177,207,209],{"class":208},"sj4cs"," segmenter",[177,211,212],{"class":183}," =",[177,214,215],{"class":183}," new",[177,217,218],{"class":191}," Intl.",[177,220,221],{"class":187},"Segmenter",[177,223,192],{"class":191},[177,225,227],{"class":226},"sZZnC","'en'",[177,229,230],{"class":191},", { granularity: ",[177,232,233],{"class":226},"'grapheme'",[177,235,236],{"class":191}," });\n",[177,238,240,242,245,247,250,253,256,259,262,265,267,270,273],{"class":179,"line":239},3,[177,241,205],{"class":183},[177,243,244],{"class":208}," clusters",[177,246,212],{"class":183},[177,248,249],{"class":191}," [",[177,251,252],{"class":183},"...",[177,254,255],{"class":191},"segmenter.",[177,257,258],{"class":187},"segment",[177,260,261],{"class":191},"(str)].",[177,263,264],{"class":187},"map",[177,266,192],{"class":191},[177,268,269],{"class":195},"s",[177,271,272],{"class":183}," =>",[177,274,275],{"class":191}," s.segment);\n",[177,277,279,282,285,288,291,294,296,299],{"class":179,"line":278},4,[177,280,281],{"class":183},"  return",[177,283,284],{"class":191}," clusters.",[177,286,287],{"class":187},"reverse",[177,289,290],{"class":191},"().",[177,292,293],{"class":187},"join",[177,295,192],{"class":191},[177,297,298],{"class":226},"''",[177,300,301],{"class":191},");\n",[177,303,305],{"class":179,"line":304},5,[177,306,307],{"class":191},"}\n",[149,309,313],{"className":310,"code":311,"language":312,"meta":157,"style":157},"language-python shiki shiki-themes github-light github-dark","import unicodedata\n\ndef reverse_graphemes(s):\n    clusters = []\n    current = ''\n    for char in s:\n        if unicodedata.combining(char) and current:\n            current += char\n        else:\n            if current:\n                clusters.append(current)\n            current = char\n    if current:\n        clusters.append(current)\n    return ''.join(reversed(clusters))\n","python",[22,314,315,320,326,331,336,341,347,353,359,365,371,377,383,389,395],{"__ignoreMap":157},[177,316,317],{"class":179,"line":180},[177,318,319],{},"import unicodedata\n",[177,321,322],{"class":179,"line":202},[177,323,325],{"emptyLinePlaceholder":324},true,"\n",[177,327,328],{"class":179,"line":239},[177,329,330],{},"def reverse_graphemes(s):\n",[177,332,333],{"class":179,"line":278},[177,334,335],{},"    clusters = []\n",[177,337,338],{"class":179,"line":304},[177,339,340],{},"    current = ''\n",[177,342,344],{"class":179,"line":343},6,[177,345,346],{},"    for char in s:\n",[177,348,350],{"class":179,"line":349},7,[177,351,352],{},"        if unicodedata.combining(char) and current:\n",[177,354,356],{"class":179,"line":355},8,[177,357,358],{},"            current += char\n",[177,360,362],{"class":179,"line":361},9,[177,363,364],{},"        else:\n",[177,366,368],{"class":179,"line":367},10,[177,369,370],{},"            if current:\n",[177,372,374],{"class":179,"line":373},11,[177,375,376],{},"                clusters.append(current)\n",[177,378,380],{"class":179,"line":379},12,[177,381,382],{},"            current = char\n",[177,384,386],{"class":179,"line":385},13,[177,387,388],{},"    if current:\n",[177,390,392],{"class":179,"line":391},14,[177,393,394],{},"        clusters.append(current)\n",[177,396,398],{"class":179,"line":397},15,[177,399,400],{},"    return ''.join(reversed(clusters))\n",[10,402,403,404,407,408,411],{},"Note that the Python version above is a simplified heuristic. Full grapheme segmentation requires following the Unicode Text Segmentation algorithm (UAX #29), which handles emoji ZWJ sequences, regional indicators, and Hangul jamo. Libraries like ",[22,405,406],{},"grapheme"," (Python) or ",[22,409,410],{},"graphemer"," (JavaScript) implement this correctly.",[14,413,415],{"id":414},"bidirectional-text-and-reversal","Bidirectional Text and Reversal",[10,417,418],{},"Right-to-left (RTL) scripts like Arabic and Hebrew add another layer of complexity. The Unicode Bidirectional Algorithm (UAX #9) determines the display order of mixed-direction text.",[10,420,421,422,425,426,429],{},"Consider: ",[22,423,424],{},"\"Hello و globalization\"",". The word ",[22,427,428],{},"و"," (Arabic \"and\") is RTL. The visual display order differs from the logical storage order:",[149,431,434],{"className":432,"code":433,"language":154},[152],"Logical order:  H e l l o   و   g l o b a l i z a t i o n\nDisplay order:  H e l l o   و   n o i t a z i l a b o l g\n",[22,435,433],{"__ignoreMap":157},[10,437,438],{},"Reversing the logical code points reverses the storage order, but the bidirectional algorithm then re-applies, producing unexpected visual output. The relationship between logical reversal and visual reversal is not obvious and depends on the surrounding text direction.",[14,440,442],{"id":441},"surrogate-pairs-in-utf-16","Surrogate Pairs in UTF-16",[10,444,445],{},"JavaScript and Java store strings as UTF-16 code units. Characters outside the BMP (like most emoji) are encoded as surrogate pairs — two code units representing one code point.",[149,447,449],{"className":171,"code":448,"language":173,"meta":157,"style":157},"const emoji = '😊';\nconsole.log(emoji.length);          \u002F\u002F 2 (surrogate pair)\nconsole.log([...emoji].length);     \u002F\u002F 1 (code point via spread)\n",[22,450,451,467,488],{"__ignoreMap":157},[177,452,453,456,459,461,464],{"class":179,"line":180},[177,454,455],{"class":183},"const",[177,457,458],{"class":208}," emoji",[177,460,212],{"class":183},[177,462,463],{"class":226}," '😊'",[177,465,466],{"class":191},";\n",[177,468,469,472,475,478,481,484],{"class":179,"line":202},[177,470,471],{"class":191},"console.",[177,473,474],{"class":187},"log",[177,476,477],{"class":191},"(emoji.",[177,479,480],{"class":208},"length",[177,482,483],{"class":191},");          ",[177,485,487],{"class":486},"sJ8bj","\u002F\u002F 2 (surrogate pair)\n",[177,489,490,492,494,497,499,502,504,507],{"class":179,"line":239},[177,491,471],{"class":191},[177,493,474],{"class":187},[177,495,496],{"class":191},"([",[177,498,252],{"class":183},[177,500,501],{"class":191},"emoji].",[177,503,480],{"class":208},[177,505,506],{"class":191},");     ",[177,508,509],{"class":486},"\u002F\u002F 1 (code point via spread)\n",[10,511,512],{},"Reversing at the code unit level splits surrogate pairs, producing invalid UTF-16 and rendering as replacement characters (�). Always iterate by code point or grapheme cluster, never by code unit.",[14,514,516],{"id":515},"comparison-of-reversal-granularities","Comparison of Reversal Granularities",[27,518,519,532],{},[30,520,521],{},[33,522,523,526,529],{},[36,524,525],{},"Level",[36,527,528],{},"Preserves",[36,530,531],{},"Breaks",[46,533,534,545,556],{},[33,535,536,539,542],{},[51,537,538],{},"Code unit (UTF-16)",[51,540,541],{},"ASCII",[51,543,544],{},"Surrogate pairs, combining marks, emoji",[33,546,547,550,553],{},[51,548,549],{},"Code point",[51,551,552],{},"BMP characters",[51,554,555],{},"Combining marks, ZWJ sequences",[33,557,558,561,564],{},[51,559,560],{},"Grapheme cluster",[51,562,563],{},"Combining marks, emoji, ZWJ sequences",[51,565,566],{},"RTL visual order, edge cases",[10,568,569],{},"For most practical purposes, grapheme-cluster reversal is the correct granularity.",[14,571,573],{"id":572},"key-takeaways","Key Takeaways",[125,575,576,579,582,589,592],{},[128,577,578],{},"Naïve code-point reversal breaks combining marks, separating diaereses from their base letters.",[128,580,581],{},"Emoji with ZWJ sequences (family, skin tones) are multi-code-point grapheme clusters that must not be split.",[128,583,584,585,588],{},"Use ",[22,586,587],{},"Intl.Segmenter"," (JavaScript) or a grapheme library (Python) to segment text before reversing.",[128,590,591],{},"RTL scripts interact with reversal in non-obvious ways because the bidirectional algorithm re-applies to the reversed logical order.",[128,593,594],{},"UTF-16 code-unit reversal is always wrong for text containing emoji or other supplementary characters.",[14,596,598],{"id":597},"try-it-yourself","Try It Yourself",[10,600,601,602,607],{},"Curious how reversal handles emoji and accented characters? Test any string in the ",[603,604,606],"a",{"href":605},"\u002Ftools\u002Ftext-reverser","Text Reverser"," and see the difference proper Unicode handling makes.",[609,610,611],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":157,"searchDepth":202,"depth":202,"links":613},[614,615,618,619,620,621,622],{"id":16,"depth":202,"text":17},{"id":119,"depth":202,"text":120,"children":616},[617],{"id":164,"depth":239,"text":165},{"id":414,"depth":202,"text":415},{"id":441,"depth":202,"text":442},{"id":515,"depth":202,"text":516},{"id":572,"depth":202,"text":573},{"id":597,"depth":202,"text":598},"2026-05-28","Why reversing Unicode text with emojis and RTL characters is harder than it looks.","md",{"immutable":324},"\u002Fguides\u002Funicode-bidi-text-reversal",{"title":5,"description":624},"guides\u002Funicode-bidi-text-reversal","50mxMyoIIGf2FGywPSSd5U6MDCoHEX-sI-ORy8qrGuo",1780401337771]