RORK LABJP
SDK57 — Expo SDK 57 landed, moving React Native from 0.85 to 0.86 while React stays at 19.2, and it is meant to ship no breaking changesCADENCE — The release hints at a new cadence: small, non-breaking upgrades slotted between the larger SDK releases, which makes keeping up easier to planRN086 — React Native 0.86 highlights include edge-to-edge fixes on Android, light and dark mode emulation in React Native DevTools, and rendering, layout, and animation fixesPREBUILD — expo prebuild improved how it clears and regenerates native directories, and expo-dev-client picked up iOS enhancementsIOS27 — iOS 27 reached beta 4 on July 20 and opened to public beta testers on July 22, so it is time to check generated apps ahead of the autumn releaseAND17 — Starting with Android 17, traditional Developer Previews are gone, replaced by continuously updated Canary buildsSDK57 — Expo SDK 57 landed, moving React Native from 0.85 to 0.86 while React stays at 19.2, and it is meant to ship no breaking changesCADENCE — The release hints at a new cadence: small, non-breaking upgrades slotted between the larger SDK releases, which makes keeping up easier to planRN086 — React Native 0.86 highlights include edge-to-edge fixes on Android, light and dark mode emulation in React Native DevTools, and rendering, layout, and animation fixesPREBUILD — expo prebuild improved how it clears and regenerates native directories, and expo-dev-client picked up iOS enhancementsIOS27 — iOS 27 reached beta 4 on July 20 and opened to public beta testers on July 22, so it is time to check generated apps ahead of the autumn releaseAND17 — Starting with Android 17, traditional Developer Previews are gone, replaced by continuously updated Canary builds
Articles/Dev Tools
Dev Tools/2026-06-13Intermediate

When Hiragana Doesn't Match Katakana — Japanese-Aware In-App Search for Rork Apps

A hiragana query silently missing katakana titles is a UX failure users never report. How I fixed Japanese search in my wallpaper app with NFKC normalization, kana folding, and precomputed search keys.

Rork521SearchJapanese LocalizationText NormalizationExpo151React Native213

When I added a search box to one of the wallpaper apps I run as an indie developer, an early tester gave me a puzzling report: searching for さくら (sakura, written in hiragana) returned nothing — even though the catalog clearly contained wallpapers titled サクラ (the exact same word, written in katakana). To a Japanese reader these are interchangeable spellings of one word. To a string comparison, they are entirely different characters.

Rork will happily wire up a working search feature in minutes if you ask for one. But this class of problem — orthographic variation — will never be solved unless you spell it out as a requirement. Here is where my implementation fell short, how I fixed it, and the exact spec I now paste into Rork.

What the Search Rork Generates by Default Actually Covers

If you tell Rork nothing more than "add a search feature," the generated code usually boils down to this:

const [query, setQuery] = useState("");
 
const results = items.filter((item) =>
  item.title.toLowerCase().includes(query.toLowerCase())
);

Substring matching and case-insensitive English are covered out of the box. For an English-only catalog, this is perfectly serviceable — the AI infers the standard English-language implementation from the word "search," and it gets there fast.

For Japanese data, though, several variants silently slip through:

  • さくら vs. サクラ (hiragana vs. katakana)
  • ABC vs. ABC (half-width vs. full-width alphanumerics)
  • サクラ vs. サクラ (half-width vs. full-width katakana)
  • 夜 景 vs. 夜景 (a stray space inside a word)

In my app, wallpaper titles happened to be in katakana while tags were in hiragana — the inconsistency was baked in the moment humans typed the data. Rather than retroactively cleaning thousands of records, absorbing the variation at search time is the pragmatic move. The same lesson applies beyond Japanese: accented characters, ligatures, and width variants all fail "obvious" searches in exactly this way.

One Normalization Function That Folds Variants Together

The strategy is simple: immediately before comparing, run both the query and the data through the same transformation. That transformation is called normalization. This function is ready to drop in:

// utils/searchNormalize.ts
export function normalizeForSearch(input: string): string {
  return input
    .normalize("NFKC") // full-width → half-width latin, half-width → full-width kana
    .toLowerCase() // fold latin case
    .replace(/[ぁ-ゖ]/g, (ch) =>
      // hiragana → katakana (the blocks are exactly 0x60 apart)
      String.fromCharCode(ch.charCodeAt(0) + 0x60)
    )
    .replace(/[\s ]+/g, ""); // strip half- and full-width whitespace
}

The first line does the heavy lifting. Unicode ships a standard mechanism — NFKC normalization — for collapsing "visually similar but technically different" characters into a canonical form: full-width ABC becomes ABC, half-width サクラ becomes サクラ. No hand-maintained conversion tables required.

The one thing NFKC will not do is unify hiragana with katakana, so we handle that ourselves. Conveniently, the two scripts sit exactly 0x60 apart in Unicode, which makes the conversion a one-line character-code shift.

Run through this function, all three of these collapse to the same string サクラ:

normalizeForSearch("さくら"); // "サクラ"
normalizeForSearch("サクラ"); // "サクラ"
normalizeForSearch("サ クラ"); // "サクラ"

Normalize Once at Load Time, Not on Every Keystroke

With the function in place, calling it on both sides inside filter works — but written naively, you are re-normalizing the entire catalog on every single keystroke.

