跳到正文
DEV Community· Wearton·· 5 小時前AI 評分49

Wearton 如何在支援 15 種語言的 React Native app 中處理複數形式:「1 item, 2 items, 5 items」

"1 item, 2 items, 5 items": plural forms in a React Native app with 15 languages

AI 導讀

Wearton 的 React Native app 以 TypeScript 型別字典處理 15 種語言的複數形式,並且不使用 i18n library。英文詞典提供必需鍵值,各語言可選擇加入 few、many,缺少相應詞形時回退至 other。文章建議用 Intl.PluralRules 對照自訂規則,逐一測試 0 至 200 的數字。

正文

Our closet screen shows a simple line: how many pieces of clothing you own. In English this needs two strings, "1 item" and "5 items". When we added Polish, Russian, Ukrainian, Czech and Slovak, two strings were not enough any more.

This post shows how we handle plural forms in Wearton, a React Native app with 15 languages, with no i18n library and with TypeScript checking every dictionary.

The problem: "one" and "other" are not enough

The Unicode CLDR project gives every language a set of plural categories: one, few, many, other (and zero, two for some languages). English uses only one and other. Other languages use more:

Language 1 2 5 12 22
English one other other other other
Polish one few many many few
Russian, Ukrainian one few many many few
Czech, Slovak one few other other other
French, Portuguese one (also 0) other other other other
Chinese, Japanese other other other other other

Two details catch people:

  1. In Polish and Russian, 12, 13 and 14 are many, but 22, 23 and 24 are few again. The rule looks at the last two digits, not only the last one.
  2. In French and Portuguese, 0 is one: "0 article", not "0 articles".

Here is the same closet line in Russian and Czech:

Count Russian Czech
1 1 вещь 1 kousek
3 3 вещи 3 kousky
5 5 вещей 5 kousků

Step 1: a typed dictionary per language

English is the source of truth. Its dictionary is a plain object with as const, and the type of every key comes from it:

export const en = {
  'closet.count.one': '{count} item',
  'closet.count.other': '{count} items',
  // ...
} as const;

export type StringKey = keyof typeof en;

Every other language must have every English key:

type Dictionary = Record<StringKey, string>;

If a translator forgets a key, tsc fails. We find a missing string at build time, not on a user's screen.

Step 2: extra plural forms without changing English

Polish needs few and many, but English has no such strings. We did not want empty few keys in the English file. A template literal type lets a dictionary add them as optional keys:

type ExtraPluralKey = `${string}.few` | `${string}.many`;

type Dictionary = Record<StringKey, string> & {
  [key: ExtraPluralKey]: string;
};

export const ru: Dictionary = {
  'closet.count.one': '{count} вещь',
  'closet.count.other': '{count} вещей',
  'closet.count.few': '{count} вещи',
  'closet.count.many': '{count} вещей',
  // ...
};

English stays with two keys. Russian adds two more. TypeScript still checks that the required keys exist.

Step 3: pick the category, then the string

The function that formats a count does two things:

  1. It finds the plural category of the number for the current language.
  2. It looks for <base>.few or <base>.many in the dictionary. If that key is missing, it falls back to <base>.other.
function formatCount(lang: Lang, count: number, base: string): string {
  const form = pluralCategory(lang, count); // 'one' | 'few' | 'many' | 'other'
  const dict = dictionaries[lang];
  const key =
    form === 'one' ? `${base}.one`
    : dict[`${base}.${form}`] ? `${base}.${form}`
    : `${base}.other`;
  return dict[key].replace('{count}', String(count));
}

The fallback matters. A new screen can ship with only one and other in every language, and the Polish translator adds few and many later. Nothing breaks in between: the user sees the other form, which is often close enough.

Step 4: test the rules against Intl.PluralRules

We wrote the category rules for our 15 languages by hand. You can also use Intl.PluralRules, which is built into modern JavaScript engines:

new Intl.PluralRules('pl').select(22); // 'few'
new Intl.PluralRules('pl').select(12); // 'many'
new Intl.PluralRules('fr').select(0);  // 'one'
new Intl.PluralRules('cs').select(1.5); // 'many'

Even if you write your own rules, Intl.PluralRules is a good test oracle. Loop over 0 to 200 for each language and compare your function with it. This catches the 12-14 and 22-24 cases at once.

The last line shows one more trap: Czech has a many form, but only for decimals. For whole numbers in a closet count, one, few and other are enough.

What we learned

  1. Start from CLDR categories, not from "singular and plural".
  2. Make English the typed source of keys, so a missing translation fails the build.
  3. Let languages add few and many as optional keys, with a fallback to other.
  4. Test the numbers 0, 1, 2, 5, 11, 12, 21, 22 and 112 in every language.

About Wearton

Wearton is a personal stylist app. It finds your colour season from one selfie, keeps your closet and suggests what to wear for the weather and your plans. It runs on iPhone, Android, the web app, Chrome, Edge, Firefox, Safari, ChatGPT and Claude, in English, German, Spanish, French, Italian, Polish, Portuguese, Russian, Turkish, Ukrainian, Chinese, Japanese, Hungarian, Slovak and Czech.

Try it at wearton.app.

來源:DEV Community · dev.to