I check a generated screen on an iPhone, nod, and close it. Android can wait — that's the order I fall into more often than I'd like.
Then "later" arrives, and on an Android device the headings look oddly thin. The code clearly says fontWeight: "700". It was bold on the iPhone. Sometimes the Latin letters are bold while the Japanese characters next to them stay light.
This note is for anyone standing in front of that screen. I'll go in this order: tell the symptoms apart, trace the cause, fix it, then verify.
First, sort the symptom into one of three
"It won't go bold" can look slightly different each time, and each look points somewhere else.
| What you see | Where to look |
|---|---|
| Bold on iPhone, everything thin on Android | The combination of fontFamily and fontWeight |
| Latin letters are bold, Japanese stays thin | No Japanese-capable font loaded (fallback in play) |
| Bold gives a clearly different-looking font | Per-weight font names mixed up |
The first is by far the most common, and it's the center of this note. The other two usually surface during the trace below.
Trace the cause in this order
Each step is short. Do them top to bottom.
1. Find which fontFamily the bold Text is actually using.
Search the generated code for every place fontWeight appears.
grep -rn "fontWeight" app components | head -30Look near each hit for a fontFamily. The typical culprit is a line that still points at the Regular name, such as fontFamily: "NotoSansJP_400Regular", with fontWeight: "700" simply added on top.
2. Check whether only Regular is loaded.
Open the list you pass to useFonts. If Bold isn't in it, Android has nothing to use as the bold face in the first place.
3. Check the spelling of the name.
The key in useFonts and the string in fontFamily must match exactly. When they don't — an extension, a hyphen versus an underscore — you often get no error, just the system font, which is why it's easy to miss. The loading basics are covered in loading custom fonts in a Rork app without flicker.
Most cases get a name by the end of these three steps.
The idea: give each weight its own name
In short: iPhone often picks a close weight from a family plus a weight value, whereas Android leans toward looking the face up by family name, so adding fontWeight to a Regular face doesn't always switch you to the Bold face. If you want the same result on both platforms, load each weight under its own name and choose the weight with fontFamily, not fontWeight.
That way you stop depending on how each OS interprets the weight.
The fix: let the name carry the weight
Here's a setup that loads Noto Sans JP from its Google Fonts package.
npx expo install expo-font @expo-google-fonts/noto-sans-jpWhat this solves: it loads Regular, Medium, and Bold under separate names, so a screen only has to pass a meaningful weight.
// components/AppText.tsx
import { Text as RNText, TextProps } from "react-native";
type Weight = "regular" | "medium" | "bold";
const FAMILY: Record<Weight, string> = {
regular: "NotoSansJP_400Regular",
medium: "NotoSansJP_500Medium",
bold: "NotoSansJP_700Bold",
};
type Props = TextProps & { weight?: Weight };
export function AppText({ weight = "regular", style, ...rest }: Props) {
return (
<RNText
{...rest}
style={[
{ fontFamily: FAMILY[weight] },
style,
// The weight is chosen by fontFamily; reset it here to avoid double-specifying
{ fontWeight: "normal" },
]}
/>
);
}// app/_layout.tsx (loading side)
import { useFonts } from "expo-font";
import {
NotoSansJP_400Regular,
NotoSansJP_500Medium,
NotoSansJP_700Bold,
} from "@expo-google-fonts/noto-sans-jp";
import * as SplashScreen from "expo-splash-screen";
import { useEffect } from "react";
import { Stack } from "expo-router";
SplashScreen.preventAutoHideAsync();
export default function RootLayout() {
const [loaded, error] = useFonts({
NotoSansJP_400Regular,
NotoSansJP_500Medium,
NotoSansJP_700Bold,
});
useEffect(() => {
if (loaded || error) SplashScreen.hideAsync();
}, [loaded, error]);
if (!loaded && !error) return null;
return <Stack />;
}Why it's written this way. Resetting fontWeight at the end of AppText means that if someone — or an AI — later adds fontWeight: "700", it can't drift back into "Regular face plus a weight value." With one entry point for weight, there's only one place to suspect when you reread generated code.
Verify with a three-line check screen
Before touching real screens, line the three weights up on a scratch screen. Mixing Japanese, Latin letters, and digits lets you catch the second symptom (Japanese stays thin) at the same time.
// app/font-check.tsx
import { View } from "react-native";
import { AppText } from "../components/AppText";
export default function FontCheck() {
return (
<View style={{ padding: 24, gap: 12 }}>
<AppText weight="regular" style={{ fontSize: 20 }}>標準 Regular 日本語ABC123</AppText>
<AppText weight="medium" style={{ fontSize: 20 }}>中太 Medium 日本語ABC123</AppText>
<AppText weight="bold" style={{ fontSize: 20 }}>太字 Bold 日本語ABC123</AppText>
</View>
);
}There's one thing to look at: on both iPhone and Android, do the three lines step up in weight? If the Japanese characters get heavier in step with the Latin ones, you're done.
Pitfalls that tend to remain
- File size. Each extra weight of a Japanese font adds to what ships in the app. If Regular and Bold are enough, adding Medium later is a perfectly good decision.
- How to ask the AI. When you ask Rork to fix this, spelling it out helps: "Don't use
fontWeight; specify a separate fontFamily per weight. Only Regular and Bold are needed." - If it still isn't fixed. Log the
errorreturned byuseFontsonce, to rule out a failed load. A name mismatch just quietly shows another font on screen, so the log tells you faster.
Your next step
In your own project, run grep -rn "fontWeight" app components once and count the hits. That number is your estimate of how much there is to fix.
Let the name decide the weight instead of leaving it to each OS's guess. As an indie developer shipping to both stores, that's a line I intend to keep holding.