Checkout country dropdowns look trivial until you need two hundred localized names sorted for the active locale. The ISO code is the value. The label is what humans read. Swap labels after hydration and you are not just reordering text. You are changing the DOM tree under a form that Playwright already started filling.
I had CreateAccountForm building options with client-side Intl.DisplayNames inside the component. Roughly two hundred lookups. Sorted in the browser. Fine on a fast Mac. Less fine in CI when react-hook-form has not attached yet and the register helper types firstName on faith. Labels arrived a beat later. Options re-sorted. The field emptied mid-fill. The test flaked. I flaked with it.
Phone dial codes use the same ISO value. Only the visible label should localize. Mixing "build labels in useMemo on the client" with "E2E assumes stable DOM" is how you get green locally and red in the pipeline.
The shop layer now resolves labels when locale is known, before the form renders. Message override first, then Intl.DisplayNames, then Centra's English fallback. Forms receive { code, name } with name already sorted. No post-hydration map rebuild. Playwright waits for country options, then selects by value, not visible text that might change.
Country labels stable on first paint
Idle. Type a first name, then watch what happens on label swap.
export function resolveCountryLabel(
countryCode: string,
fallbackName: string,
locale: Locale,
translateCountry?: CountryLabelTranslator
): string {
const normalizedCode = countryCode.trim().toUpperCase();
const messageKey = `countries.${normalizedCode}`;
if (translateCountry?.has(messageKey)) return translateCountry(messageKey);
try {
const localized = getCountryDisplayNames(locale).of(normalizedCode);
if (localized && isUsableIntlCountryLabel(localized, normalizedCode)) {
return localized;
}
} catch {}
return fallbackName;
}Shared E2E helpers now wait for [name="country"] option count, then selectOption({ value: 'NL' }). firstName waits on the input, not on a label sort finishing. Server-side resolution is not micro-optimization. It is contract work: stable DOM for humans, stable selectors for tests, localized copy without a second render pass.
Happy coding! Sander