Intl.PluralRules API
JavaScript에서 복수형을 올바르게 처리하는 방법
소개
복수화는 개수에 따라 다른 텍스트를 표시하는 과정입니다. 영어에서는 단일 항목에 대해 "1 item"을, 여러 항목에 대해 "2 items"를 표시할 수 있습니다. 대부분의 개발자는 1이 아닌 개수에 "s"를 추가하는 간단한 조건문으로 이를 처리합니다.
이 접근 방식은 영어 이외의 언어에서는 작동하지 않습니다. 폴란드어는 1, 2-4, 5 이상에 대해 다른 형태를 사용합니다. 아랍어는 0, 1, 2, 소수, 다수에 대한 형태가 있습니다. 웨일스어는 6개의 뚜렷한 형태가 있습니다. 심지어 영어에서도 "person"에서 "people"로 바뀌는 불규칙 복수형은 특별한 처리가 필요합니다.
Intl.PluralRules API는 모든 언어에서 모든 숫자에 대한 복수형 카테고리를 제공함으로써 이 문제를 해결합니다. 개수를 제공하면 API는 대상 언어의 규칙에 따라 사용할 형태를 알려줍니다. 이를 통해 언어별 규칙을 수동으로 인코딩하지 않고도 언어 간에 올바르게 작동하는 국제화 준비가 된 코드를 작성할 수 있습니다.
언어별 복수형 처리 방식
언어마다 수량을 표현하는 방식이 크게 다릅니다. 영어는 두 가지 형태가 있습니다: 하나에 대한 단수형, 나머지 모든 것에 대한 복수형. 이는 다른 시스템을 가진 언어를 접할 때까지는 간단해 보입니다.
러시아어와 폴란드어는 세 가지 형태를 사용합니다. 단수형은 한 항목에 적용됩니다. 특별한 형태는 2, 3, 또는 4로 끝나는 개수에 적용됩니다(단, 12, 13, 또는 14는 제외). 다른 모든 개수는 세 번째 형태를 사용합니다.
아랍어는 여섯 가지 형태를 사용합니다: 0, 1, 2, 소수(3-10), 다수(11-99), 기타(100+). 웨일스어도 다른 숫자 경계를 가진 여섯 가지 형태가 있습니다.
중국어와 일본어와 같은 일부 언어는 단수와 복수를 전혀 구분하지 않습니다. 동일한 형태가 모든 개수에 적용됩니다.
Intl.PluralRules API는 유니코드 CLDR 복수 규칙에 기반한 표준화된 카테고리 이름을 사용하여 이러한 차이를 추상화합니다. 여섯 가지 카테고리는 다음과 같습니다: zero, one, two, few, many, other. 모든 언어가 여섯 가지 카테고리를 모두 사용하는 것은 아닙니다. 영어는 one과 other만 사용합니다. 아랍어는 여섯 가지 모두를 사용합니다.
로케일에 대한 PluralRules 인스턴스 생성하기
Intl.PluralRules 생성자는 로케일 식별자를 받아 주어진 숫자에 어떤 복수 카테고리가 적용되는지 결정할 수 있는 객체를 반환합니다.
const enRules = new Intl.PluralRules('en-US');
로케일당 하나의 인스턴스를 생성하고 재사용하세요. 모든 복수화에 대해 새 인스턴스를 구성하는 것은 낭비입니다. 인스턴스를 변수에 저장하거나 캐싱 메커니즘을 사용하세요.
기본 유형은 객체 계산을 처리하는 기수(cardinal)입니다. 옵션 객체를 전달하여 서수(ordinal) 숫자에 대한 규칙도 생성할 수 있습니다.
const enOrdinalRules = new Intl.PluralRules('en-US', { type: 'ordinal' });
기수 규칙은 "1 apple, 2 apples"와 같은 개수에 적용됩니다. 서수 규칙은 "1st place, 2nd place"와 같은 위치에 적용됩니다.
select()를 사용하여 숫자에 대한 복수 카테고리 가져오기
select() 메서드는 숫자를 받아 대상 언어에서 해당 숫자가 속하는 복수 카테고리를 반환합니다.
const enRules = new Intl.PluralRules('en-US');
enRules.select(0); // 'other'
enRules.select(1); // 'one'
enRules.select(2); // 'other'
enRules.select(5); // 'other'
반환 값은 항상 여섯 가지 카테고리 이름 중 하나입니다: zero, one, two, few, many, 또는 other. 영어는 one과 other만 반환하는데, 이는 영어가 사용하는 유일한 형태이기 때문입니다.
더 복잡한 규칙을 가진 아랍어의 경우, 여섯 가지 카테고리가 모두 사용되는 것을 볼 수 있습니다:
const arRules = new Intl.PluralRules('ar-EG');
arRules.select(0); // 'zero'
arRules.select(1); // 'one'
arRules.select(2); // 'two'
arRules.select(6); // 'few'
arRules.select(18); // 'many'
arRules.select(100); // 'other'
카테고리를 지역화된 문자열에 매핑하기
API는 어떤 카테고리가 적용되는지만 알려줍니다. 각 카테고리에 대한 실제 텍스트는 사용자가 제공해야 합니다. 텍스트 형식을 카테고리 이름으로 키가 지정된 Map이나 객체에 저장하세요.
const enRules = new Intl.PluralRules('en-US');
const enForms = new Map([
['one', 'item'],
['other', 'items'],
]);
function formatItems(count) {
const category = enRules.select(count);
const form = enForms.get(category);
return `${count} ${form}`;
}
formatItems(1); // '1 item'
formatItems(5); // '5 items'
이 패턴은 로직과 데이터를 분리합니다. PluralRules 인스턴스는 규칙을 처리합니다. Map은 번역을 보관합니다. 함수는 이들을 결합합니다.
더 많은 카테고리가 있는 언어의 경우, Map에 더 많은 항목을 추가하세요:
const arRules = new Intl.PluralRules('ar-EG');
const arForms = new Map([
['zero', 'عناصر'],
['one', 'عنصر واحد'],
['two', 'عنصران'],
['few', 'عناصر'],
['many', 'عنصرًا'],
['other', 'عنصر'],
]);
function formatItems(count) {
const category = arRules.select(count);
const form = arForms.get(category);
return `${count} ${form}`;
}
언어가 사용하는 모든 카테고리에 대한 항목을 항상 제공하세요. 누락된 카테고리는 정의되지 않은 조회를 유발합니다. 언어가 어떤 카테고리를 사용하는지 확실하지 않은 경우, 유니코드 CLDR 복수 규칙을 확인하거나 다양한 숫자로 API를 테스트하세요.
소수 및 분수 개수 처리하기
select() 메서드는 소수와 함께 작동합니다. 영어에서는 0과 2 사이의 값이라도 소수를 복수형으로 취급합니다.
const enRules = new Intl.PluralRules('en-US');
enRules.select(1); // 'one'
enRules.select(1.0); // 'one'
enRules.select(1.5); // 'other'
enRules.select(0.5); // 'other'
다른 언어들은 소수에 대해 다른 규칙을 가지고 있습니다. 일부는 모든 소수를 복수형으로 취급하는 반면, 다른 언어들은 소수 부분에 기반한 더 세분화된 규칙을 사용합니다.
만약 UI에서 "1.5 GB" 또는 "2.7 miles"와 같은 분수 수량을 표시한다면, 분수 숫자를 직접 select()에 전달하세요. UI가 표시 값을 반올림하지 않는 한 먼저 반올림하지 마세요.
1st, 2nd, 3rd와 같은 서수 형식화하기
서수는 위치나 순위를 나타냅니다. 영어에서는 서수를 접미사를 추가하여 형성합니다: 1st, 2nd, 3rd, 4th. 이 패턴은 단순히 "th를 추가"하는 것이 아닙니다. 1, 2, 3은 특별한 형태를 가지며, 1, 2, 3으로 끝나는 숫자들은 특별한 규칙을 따릅니다(21st, 22nd, 23rd). 단, 11, 12, 13으로 끝나는 경우는 예외입니다(11th, 12th, 13th).
Intl.PluralRules API는 type: 'ordinal'을 지정할 때 이러한 규칙을 처리합니다.
const enOrdinalRules = new Intl.PluralRules('en-US', { type: 'ordinal' });
enOrdinalRules.select(1); // 'one'
enOrdinalRules.select(2); // 'two'
enOrdinalRules.select(3); // 'few'
enOrdinalRules.select(4); // 'other'
enOrdinalRules.select(11); // 'other'
enOrdinalRules.select(21); // 'one'
enOrdinalRules.select(22); // 'two'
enOrdinalRules.select(23); // 'few'
카테고리를 서수 접미사에 매핑합니다:
const enOrdinalRules = new Intl.PluralRules('en-US', { type: 'ordinal' });
const enOrdinalSuffixes = new Map([
['one', 'st'],
['two', 'nd'],
['few', 'rd'],
['other', 'th'],
]);
function formatOrdinal(n) {
const category = enOrdinalRules.select(n);
const suffix = enOrdinalSuffixes.get(category);
return `${n}${suffix}`;
}
formatOrdinal(1); // '1st'
formatOrdinal(2); // '2nd'
formatOrdinal(3); // '3rd'
formatOrdinal(4); // '4th'
formatOrdinal(11); // '11th'
formatOrdinal(21); // '21st'
다른 언어들은 완전히 다른 서수 시스템을 가지고 있습니다. 프랑스어는 첫 번째에 "1er"를 사용하고 다른 모든 경우에 "2e"를 사용합니다. 스페인어는 성별에 따라 다른 서수를 가집니다. API는 카테고리를 제공하고, 사용자는 지역화된 형태를 제공합니다.
selectRange()로 범위 처리하기
selectRange() 메서드는 "1-5개 항목" 또는 "10-20개 결과"와 같은 숫자 범위에 대한 복수 카테고리를 결정합니다. 일부 언어에서는 개별 숫자와 범위에 대한 복수 규칙이 다릅니다.
const enRules = new Intl.PluralRules('en-US');
enRules.selectRange(1, 5); // 'other'
enRules.selectRange(0, 1); // 'other'
영어에서는 범위가 1부터 시작하더라도 거의 항상 복수형입니다. 다른 언어에서는 더 복잡한 범위 규칙이 있습니다.
const slRules = new Intl.PluralRules('sl');
slRules.selectRange(102, 201); // 'few'
const ptRules = new Intl.PluralRules('pt');
ptRules.selectRange(102, 102); // 'other'
UI에서 범위를 명시적으로 표시할 때는 selectRange()를 사용하세요. 단일 숫자의 경우 select()를 사용하세요.
현지화된 숫자 표시를 위해 Intl.NumberFormat과 결합하기
복수형은 종종 형식이 지정된 숫자와 함께 나타납니다. Intl.NumberFormat을 사용하여 로케일 규칙에 따라 숫자 형식을 지정한 다음, Intl.PluralRules를 사용하여 올바른 텍스트를 선택하세요.
const locale = 'en-US';
const numberFormat = new Intl.NumberFormat(locale);
const pluralRules = new Intl.PluralRules(locale);
const forms = new Map([
['one', 'item'],
['other', 'items'],
]);
function formatCount(count) {
const formattedNumber = numberFormat.format(count);
const category = pluralRules.select(count);
const form = forms.get(category);
return `${formattedNumber} ${form}`;
}
formatCount(1); // '1 item'
formatCount(1000); // '1,000 items'
formatCount(1.5); // '1.5 items'
천 단위 구분 기호로 마침표를 사용하고 소수점 구분 기호로 쉼표를 사용하는 독일어의 경우:
const locale = 'de-DE';
const numberFormat = new Intl.NumberFormat(locale);
const pluralRules = new Intl.PluralRules(locale);
const forms = new Map([
['one', 'Artikel'],
['other', 'Artikel'],
]);
function formatCount(count) {
const formattedNumber = numberFormat.format(count);
const category = pluralRules.select(count);
const form = forms.get(category);
return `${formattedNumber} ${form}`;
}
formatCount(1); // '1 Artikel'
formatCount(1000); // '1.000 Artikel'
formatCount(1.5); // '1,5 Artikel'
이 패턴은 숫자 형식과 텍스트 형식이 모두 로케일에 대한 사용자 기대에 맞게 합니다.
필요할 때 명시적으로 0 케이스 처리하기
0을 복수형으로 표현하는 방식은 언어마다 다릅니다. 영어는 일반적으로 복수형을 사용합니다: "0 items", "0 results". 일부 언어는 0에 단수형을 사용하고, 다른 언어들은 0에 대한 별도의 카테고리가 있습니다.
Intl.PluralRules API는 언어 규칙에 따라 0에 대한 적절한 카테고리를 반환합니다. 영어에서 0은 'other'를 반환하며, 이는 복수형에 매핑됩니다:
const enRules = new Intl.PluralRules('en-US');
enRules.select(0); // 'other'
아랍어에서는 0이 자체 카테고리를 가집니다:
const arRules = new Intl.PluralRules('ar-EG');
arRules.select(0); // 'zero'
텍스트는 이를 고려해야 합니다. 영어의 경우, 더 나은 UX를 위해 "0 items" 대신 "No items"를 표시할 수 있습니다. 복수형 규칙을 호출하기 전에 이를 처리하세요:
function formatItems(count) {
if (count === 0) {
return 'No items';
}
const category = enRules.select(count);
const form = enForms.get(category);
return `${count} ${form}`;
}
아랍어의 경우, 번역에 특정 0 형식을 제공하세요:
const arForms = new Map([
['zero', 'لا توجد عناصر'],
['one', 'عنصر واحد'],
['two', 'عنصران'],
['few', 'عناصر'],
['many', 'عنصرًا'],
['other', 'عنصر'],
]);
이는 각 언어의 언어적 관습을 존중하면서 더 나은 사용자 경험을 위해 0 케이스를 커스터마이징할 수 있게 합니다.
성능을 위해 PluralRules 인스턴스 재사용하기
PluralRules 인스턴스를 생성하는 것은 로케일을 파싱하고 복수형 규칙 데이터를 로드하는 작업을 포함합니다. 이 작업은 모든 함수 호출이나 렌더링 주기마다 하지 말고, 로케일당 한 번만 수행하세요.
// 좋은 방법: 한 번 생성하고 재사용
const enRules = new Intl.PluralRules('en-US');
const enForms = new Map([
['one', 'item'],
['other', 'items'],
]);
function formatItems(count) {
const category = enRules.select(count);
const form = enForms.get(category);
return `${count} ${form}`;
}
여러 로케일을 지원하는 경우, 각 로케일에 대한 인스턴스를 생성하고 Map이나 캐시에 저장하세요:
const rulesCache = new Map();
function getPluralRules(locale) {
if (!rulesCache.has(locale)) {
rulesCache.set(locale, new Intl.PluralRules(locale));
}
return rulesCache.get(locale);
}
const rules = getPluralRules('en-US');
이 패턴은 여러 호출에 걸쳐 초기화 비용을 분산시킵니다.
브라우저 지원 및 호환성
Intl.PluralRules는 2019년 이후 모든 최신 브라우저에서 지원됩니다. 여기에는 Chrome 63+, Firefox 58+, Safari 13+ 및 Edge 79+가 포함됩니다. Internet Explorer에서는 지원되지 않습니다.
최신 브라우저를 대상으로 하는 애플리케이션의 경우 폴리필 없이 Intl.PluralRules를 사용할 수 있습니다. 구형 브라우저를 지원해야 하는 경우, npm의 intl-pluralrules와 같은 패키지를 통해 폴리필을 사용할 수 있습니다.
selectRange() 메서드는 더 최신이며 지원이 약간 더 제한적입니다. Chrome 106+, Firefox 116+, Safari 15.4+ 및 Edge 106+에서 사용할 수 있습니다. selectRange()를 사용하고 구형 브라우저 버전을 지원해야 하는 경우 호환성을 확인하세요.
로직에서 복수형 하드코딩 피하기
코드에서 개수를 확인하고 분기하여 복수형을 선택하지 마세요. 이 접근 방식은 두 가지 이상의 형태를 가진 언어로 확장되지 않으며 로직이 영어 규칙에 종속됩니다.
// 이 패턴은 피하세요
function formatItems(count) {
if (count === 1) {
return `${count} item`;
}
return `${count} items`;
}
Intl.PluralRules와 형태를 저장하는 데이터 구조를 사용하세요. 이렇게 하면 코드가 언어에 구애받지 않고 새 번역을 제공하여 새 언어를 쉽게 추가할 수 있습니다.
// 이 패턴을 선호하세요
const rules = new Intl.PluralRules('en-US');
const forms = new Map([
['one', 'item'],
['other', 'items'],
]);
function formatItems(count) {
const category = rules.select(count);
const form = forms.get(category);
return `${count} ${form}`;
}
이 패턴은 모든 언어에서 동일하게 작동합니다. rules 인스턴스와 forms Map만 변경됩니다.
여러 로케일 및 엣지 케이스로 테스트하기
복수형 규칙에는 영어로만 테스트할 때 놓치기 쉬운 엣지 케이스가 있습니다. 폴란드어나 아랍어와 같이 두 가지 이상의 형태를 사용하는 언어로 복수화 로직을 테스트하세요.
다양한 카테고리를 트리거하는 개수를 테스트하세요:
- 영(0)
- 하나(1)
- 둘(2)
- 몇 개(아랍어에서 3-10)
- 많음(아랍어에서 11-99)
- 큰 숫자(100+)
- 소수 값(0.5, 1.5, 2.3)
- UI에 표시되는 경우 음수
서수 규칙을 사용하는 경우, 다른 접미사를 트리거하는 숫자를 테스트하세요: 1, 2, 3, 4, 11, 21, 22, 23. 이렇게 하면 특수 케이스를 올바르게 처리할 수 있습니다.
초기에 여러 로케일로 테스트하면 나중에 새 언어를 추가할 때 예상치 못한 상황을 방지할 수 있습니다. 또한 데이터 구조에 필요한 모든 카테고리가 포함되어 있고 로직이 이를 올바르게 처리하는지 확인할 수 있습니다.