why-hydration 0.1.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,121 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Hebrew and Persian overlays, live prop updates, one report per invalid nesting.
8
+
9
+ **Hebrew and Persian**
10
+
11
+ - Pages whose `<html lang>` is Hebrew (`he`, `iw`) or Persian (`fa`, `prs`,
12
+ `pes`) now get the overlay in that language, right-to-left, alongside the
13
+ Arabic one. `overlay={{ locale }}` accepts `'he'` and `'fa'` too. Other
14
+ right-to-left languages, such as Urdu, keep the English left-to-right panel.
15
+
16
+ **`<HydrationInspector>` props apply while the app runs**
17
+
18
+ - Props were read once, at mount, and later changes were ignored until a
19
+ reload. Now a new `onReport` receives the next report, a new `overlay`
20
+ (on/off, `position`, `locale`) rebuilds the panel with the reports so far,
21
+ and new `ignore`, `classify` and `maxReports` apply from then on. Inline
22
+ values re-created on every render do not rebuild anything.
23
+
24
+ **Invalid HTML nesting is one report**
25
+
26
+ - A `<div>` inside a `<p>` used to surface as three wrongly labelled reports
27
+ ("browser-only API", "viewport branching" twice) next to React's own
28
+ warning, and a real text change inside it was never reported as one. The
29
+ browser repairs the server's HTML while parsing it, but React builds the
30
+ client DOM node by node, so the client side is now repaired the same way
31
+ before comparing. The nesting is one report at the misplaced element, and a
32
+ text difference inside it is its own report. Covers block elements in `<p>`,
33
+ nested `<a>`/`<button>`/`<form>`, and table content outside its section or
34
+ row, which the parser moves out of the whole table.
35
+
36
+ **Other fixes**
37
+
38
+ - A `roots` selector that matches nothing on the page — usually a typo — is
39
+ warned about once the settling window closes, instead of being skipped
40
+ silently.
41
+ - Reports no longer show the wrong component. A report parsed from one of
42
+ React's messages borrowed the component of whichever error came last, and
43
+ a text node the client added pointed at nothing, so its lookup fell back
44
+ the same way (a `localStorage` read labelled `<InvalidNesting>`). Each
45
+ report now carries only its own context, and an added text node points at
46
+ its parent element — which also lets `ignore` match it.
47
+ - Adjacent text nodes (`{label}: ` in JSX) are kept apart when the client
48
+ side of an invalid nesting is repaired, matching React's server HTML, so
49
+ they no longer show up as a bogus text change.
50
+ - The Release workflow no longer fails on every merge when no `NPM_TOKEN` is
51
+ set: it skips publishing with a notice and still opens version PRs.
52
+ - A GitHub Pages site (`docs/`) with search metadata, structured data and a
53
+ sitemap. A test keeps its error list identical to the README's.
54
+
55
+ ### Patch Changes
56
+
57
+ - Make the package easier to find.
58
+
59
+ - The README has a "Searching for this error?" section quoting React's
60
+ hydration error messages exactly as React 18 and React 19 print them, so a
61
+ search for the error leads here. A test reads the list back out of the
62
+ README and checks the package recognizes every message on it.
63
+ - The npm description and keywords now cover the terms people search for:
64
+ Next.js, React 18 and 19, "hydration failed", "text content does not match",
65
+ `suppressHydrationWarning`, RTL and Arabic.
66
+
67
+ No code changes to this part.
68
+
69
+ ## 0.2.0
70
+
71
+ ### Minor Changes
72
+
73
+ - The overlay now speaks Arabic, and renders right-to-left data correctly in any language.
74
+
75
+ **Arabic overlay**
76
+
77
+ - On a page whose `<html lang>` is Arabic, the whole panel is in Arabic and
78
+ laid out right-to-left — title, buttons, category names, explanations, fixes,
79
+ and the scroll hint, with correct Arabic plurals. Every other page, including
80
+ right-to-left pages in languages the overlay does not translate, gets the
81
+ English panel as before.
82
+ - New `overlay={{ locale: 'auto' | 'en' | 'ar' }}` option. `'auto'` is the
83
+ default and follows `<html lang>`.
84
+ - The console output and the `explanation`/`suggestion` strings in `onReport`
85
+ stay English and are byte-identical to 0.1.5. Every built-in cause now also
86
+ carries `messageId` and `params`, which is how the overlay finds the
87
+ translation.
88
+
89
+ **Right-to-left rendering fixes**
90
+
91
+ - Arabic values no longer have their sentence punctuation moved to the wrong
92
+ end. Each value cell takes its direction from its own content, in either
93
+ panel language.
94
+ - Arabic values are drawn in a proportional face instead of a monospace
95
+ fallback, which rendered them with their letters disconnected.
96
+ - Selectors, component names, file paths and attribute names are isolated
97
+ left-to-right, and quoted values inside explanations are isolated one by one,
98
+ so brackets cannot mirror and tokens cannot swap places.
99
+ - The panel declares its own `lang`. Previously an English panel on an Arabic
100
+ page inherited `lang="ar"`, so screen readers voiced it with an Arabic voice.
101
+
102
+ **Other fixes**
103
+
104
+ - Invisible bidi marks (LRM, RLM, isolates…) are drawn as labelled badges in
105
+ the value cells, so the bidi-mark case no longer shows two identical-looking
106
+ values. Value cells also preserve whitespace, so whitespace-only mismatches
107
+ are visible.
108
+ - Component attribution from a React component stack no longer names a DOM tag
109
+ such as `<p>`; it names the nearest component you wrote.
110
+ - React's bare "hydration failed" message no longer adds an empty card next to
111
+ a real mismatch. When it is the only signal it is still reported, with text
112
+ that says React did not name the node, instead of "inspect the values above".
113
+ - React 19 hydration errors no longer produce junk reports. React 19 prints a
114
+ bulleted list of possible causes above its diff tree, and each bullet was
115
+ read as a removed ("server") diff line — five bogus "Unknown" reports per
116
+ mismatch, with the real client value paired against the first bullet. Only
117
+ the tree after React's hydration-mismatch link is read as a diff now.
118
+
3
119
  ## 0.1.5