My catalog held about 1,200 items, and on an older Android test device the input field developed a noticeable lag. The cause was plain: each keypress triggered 1,200 normalization passes. The fix is to normalize the data side once at load time and keep the result as a precomputed search key:

// hooks/useWallpaperSearch.ts
import { useMemo, useState, useDeferredValue } from "react";
import { normalizeForSearch } from "../utils/searchNormalize";
 
type Wallpaper = { id: string; title: string; tags: string[] };
 
export function useWallpaperSearch(items: Wallpaper[]) {
  const [query, setQuery] = useState("");
  // keep the input responsive; let the result list render at lower priority
  const deferredQuery = useDeferredValue(query);
 
  // normalize the data side exactly once
  const indexed = useMemo(
    () =>
      items.map((item) => ({
        item,
        key: normalizeForSearch(item.title + " " + item.tags.join(" ")),
      })),
    [items]
  );
 
  const results = useMemo(() => {
    const q = normalizeForSearch(deferredQuery);
    if (!q) return items;
    return indexed
      .filter((entry) => entry.key.includes(q))
      .map((entry) => entry.item);
  }, [indexed, deferredQuery, items]);
 
  return { query, setQuery, results };
}

useDeferredValue is a standard React 18 hook, so there is nothing to install: it prioritizes the text field's responsiveness and lets the list re-render trail slightly behind. In my experience, precomputed keys plus this hook keep client-side search over a few thousand records feeling smooth — and standing up a search server for a catalog of this size would be over-engineering, in my view.

Don't Let the Zero-Results Screen Become a Dead End

Even with normalization, some queries will return nothing. If the user is left staring at a blank area, they cannot tell an empty result from a broken app. With FlatList's ListEmptyComponent, I make sure two things are always communicated: what happened, and what to do next.

<FlatList
  data={results}
  keyExtractor={(w) => w.id}
  renderItem={({ item }) => <WallpaperCard wallpaper={item} />}
  ListEmptyComponent={
    isLoading ? null : (
      <View style={{ padding: 32, alignItems: "center" }}>
        <Text>No wallpapers matched "{query}"</Text>
        <Pressable onPress={() => setQuery("")}>
          <Text style={{ color: "#6366f1", marginTop: 12 }}>
            Clear search and show everything
          </Text>
        </Pressable>
      </View>
    )
  }
/>

One caveat: ListEmptyComponent also renders while data is still loading, so unless you gate it behind isLoading, users see a "no results" flash at startup. For diagnosing lists that render blank, see FlatList Renders Blank in Rork? A Calm, Ordered Way to Debug Empty Lists; for what to show during loading, see Skeleton Screens in Rork Apps — Practical Patterns for Better Perceived Performance.

The Spec I Paste Into Rork

Everything above can be built in from the start if you hand Rork the requirements explicitly. This is the instruction block I actually use:

Add a search feature with the following requirements:
 
1. Filter by substring match against titles and tags
2. Before comparing, normalize BOTH the query and the data
   with the same rules:
   - Unicode NFKC normalization (full-width latin → half-width,
     half-width katakana → full-width)
   - Fold latin characters to lowercase
   - Convert hiragana to katakana before comparison
   - Ignore half-width and full-width whitespace
3. Normalize the data side once at load time — do not
   re-normalize the whole list on every keystroke
4. When there are zero results, show a button that clears the query
5. Never show the zero-results state while data is loading

The difference between this and a one-line "add search" request shows up immediately in the generated code. Even with a no-code tool, translating your market's language-specific quirks into an explicit spec remains a human job — and frankly, it is one of the places where an indie developer can out-polish bigger apps.

One more thing I routinely patch in Rork-generated search fields is the TextInput configuration. For Japanese input, OS-level auto-capitalization and autocorrect tend to fight the user, so I explicitly set autoCapitalize="none", autoCorrect={false}, and returnKeyType="search" so the keyboard's search key submits the query. Generated code rarely includes these three, and they quietly change how pleasant the field feels to type in.

Since a search field means the keyboard is constantly appearing and disappearing, if your input ends up hidden behind it, Fix Keyboard Hiding Input Fields in Rork Apps: A Complete Troubleshooting Guide walks through the fixes.

Your Next Step: Hunt for One Miss in Your Own App

If your app has a search box, try two probes: query in hiragana for data stored in katakana, and query in full-width latin for data stored in half-width. If either slips through, the ten-line normalizeForSearch above is worth shipping. The cleaner your own data looks, the easier this is to postpone — but you can never clean what users will type. That was the lesson search taught me.

I hope this saves someone wrestling with the same problem a few hours.

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 →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Dev Tools2026-07-27
When to Raise Your Minimum iOS Version — Count Leftover Branches, Not User Percentages
Judging a minimum OS bump by usage share produces the same answer every year, so the decision never happens. Here is the annotation convention, the sweep script that counts how many branches each candidate floor would retire, and what to watch for 30 days after.
Dev Tools2026-07-26
What Filtering Out Prerelease OS Devices Cost Me — Separating Telemetry Tracks Instead of Dropping Them
Filtering prerelease OS devices out of telemetry left me with no warning signs on GA day. Here is the rebuild: an os_track dimension, a remote GA baseline, split alert thresholds, and a release gate that separates blockers from fixes.
Dev Tools2026-07-15
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.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →