로케일에서 사용 가능한 모든 복수형 형태 가져오기
번역을 제공해야 하는 복수형 카테고리 알아보기
소개
다국어 애플리케이션을 구축할 때, 다양한 수량에 대해 서로 다른 텍스트 형식을 제공해야 합니다. 영어에서는 "1 item"과 "2 items"라고 작성합니다. 이는 다른 언어를 지원하기 시작할 때까지는 간단해 보입니다.
러시아어는 수에 따라 세 가지 다른 형태를 사용합니다. 아랍어는 여섯 가지를 사용합니다. 일부 언어는 모든 수에 대해 동일한 형태를 사용합니다. 이러한 형태에 대한 번역을 제공하기 전에, 각 언어에 어떤 형태가 존재하는지 알아야 합니다.
JavaScript는 로케일이 어떤 복수 카테고리를 사용하는지 알아내는 방법을 제공합니다. PluralRules 인스턴스의 resolvedOptions() 메서드는 해당 로케일이 필요로 하는 모든 복수 형태를 나열하는 pluralCategories 속성을 반환합니다. 이를 통해 추측하거나 언어별 규칙 테이블을 유지 관리할 필요 없이 정확히 어떤 번역을 제공해야 하는지 알 수 있습니다.
복수 카테고리란 무엇인가
복수 카테고리는 언어 전반에 걸쳐 사용되는 다양한 복수 형태에 대한 표준화된 이름입니다. 유니코드 CLDR(Common Locale Data Repository)은 zero, one, two, few, many, other의 여섯 가지 카테고리를 정의합니다.
모든 언어가 여섯 가지 카테고리를 모두 사용하는 것은 아닙니다. 영어는 one과 other 두 가지만 사용합니다. one 카테고리는 수 1에 적용되고, other는 나머지 모든 것에 적용됩니다.
아랍어는 여섯 가지 카테고리를 모두 사용합니다. zero 카테고리는 0에, one은 1에, two는 2에, few는 3-10과 같은 수에, many는 11-99와 같은 수에, other는 100 이상과 같은 수에 적용됩니다.
러시아어는 세 가지 카테고리를 사용합니다: one은 1로 끝나는 수(11 제외), few는 2-4로 끝나는 수(12-14 제외), many는 나머지 모든 것에 적용됩니다.
일본어와 중국어는 other 카테고리만 사용합니다. 이는 이러한 언어들이 단수와 복수 형태를 구분하지 않기 때문입니다.
이러한 카테고리는 각 언어의 언어학적 규칙을 나타냅니다. 번역을 제공할 때, 해당 언어가 사용하는 각 카테고리에 대해 하나의 문자열을 만듭니다.
resolvedOptions를 사용하여 복수형 카테고리 가져오기
PluralRules 인스턴스의 resolvedOptions() 메서드는 해당 로케일이 사용하는 복수형 카테고리를 포함하여 규칙에 대한 정보가 담긴 객체를 반환합니다.
const enRules = new Intl.PluralRules('en-US');
const options = enRules.resolvedOptions();
console.log(options.pluralCategories);
// 출력: ["one", "other"]
pluralCategories 속성은 문자열 배열입니다. 각 문자열은 여섯 가지 표준 카테고리 이름 중 하나입니다. 이 배열은 해당 로케일이 실제로 사용하는 카테고리만 포함합니다.
영어의 경우, 배열에는 one과 other가 포함됩니다. 이는 영어가 단수형과 복수형을 구분하기 때문입니다.
더 복잡한 규칙을 가진 언어의 경우, 배열에는 더 많은 카테고리가 포함됩니다:
const arRules = new Intl.PluralRules('ar-EG');
const options = arRules.resolvedOptions();
console.log(options.pluralCategories);
// 출력: ["zero", "one", "two", "few", "many", "other"]
아랍어는 여섯 가지 카테고리를 모두 사용하므로 배열에는 여섯 가지 값이 모두 포함됩니다.
다양한 로케일의 복수형 카테고리 확인하기
언어마다 복수형 규칙이 다르기 때문에 서로 다른 카테고리 집합을 사용합니다. 여러 언어를 비교하여 그 차이를 확인해 보세요:
const locales = ['en-US', 'ar-EG', 'ru-RU', 'pl-PL', 'ja-JP', 'zh-CN'];
locales.forEach(locale => {
const rules = new Intl.PluralRules(locale);
const categories = rules.resolvedOptions().pluralCategories;
console.log(`${locale}: [${categories.join(', ')}]`);
});
// 출력:
// en-US: [one, other]
// ar-EG: [zero, one, two, few, many, other]
// ru-RU: [one, few, many, other]
// pl-PL: [one, few, many, other]
// ja-JP: [other]
// zh-CN: [other]
영어는 두 가지 카테고리를 가집니다. 아랍어는 여섯 가지입니다. 러시아어와 폴란드어는 각각 네 가지입니다. 일본어와 중국어는 복수형을 전혀 구분하지 않기 때문에 하나만 있습니다.
이러한 차이는 모든 언어가 영어처럼 작동한다고 가정할 수 없음을 보여줍니다. 각 로케일이 어떤 카테고리를 사용하는지 확인하고 각각에 대한 적절한 번역을 제공해야 합니다.
각 로케일에 대한 카테고리의 의미 이해하기
동일한 카테고리 이름이 다른 언어에서는 다른 의미를 가집니다. 영어에서 one 카테고리는 숫자 1에만 적용됩니다. 러시아어에서는 one이 11을 제외하고 1로 끝나는 숫자들에 적용되므로 1, 21, 31, 101 등이 포함됩니다.
다양한 로케일에서 어떤 숫자가 어떤 카테고리에 매핑되는지 테스트해보세요:
const enRules = new Intl.PluralRules('en-US');
const ruRules = new Intl.PluralRules('ru-RU');
const numbers = [0, 1, 2, 3, 5, 11, 21, 22, 100];
console.log('English:');
numbers.forEach(n => {
console.log(` ${n}: ${enRules.select(n)}`);
});
console.log('Russian:');
numbers.forEach(n => {
console.log(` ${n}: ${ruRules.select(n)}`);
});
// Output:
// English:
// 0: other
// 1: one
// 2: other
// 3: other
// 5: other
// 11: other
// 21: other
// 22: other
// 100: other
// Russian:
// 0: many
// 1: one
// 2: few
// 3: few
// 5: many
// 11: many
// 21: one
// 22: few
// 100: many
영어에서는 1만 one 카테고리를 사용합니다. 러시아어에서는 1과 21 모두 1로 끝나기 때문에 one을 사용합니다. 2, 3, 22와 같은 숫자는 2-4로 끝나기 때문에 few를 사용합니다. 0, 5, 11, 100과 같은 숫자는 many를 사용합니다.
이는 언어 규칙을 알지 못하면 어떤 카테고리가 숫자에 적용되는지 예측할 수 없음을 보여줍니다. pluralCategories 배열은 어떤 카테고리가 존재하는지 알려주고, select() 메서드는 각 숫자에 어떤 카테고리가 적용되는지 알려줍니다.
서수에 대한 카테고리 가져오기
1st, 2nd, 3rd와 같은 서수는 기수와 다른 복수 규칙을 가집니다. 서수에 대한 카테고리를 얻으려면 type: 'ordinal'로 PluralRules 인스턴스를 생성하세요:
const enCardinalRules = new Intl.PluralRules('en-US', { type: 'cardinal' });
const enOrdinalRules = new Intl.PluralRules('en-US', { type: 'ordinal' });
console.log('Cardinal:', enCardinalRules.resolvedOptions().pluralCategories);
// Output: Cardinal: ["one", "other"]
console.log('Ordinal:', enOrdinalRules.resolvedOptions().pluralCategories);
// Output: Ordinal: ["one", "two", "few", "other"]
영어 기수는 두 개의 카테고리를 사용합니다. 영어 서수는 1st, 2nd, 3rd 및 기타 모든 것을 구분해야 하기 때문에 네 개의 카테고리를 사용합니다.
서수 카테고리는 서수 접미사에 매핑됩니다:
const enOrdinalRules = new Intl.PluralRules('en-US', { type: 'ordinal' });
const numbers = [1, 2, 3, 4, 11, 21, 22, 23];
numbers.forEach(n => {
const category = enOrdinalRules.select(n);
console.log(`${n}: ${category}`);
});
// Output:
// 1: one
// 2: two
// 3: few
// 4: other
// 11: other
// 21: one
// 22: two
// 23: few
one 카테고리는 st 접미사(1st, 21st)에 해당하고, two는 nd(2nd, 22nd), few는 rd(3rd, 23rd), other는 th(4th, 11th)에 해당합니다.
다른 언어들은 다른 서수 카테고리를 가집니다:
const locales = ['en-US', 'es-ES', 'fr-FR'];
locales.forEach(locale => {
const rules = new Intl.PluralRules(locale, { type: 'ordinal' });
const categories = rules.resolvedOptions().pluralCategories;
console.log(`${locale}: [${categories.join(', ')}]`);
});
// Output:
// en-US: [one, two, few, other]
// es-ES: [other]
// fr-FR: [one, other]
스페인어는 서수가 더 단순한 패턴을 따르기 때문에 하나의 서수 카테고리만 사용합니다. 프랑스어는 첫 번째 위치와 다른 모든 위치를 구분하기 위해 두 개의 카테고리를 사용합니다.
복수 카테고리를 사용하여 번역 맵 구축하기
로케일에서 사용하는 카테고리를 알고 있다면, 정확히 필요한 수의 항목으로 번역 맵을 구축할 수 있습니다:
function buildTranslationMap(locale, translations) {
const rules = new Intl.PluralRules(locale);
const categories = rules.resolvedOptions().pluralCategories;
const map = new Map();
categories.forEach(category => {
if (translations[category]) {
map.set(category, translations[category]);
} else {
console.warn(`Missing translation for category "${category}" in locale "${locale}"`);
}
});
return map;
}
const enTranslations = {
one: 'item',
other: 'items'
};
const arTranslations = {
zero: 'لا توجد عناصر',
one: 'عنصر واحد',
two: 'عنصران',
few: 'عناصر',
many: 'عنصرًا',
other: 'عنصر'
};
const enMap = buildTranslationMap('en-US', enTranslations);
const arMap = buildTranslationMap('ar-EG', arTranslations);
console.log(enMap);
// 출력: Map(2) { 'one' => 'item', 'other' => 'items' }
console.log(arMap);
// 출력: Map(6) { 'zero' => 'لا توجد عناصر', 'one' => 'عنصر واحد', ... }
이 함수는 필요한 모든 카테고리에 대한 번역을 제공했는지 확인하고 누락된 항목이 있으면 경고합니다. 이를 통해 카테고리가 사용되지만 번역이 없는 경우 런타임 오류를 방지할 수 있습니다.
번역 완전성 검증하기
복수 카테고리를 사용하여 프로덕션에 배포하기 전에 번역에 필요한 모든 형태가 포함되어 있는지 확인하세요:
function validateTranslations(locale, translations) {
const rules = new Intl.PluralRules(locale);
const requiredCategories = rules.resolvedOptions().pluralCategories;
const providedCategories = Object.keys(translations);
const missing = requiredCategories.filter(cat => !providedCategories.includes(cat));
const extra = providedCategories.filter(cat => !requiredCategories.includes(cat));
if (missing.length > 0) {
console.error(`Locale ${locale} is missing categories: ${missing.join(', ')}`);
return false;
}
if (extra.length > 0) {
console.warn(`Locale ${locale} has unused categories: ${extra.join(', ')}`);
}
return true;
}
const enTranslations = {
one: 'item',
other: 'items'
};
const incompleteArTranslations = {
one: 'عنصر واحد',
other: 'عنصر'
};
validateTranslations('en-US', enTranslations);
// 출력: true
validateTranslations('ar-EG', incompleteArTranslations);
// 출력: Locale ar-EG is missing categories: zero, two, few, many
// 출력: false
이 검증은 사용자가 번역되지 않은 텍스트를 발견하는 대신 개발 중에 누락된 번역을 포착합니다.
동적 번역 인터페이스 구축하기
번역가를 위한 도구를 구축할 때, 복수형 카테고리를 쿼리하여 정확히 어떤 형태가 번역이 필요한지 보여줍니다:
function generateTranslationForm(locale, key) {
const rules = new Intl.PluralRules(locale);
const categories = rules.resolvedOptions().pluralCategories;
const form = document.createElement('div');
form.className = 'translation-form';
const heading = document.createElement('h3');
heading.textContent = `Translate "${key}" for ${locale}`;
form.appendChild(heading);
categories.forEach(category => {
const label = document.createElement('label');
label.textContent = `${category}:`;
const input = document.createElement('input');
input.type = 'text';
input.name = `${key}.${category}`;
input.placeholder = `Enter ${category} form`;
const wrapper = document.createElement('div');
wrapper.appendChild(label);
wrapper.appendChild(input);
form.appendChild(wrapper);
});
return form;
}
const enForm = generateTranslationForm('en-US', 'items');
const arForm = generateTranslationForm('ar-EG', 'items');
document.body.appendChild(enForm);
document.body.appendChild(arForm);
이는 각 로케일에 맞는 정확한 수의 입력 필드가 있는 폼을 생성합니다. 영어는 두 개의 필드(one과 other)를 얻고, 아랍어는 여섯 개의 필드(zero, one, two, few, many, other)를 얻습니다.
로케일 간 카테고리 비교하기
여러 로케일에 대한 번역을 관리할 때, 어떤 카테고리를 사용하는지 비교하여 번역 복잡성을 이해합니다:
function compareLocalePluralCategories(locales) {
const comparison = {};
locales.forEach(locale => {
const rules = new Intl.PluralRules(locale);
const categories = rules.resolvedOptions().pluralCategories;
comparison[locale] = categories;
});
return comparison;
}
const locales = ['en-US', 'es-ES', 'ar-EG', 'ru-RU', 'ja-JP'];
const comparison = compareLocalePluralCategories(locales);
console.log(comparison);
// Output:
// {
// 'en-US': ['one', 'other'],
// 'es-ES': ['one', 'other'],
// 'ar-EG': ['zero', 'one', 'two', 'few', 'many', 'other'],
// 'ru-RU': ['one', 'few', 'many', 'other'],
// 'ja-JP': ['other']
// }
이는 영어와 스페인어가 동일한 복수형 카테고리를 가지고 있어 이들 간에 번역 구조를 쉽게 재사용할 수 있음을 보여줍니다. 아랍어는 여섯 개의 카테고리를 사용하기 때문에 상당히 더 많은 번역 작업이 필요합니다.
로케일이 특정 카테고리를 사용하는지 확인하기
코드에서 특정 복수형 카테고리를 사용하기 전에, 해당 로케일이 실제로 그것을 사용하는지 확인하세요:
function localeUsesCategory(locale, category) {
const rules = new Intl.PluralRules(locale);
const categories = rules.resolvedOptions().pluralCategories;
return categories.includes(category);
}
console.log(localeUsesCategory('en-US', 'zero'));
// 출력: false
console.log(localeUsesCategory('ar-EG', 'zero'));
// 출력: true
console.log(localeUsesCategory('ja-JP', 'one'));
// 출력: false
이는 모든 로케일이 zero 카테고리나 one 카테고리를 가지고 있다고 가정하는 것을 방지합니다. 이 확인을 통해 카테고리별 로직을 안전하게 구현할 수 있습니다.
other 카테고리 이해하기
모든 언어는 other 카테고리를 사용합니다. 이 카테고리는 다른 카테고리가 적용되지 않을 때 기본 케이스로 작동합니다.
영어에서는 other가 1을 제외한 모든 수를 포함합니다. 아랍어에서는 other가 100 이상과 같은 큰 숫자를 포함합니다. 일본어에서는 일본어가 복수형을 구분하지 않기 때문에 other가 모든 수를 포함합니다.
항상 other 카테고리에 대한 번역을 제공하세요. 이 카테고리는 모든 로케일에 존재하는 것이 보장되며, 더 구체적인 카테고리가 일치하지 않을 때 사용됩니다.
const locales = ['en-US', 'ar-EG', 'ru-RU', 'ja-JP'];
locales.forEach(locale => {
const rules = new Intl.PluralRules(locale);
const categories = rules.resolvedOptions().pluralCategories;
const hasOther = categories.includes('other');
console.log(`${locale} uses "other": ${hasOther}`);
});
// 출력:
// en-US uses "other": true
// ar-EG uses "other": true
// ru-RU uses "other": true
// ja-JP uses "other": true
모든 해결된 옵션 함께 가져오기
resolvedOptions() 메서드는 복수형 카테고리뿐만 아니라 로케일, 유형 및 숫자 서식 지정 옵션에 대한 정보도 반환합니다:
const rules = new Intl.PluralRules('de-DE', {
type: 'cardinal',
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
const options = rules.resolvedOptions();
console.log(options);
// 출력:
// {
// locale: 'de-DE',
// type: 'cardinal',
// pluralCategories: ['one', 'other'],
// minimumIntegerDigits: 1,
// minimumFractionDigits: 2,
// maximumFractionDigits: 2,
// minimumSignificantDigits: undefined,
// maximumSignificantDigits: undefined
// }
pluralCategories 속성은 해결된 옵션 객체의 한 부분입니다. 다른 속성들은 PluralRules 인스턴스가 사용하는 정확한 구성을 알려주며, 기본값으로 설정된 옵션들도 포함합니다.
성능을 위한 복수형 카테고리 캐싱
PluralRules 인스턴스를 생성하고 resolvedOptions()를 호출하는 것은 비용이 발생합니다. 반복적으로 쿼리하는 대신 각 로케일에 대한 결과를 캐시하세요:
const categoriesCache = new Map();
function getPluralCategories(locale, type = 'cardinal') {
const key = `${locale}:${type}`;
if (categoriesCache.has(key)) {
return categoriesCache.get(key);
}
const rules = new Intl.PluralRules(locale, { type });
const categories = rules.resolvedOptions().pluralCategories;
categoriesCache.set(key, categories);
return categories;
}
const enCardinal = getPluralCategories('en-US', 'cardinal');
const enOrdinal = getPluralCategories('en-US', 'ordinal');
const arCardinal = getPluralCategories('ar-EG', 'cardinal');
console.log('en-US cardinal:', enCardinal);
console.log('en-US ordinal:', enOrdinal);
console.log('ar-EG cardinal:', arCardinal);
// 이후 호출은 캐시된 결과 사용
const enCardinal2 = getPluralCategories('en-US', 'cardinal');
// 새로운 PluralRules 인스턴스가 생성되지 않음
이 패턴은 많은 복수형 문자열을 포맷하거나 많은 로케일을 지원하는 애플리케이션에서 특히 중요합니다.
브라우저 지원 및 호환성
resolvedOptions()의 pluralCategories 속성은 2020년에 JavaScript에 추가되었습니다. Chrome 106+, Firefox 116+, Safari 15.4+, Edge 106+에서 지원됩니다.
Intl.PluralRules는 지원하지만 pluralCategories는 지원하지 않는 구형 브라우저에서는 이 속성에 대해 undefined를 반환합니다. 사용하기 전에 존재 여부를 확인하세요:
function getPluralCategories(locale) {
const rules = new Intl.PluralRules(locale);
const options = rules.resolvedOptions();
if (options.pluralCategories) {
return options.pluralCategories;
}
// 구형 브라우저를 위한 대체 방안
return ['one', 'other'];
}
이 대체 방안은 영어와 많은 유럽 언어에서 작동하는 간단한 두 카테고리 시스템을 가정하지만, 더 복잡한 규칙을 가진 언어에는 정확하지 않을 수 있습니다. 더 나은 호환성을 위해 언어별 대체 방안을 제공하거나 폴리필을 사용하세요.