4
120
 
5
121
  ### Patch Changes
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  [![node: >=18](https://img.shields.io/node/v/why-hydration)](#install)
8
8
  [![license: MIT](https://img.shields.io/npm/l/why-hydration.svg)](LICENSE)
9
9
 
10
- 📦 **npm:** https://www.npmjs.com/package/why-hydration &nbsp;·&nbsp; 🐙 **GitHub:** https://github.com/razan-aboushi/why-hydration &nbsp;·&nbsp; 💼 **Author:** [Razan Aboushi](https://www.linkedin.com/in/razan-aboushi/)
10
+ 🌐 **Site:** https://razan-aboushi.github.io/why-hydration/ &nbsp;·&nbsp; 📦 **npm:** https://www.npmjs.com/package/why-hydration &nbsp;·&nbsp; 🐙 **GitHub:** https://github.com/razan-aboushi/why-hydration &nbsp;·&nbsp; 💼 **Author:** [Razan Aboushi](https://www.linkedin.com/in/razan-aboushi/)
11
11
 
12
12
  **Tells you which component broke hydration, what differed, and how to fix it — in dev, with zero production cost.**
13
13
 
@@ -60,9 +60,10 @@ together.
60
60
  incidental page noise.
61
61
  - 🌐 **Arabic-first locale detection** — digit-script mismatches (٠١٢ vs 012)
62
62
  are a first-class cause, not an afterthought.
63
- - 🔤 **RTL-safe overlay** — the overlay always renders left-to-right, even on
64
- pages with `<html dir="rtl">`, since its content (paths, values, code) is
65
- English.
63
+ - 🔤 **Speaks Arabic, Hebrew and Persian, right-to-left** — on a page whose
64
+ `<html lang>` is one of those, the overlay is in that language and laid out
65
+ right-to-left; everywhere else it is in English. Page data renders in its own direction in both, and invisible bidi
66
+ marks are shown instead of hidden.
66
67
  - 🫧 **Zero production cost** — every code path is gated behind
67
68
  `process.env.NODE_ENV`, and CI fails the build if the production bundle for
68
69
  any entry point isn't tree-shaken to a no-op.
@@ -78,6 +79,7 @@ Supports **React 18 and 19**.
78
79
  - [Install](#install)
79
80
  - [Quick setup](#quick-setup)
80
81
  - [What is a hydration mismatch](#what-is-a-hydration-mismatch)
82
+ - [Searching for this error?](#searching-for-this-error)
81
83
  - [What why-hydration detects](#what-why-hydration-detects)
82
84
  - [How detection works](#how-detection-works)
83
85
  - [What you will see](#what-you-will-see)
@@ -237,6 +239,37 @@ resolved a different locale or timezone. React logs a warning to the console
237
239
  when it detects this and, depending on what mismatched, may or may not repair
238
240
  the DOM to match. `why-hydration` exists to make that warning actionable.
239
241
 
242
+ ### Searching for this error?
243
+
244
+ If you landed here from one of these messages, this is the tool for it. Each
245
+ is quoted exactly as React prints it, and `why-hydration` recognizes every one.
246
+ With the [snapshot script](#quick-setup) in place it also names the component,
247
+ shows the server and client values side by side — including for the messages
248
+ that carry no values of their own — and tells you the likely cause and fix.
249
+
250
+ **React 19** (and Next.js 15+, which uses it):
251
+
252
+ ```text
253
+ Hydration failed because the server rendered text didn't match the client. As a result this tree will be regenerated on the client.
254
+ Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client.
255
+ A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up.
256
+ In HTML, <div> cannot be a descendant of <p>. This will cause a hydration error.
257
+ ```
258
+
259
+ **React 18** (and Next.js 13–14):
260
+
261
+ ```text
262
+ Hydration failed because the initial UI does not match what was rendered on the server.
263
+ There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.
264
+ There was an error while hydrating this Suspense boundary. Switched to client rendering.
265
+ Text content does not match server-rendered HTML.
266
+ Warning: Text content did not match. Server: "…" Client: "…"
267
+ Warning: Prop `className` did not match. Server: "…" Client: "…"
268
+ Warning: Expected server HTML to contain a matching <div> in <div>.
269
+ Warning: Did not expect server HTML to contain a <div> in <div>.
270
+ Warning: validateDOMNesting(...): <div> cannot appear as a descendant of <p>.
271
+ ```
272
+
240
273
  ---
241
274
 
242
275
  ## What why-hydration detects
@@ -339,6 +372,7 @@ A page with no mismatches renders **nothing** — no overlay, no console output.
339
372
  | **✕** on the hint bar | Closes just the "scroll to see all" hint; the panel stays. |
340
373
  | `overlay={false}` | Never mounts it at all — `onReport` and the console output still work. |
341
374
  | `overlay={{ position }}` | `bottom-right` (default), `bottom-left`, `top-right`, `top-left`. |
375
+ | `overlay={{ locale }}` | `'auto'` (default) follows `<html lang>`; `'en'`, `'ar'`, `'he'` or `'fa'` pins the language. See [RTL and Arabic support](#rtl-and-arabic-support). |
342
376
 
343
377
  The overlay is a *view* over the collected reports, not the collector itself:
344
378
  dismissing it does not stop detection, and `onReport` keeps firing. If a
@@ -368,24 +402,60 @@ there are more than fit:
368
402
 
369
403
  ## RTL and Arabic support
370
404
 
371
- An Arabic app gets the same diagnosis quality as an English one. That covers
372
- both how the overlay renders and what the engine can actually detect.
405
+ An Arabic, Hebrew or Persian app gets the same diagnosis quality as an English
406
+ one. That covers both how the overlay renders and what the engine can actually
407
+ detect.
373
408
 
374
409
  ### The overlay
375
410
 
376
- - The overlay's own layout **always renders left-to-right**, on any page. Its
377
- content — file paths, DOM selectors, code values, category names — is
378
- English, so keeping it LTR keeps it readable regardless of the host page's
379
- direction.
380
- - This is automatic and needs no configuration. The overlay lives in an
381
- isolated Shadow DOM, sets `direction: ltr` on both `:host` and the panel, and
382
- carries a `dir="ltr"` attribute as well — belt and braces, because the CSS
383
- `all` shorthand deliberately excludes `direction` (per spec), so
384
- `:host { all: initial }` alone would still let `direction: rtl` leak in and
385
- flip the server/client diff columns.
386
- - Values are rendered as **text**, so Arabic, Hebrew and mixed bidi content
387
- display intact inside the LTR panel without reordering the surrounding
388
- layout.
411
+ **The overlay speaks the page's language.** On a page whose `<html lang>` is
412
+ Arabic, Hebrew or Persian, the whole panel is in that language and laid out
413
+ right-to-left: title, buttons, category names, the explanation, the fix, and
414
+ the scroll hint, with each language's plural rules.
415
+
416
+ | `<html lang>` | Overlay |
417
+ | ------------- | ------- |
418
+ | `ar`, `ar-SA`, `ar-EG`, … and regional varieties (`arz`, `ary`, …) | Arabic |
419
+ | `he`, `he-IL`, and the legacy `iw` | Hebrew |
420
+ | `fa`, `fa-IR`, `fa-AF`, and `prs` (Dari), `pes` | Persian |
421
+ | anything else — including other right-to-left languages such as Urdu | English, left-to-right |
422
+
423
+ An untranslated right-to-left page keeps the English panel left-to-right rather
424
+ than mirroring English text. Pin the language either way with
425
+ `overlay={{ locale: 'en' }}`, `'ar'`, `'he'` or `'fa'`.
426
+
427
+ What stays English: the console output, and `cause.explanation` /
428
+ `cause.suggestion` in the report `onReport` receives. Those are what people log,
429
+ grep and paste into issues, so they do not change with the page. Each report
430
+ also carries `cause.messageId` and `cause.params`, which is how the overlay
431
+ finds the translation — and how your own tooling can, too. The "Learn more"
432
+ links point at this README, which is in English, and say so.
433
+
434
+ **Page data renders in its own direction, in either panel.** A mismatched value
435
+ can be in any script, so each value cell takes its direction from its own first
436
+ strong character. `السعر: ١٬٤٠٠ د.ك.` reads right-to-left with its full stop at
437
+ the end even inside the English panel, and `Price: 1,400 KWD.` reads
438
+ left-to-right inside the Arabic one. Selectors, component names, file paths and
439
+ attribute names are code, so they are always isolated left-to-right — otherwise
440
+ a right-to-left line would mirror their brackets and reorder their segments.
441
+ Class tokens and other values quoted inside a sentence are isolated one by one,
442
+ so two Arabic tokens cannot swap places. Arabic is drawn in a proportional face
443
+ (a monospace fallback renders it with its letters disconnected), and Arabic
444
+ labels are never letter-spaced or uppercased, which breaks their joining.
445
+
446
+ **What differs is what you see.** Value cells preserve whitespace, so a
447
+ whitespace-only mismatch no longer looks like two identical strings. Invisible
448
+ characters that change layout — `LRM`, `RLM`, `ALM`, the bidi embeddings and
449
+ isolates, zero-width space, BOM — are drawn as small labelled badges rather than
450
+ applied, so the [bidi-mark case](#cause-locale-format) shows a visible
451
+ difference instead of two cells that look the same.
452
+
453
+ **It is isolated from the page.** The panel lives in a Shadow DOM, and its
454
+ direction is set inside it, so nothing in the host page's CSS can flip it — the
455
+ `all` shorthand deliberately excludes `direction` (per spec), so
456
+ `:host { all: initial }` alone would not be enough. The panel also declares its
457
+ own `lang`, so a screen reader voices it in its own language: before this, an
458
+ English panel on an Arabic page was read aloud with an Arabic voice.
389
459
 
390
460
  ### Detection
391
461
 
@@ -460,6 +530,14 @@ Wrap your app (or, for Next.js, use the `why-hydration/next` re-export).
460
530
  | `classify` | `Classifier[]` | `[]` | Custom classification rules, run **before** the built-in ones. |
461
531
  | `maxReports` | `number` | `25` | Cap on the number of unique reports collected per page load. |
462
532
 
533
+ Props can change while the app is running, and take effect without a reload:
534
+ a new `onReport` receives the next report; a new `overlay` (switched on or off,
535
+ or a different `position` or `locale`) rebuilds the panel with the reports so
536
+ far; new `ignore`, `classify` or `maxReports` apply to everything reported from
537
+ then on. Reports already made are kept as they were. Inline values that are
538
+ re-created on every render, such as `overlay={{ position: 'top-left' }}`, do
539
+ not rebuild anything unless they actually change.
540
+
463
541
  ### `createHydrationInspector(options)`
464
542
 
465
543
  For Vite/CRA/Remix, where you call `hydrateRoot` yourself. Accepts the same
@@ -490,6 +568,7 @@ effect setup → cleanup → setup.
490
568
  ```ts
491
569
  interface OverlayOptions {
492
570
  position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'; // default 'bottom-right'
571
+ locale?: 'auto' | 'en' | 'ar' | 'he' | 'fa'; // default 'auto' — follows <html lang>
493
572
  }
494
573
  ```
495
574
 
@@ -538,9 +617,11 @@ interface HydrationReport {
538
617
  cause: {
539
618
  category: HydrationCauseCategory; // one of the categories below, or "unknown"
540
619
  confidence: number; // 0–1
541
- explanation: string;
542
- suggestion: string;
620
+ explanation: string; // always English
621
+ suggestion: string; // always English
543
622
  docsUrl?: string;
623
+ messageId?: string; // which message produced the text, e.g. "attribute-mismatch.class"
624
+ params?: Record<string, string | string[]>; // the values in it, e.g. { added: ['wide'], removed: [] }
544
625
  };
545
626
  raw?: { reactMessage?: string }; // the original React console message, if any
546
627
  }
@@ -662,6 +743,16 @@ JavaScript.
662
743
  A node was moved or ejected because the markup is invalid HTML (e.g. a `<div>`
663
744
  inside a `<p>`, or a nested `<a>`). The browser repairs the server-rendered DOM
664
745
  so it no longer matches what React expects.
746
+
747
+ It is reported **once**, at the element that is misplaced. The browser repairs
748
+ the server's HTML while parsing it, but React builds the client DOM node by
749
+ node, so nothing repairs that side — and compared as-is, one invalid `<div>`
750
+ used to surface as three unrelated reports. The client side is now put through
751
+ the same parser repair before comparing, so the nesting shows up as a single
752
+ report, and any real text difference inside it still shows up as its own.
753
+ Covered: block elements inside `<p>`, nested `<a>`, `<button>` and `<form>`,
754
+ and table content outside its section or row (which the parser moves out of
755
+ the whole table).
665
756
  **Fix:** correct the markup validity — block elements cannot live inside
666
757
  `<p>`, anchors cannot nest, etc.
667
758
  **Reference:** [MDN — `<p>` (permitted content)](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/p).
@@ -701,6 +792,13 @@ drive the visual change with a CSS media query instead of a JS class toggle.
701
792
  A mismatch was detected but didn't match any of the rules above. The report
702
793
  still shows the exact server vs. client values and node path so you can
703
794
  diagnose it directly.
795
+
796
+ React also logs a bare "hydration failed" message that names no node at all.
797
+ When the DOM diff or another React message has already located the mismatch,
798
+ that message adds nothing and is not shown. When it is the *only* signal —
799
+ typically because the snapshot script is missing — it is reported once, with an
800
+ explanation that says React did not name the node and a fix that points at the
801
+ snapshot script, rather than asking you to inspect values that do not exist.
704
802
  **Fix:** compare the two values — the cause is usually one of the categories
705
803
  above. If you find a reliable signal for it, add a custom rule via the
706
804
  `classify` option (see [Contributing](#contributing)).
@@ -729,9 +827,12 @@ above. If you find a reliable signal for it, add a custom rule via the
729
827
  budget (`npm run size`) against a real production bundle built with webpack
730
828
  and terser — the same toolchain Next.js and CRA use for production — and
731
829
  fails the build if the tree-shaken output for any entry point isn't reduced
732
- to a near-empty stub. Verified independently against Rollup, which is what
733
- Vite uses for production builds: **99 B** under webpack, **166 B** under
734
- Rollup, versus ~40 KB for the same entry built for development.
830
+ to a near-empty stub. The budgets sit just above today's output, so even a
831
+ small leak fails CI. Verified independently against Rollup, which is what
832
+ Vite uses for production builds: **99 B** minified + gzipped under webpack,
833
+ and **156 B** minified (not gzipped) under Rollup — versus ~59 KB minified
834
+ for the same entry built for development. The Arabic translations exist only
835
+ in that development build.
735
836
  - `<HydrationSnapshotScript>` also renders `null` outside development, so no
736
837
  snapshot script is emitted into your production HTML.
737
838
 
@@ -788,9 +889,11 @@ snapshot script is present in `<head>` and runs before your app's hydration
788
889
  script. Without the snapshot script, the tool still reports mismatches it can
789
890
  parse from React's own console warning, but loses the precise DOM-level diff.
790
891
  If you passed `roots`, check the console for a
791
- `[why-hydration] Skipping root:` warning — a root with no captured server HTML
792
- can never produce a DOM-level report, and it says so rather than failing
793
- silently.
892
+ `[why-hydration] Skipping root:` warning. There are three: a selector that is
893
+ not valid CSS, a root with no captured server HTML (it can never produce a
894
+ DOM-level report), and a selector that matches nothing on the page — usually a
895
+ typo. The last is only raised once the settling window has closed, so a root
896
+ rendered a moment after hydration is not reported by mistake.
794
897
 
795
898
  **Does it work under `<React.StrictMode>`?** Yes. Strict Mode runs every mount
796
899
  effect setup → cleanup → setup in development, which tears the inspector down
@@ -802,6 +905,11 @@ to the rebuilt inspector rather than lost.
802
905
  suspend `requestAnimationFrame` on hidden pages, so scheduling races a frame
803
906
  against a 50 ms timer and no longer depends on the page being painted.
804
907
 
908
+ **The component name in a report was a tag like `<p>`.** Fixed. React's
909
+ component stacks list host elements first (`at p`, `at span`), and those were
910
+ being taken as the component. The nearest component you wrote is used instead,
911
+ and when there is none the report names no component rather than a tag.
912
+
805
913
  **The "Learn more →" link 404s.** The links point at this README on GitHub
806
914
  (`github.com/razan-aboushi/why-hydration#cause-…`). If you've forked the
807
915
  package under a different name or repository, update `DOCS_BASE` in
@@ -823,16 +931,28 @@ production. It's dev-only, runs a bounded diff across roughly 1.5 seconds
823
931
  right after hydration, then stops.
824
932
 
825
933
  **Does it work with `<html dir="rtl">`?** Yes — see
826
- [RTL and Arabic support](#rtl-and-arabic-support). The overlay stays
827
- left-to-right on purpose; this is not a bug. Detection covers Arabic-script
828
- formatting mismatches on both sides, not just Arabic-vs-Latin.
829
-
830
- **My Arabic app shows two identical-looking values as a mismatch.** They differ
831
- by invisible bidirectional control characters — `Intl` adds LRM/RLM/isolate
934
+ [RTL and Arabic support](#rtl-and-arabic-support). The panel's language follows
935
+ `<html lang>`, not `dir`: an Arabic, Hebrew or Persian page gets the panel in
936
+ that language, right-to-left, and a right-to-left page in another language gets
937
+ the English left-to-right one. Detection covers Arabic-script formatting
938
+ mismatches on both sides, not just Arabic-vs-Latin.
939
+
940
+ **I want the English panel on my Arabic site (or the reverse).** Pass
941
+ `overlay={{ locale: 'en' }}` (or `'ar'`, `'he'`, `'fa'`). The console and
942
+ `onReport` are English either way.
943
+
944
+ **I changed a `<HydrationInspector>` prop and nothing happened.** Fixed —
945
+ props used to be read once, at mount. They now apply while the app runs; see
946
+ [`<HydrationInspector>`](#hydrationinspector).
947
+
948
+ **My Arabic app reports a mismatch between two values that look the same.** They
949
+ differ by invisible bidirectional control characters — `Intl` adds LRM/RLM/isolate
832
950
  marks around numbers and dates in RTL locales, and Node's ICU and the browser's
833
951
  ICU do not always agree on which. The report names this explicitly under
834
- [`locale-format`](#cause-locale-format). Format the value in one place and pass
835
- the string down, or add `suppressHydrationWarning` if the marks are harmless.
952
+ [`locale-format`](#cause-locale-format), and the overlay draws each invisible
953
+ mark as a small labelled badge (`RLM`, `LRM`, …) in the value cell, so you can
954
+ see which side has it. Format the value in one place and pass the string down,
955
+ or add `suppressHydrationWarning` if the marks are harmless.
836
956
 
837
957
  ---
838
958