◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Expo SDK 58 is out of beta as of 10/15 (beta since 09/15). Check your dependencies before upgrading●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Expo SDK 58 is out of beta as of 10/15 (beta since 09/15). Check your dependencies before upgrading●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-07-15Advanced

The Comma That Fell to the Start of a Line: Rork, Quote Cards, and Japanese Line Breaking

React Native's Text component does not guarantee Japanese line-breaking rules. Here are the violation rates I measured, a WORD JOINER implementation that survives copy and VoiceOver, an onTextLayout audit harness, and how Rork Max (SwiftUI) differs.

Rork588React Native240Expo219JapaneseTypographyText

✦ Premium Article

I picked up the iPhone SE and stopped halfway through the screen.

A quote card in one of my wallpaper apps. The second line began with a Japanese comma — the punctuation had fallen to the start of the line, which Japanese typography does not allow.

Rork had generated the screen from a plain request: show the quote large and centered. The code worked. The layout held. It simply was not set as Japanese text.

The iPhone 15 Pro simulator never showed it. Different width, different wrap points. I had only been lucky.

React Native's Text does not guarantee line-breaking rules

React Native's Text does not implement line breaking itself. On iOS it hands off to TextKit / CoreText. On Android it hands off to android.text.LineBreaker.

Wrap quality therefore depends on the platform and its locale resolution. That turned out to be the hard part.

EnvironmentLine-start rulesWhat I observed
iOS (default)CoreText applies some rulesCommas and periods mostly avoided. Prolonged sound mark and small kana still fall to line start
iOS with lineBreakStrategyIOS="push-out"Pushes the previous character downViolations drop sharply, but line count can grow
Android (default simple)Essentially noneCommas, periods, and closing brackets land at line start
Android with textBreakStrategy="balanced"Evens out line lengthsLooks tidier, but the rules themselves are not guaranteed

I had assumed Android would apply Japanese rules whenever the device was set to Japanese. In practice the decision comes from the locale attached to the text, not from the user's system language.

My apps ship in 16 languages. A reader looking at Japanese quotes may well have an English device. I had missed that premise entirely.

I measured the violation rate first

Fixing by feel produces the feeling of a fix and nothing else. So I took numbers.

The sample: 120 quote strings used across six wallpaper apps, including the calm and affirmation titles. Four container widths: 320, 375, 393, and 430.

onTextLayout returns each line after layout resolves. If you can read the first character of a line, you can decide the violation directly.

// KinsokuAudit.tsx — detect line-start violations at runtime
import { Text, type TextLayoutEventData, type NativeSyntheticEvent } from "react-native";
 
// Characters that must not start a line: punctuation, small kana, prolonged sound mark
const LINE_START_FORBIDDEN =
  "、。,.・:;?!ヽヾゝゞ々ー’”)〕]}〉》」』】…‥" +
  "ぁぃぅぇぉっゃゅょゎァィゥェォッャュョヮヵヶ";
 
type Violation = { lineIndex: number; head: string; lineText: string };
 
export function auditLines(
  e: NativeSyntheticEvent<TextLayoutEventData>
): Violation[] {
  const lines = e.nativeEvent.lines;
  const out: Violation[] = [];
 
  // Line 0 is a line start, but not one produced by wrapping. Skip it.
  for (let i = 1; i < lines.length; i++) {
    const text = lines[i].text ?? "";
    const head = Array.from(text)[0];
    if (!head) continue;
    if (LINE_START_FORBIDDEN.includes(head)) {
      out.push({ lineIndex: i, head, lineText: text });
    }
  }
  return out;
}
 
export function QuoteCard({ quote, onAudit }: { quote: string; onAudit?: (v: Violation[]) => void }) {
  return (
    <Text
      style={{ fontSize: 22, lineHeight: 36 }}
      onTextLayout={(e) => onAudit?.(auditLines(e))}
    >
      {quote}
    </Text>
  );
}

Skipping line 0 keeps the number meaningful: I only want line starts that wrapping created. Source text rarely opens with a comma, but excluding it removes the ambiguity.

I ran all 120 strings through four fixed-width containers. The results:

ConditionViolations / wrapped lines (iOS)Violations / wrapped lines (Android)
Plain Text (defaults)31 / 664 (4.7%)96 / 671 (14.3%)
lineBreakStrategyIOS="push-out" only9 / 682 (1.3%)96 / 671 (14.3%)
textBreakStrategy="balanced" only31 / 664 (4.7%)88 / 669 (13.2%)
Pre-binding (below)0 / 689 (0%)0 / 690 (0%)

Android's 14.3% is the number that landed. Roughly one wrapped line in seven broke in a way a Japanese reader would notice.

push-out helps a great deal on iOS, but the nine survivors were all prolonged sound marks and small kana. Leave the decision to the platform and it will not go that far for you.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦Measured violation rates across 120 quotes and 4 device widths, plus the harness that produced them
✦A WORD JOINER pre-binding implementation that keeps copy, search, and VoiceOver intact
✦A CI audit that catches double-application, and where Rork Max (SwiftUI) behaves differently
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

⬡ Dev Tools2026-09-04
EAS secret visibility does not keep a value out of your app — deciding prefix and visibility separately
The EXPO_PUBLIC_ prefix decides what ships inside your app; EAS visibility decides who can read it. Why stacking them blanks a value on OTA updates, and how to check your build.
⬡ Dev Tools2026-08-22
Every bulk replace exited zero. The damage was in the lines I did not delete
Run a bulk replace over generated code and the breakage lands on the neighbouring lines, not the matched ones. Here is what broke in a live project, and a dependency-free guard that checks the invariants a replace must preserve.
⬡ Dev Tools2026-07-30
What Renovate may bump in an Expo app, and what it must never touch
Turning on automated dependency updates in a Rork-generated app also hands Renovate the 123 packages Expo SDK 57 pins. Measured on 2026-07-30, six of them sit a full major version behind npm latest. Here is how to generate the ignore list from the SDK instead of maintaining it by hand.