로케일에서 사용 가능한 모든 복수형 가져오기
번역을 제공해야 하는 복수형 카테고리 알아보기
소개
다국어 애플리케이션을 구축할 때는 수량에 따라 다른 텍스트 형태를 제공해야 합니다. 영어에서는 "1 item"과 "2 items"로 작성합니다. 다른 언어를 지원하기 시작하면 이것이 단순해 보이지 않습니다.
러시아어는 개수에 따라 세 가지 다른 형태를 사용합니다. 아랍어는 여섯 가지를 사용합니다. 일부 언어는 모든 개수에 대해 동일한 형태를 사용합니다. 이러한 형태에 대한 번역을 제공하기 전에 각 언어에 어떤 형태가 존재하는지 알아야 합니다.
JavaScript는 로케일이 사용하는 복수형 범주를 확인하는 방법을 제공합니다. resolvedOptions() 인스턴스의 PluralRules 메서드는 로케일에 필요한 모든 복수형을 나열하는 pluralCategories 속성을 반환합니다. 이를 통해 추측하거나 언어별 규칙 테이블을 유지 관리할 필요 없이 제공해야 할 번역을 정확히 알 수 있습니다.
복수형 카테고리란
복수형 카테고리는 언어 전반에 걸쳐 사용되는 다양한 복수형에 대한 표준화된 이름입니다. Unicode 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 이상의 개수에 적용됩니다.
러시아어는 세 가지 카테고리를 사용합니다: 1로 끝나는 개수(11 제외)에는 one, 2-4로 끝나는 개수(12-14 제외)에는 few, 그 외 모든 것에는 many를 사용합니다.
일본어와 중국어는 단수형과 복수형을 구분하지 않기 때문에 other 범주만 사용합니다.
이러한 범주는 각 언어의 언어학적 규칙을 나타냅니다. 번역을 제공할 때는 해당 언어가 사용하는 각 범주에 대해 하나의 문자열을 생성합니다.
resolvedOptions로 복수 범주 가져오기
PluralRules 인스턴스의 resolvedOptions() 메서드는 로케일이 사용하는 복수형 범주를 포함한 규칙에 대한 정보가 담긴 객체를 반환합니다.
const enRules = new Intl.PluralRules('en-US');
const options = enRules.resolvedOptions();
console.log(options.pluralCategories);
// Output: ["one", "other"]
pluralCategories 속성은 문자열 배열입니다. 각 문자열은 6개의 표준 범주 이름 중 하나입니다. 배열에는 로케일이 실제로 사용하는 범주만 포함됩니다.
영어의 경우 단수형과 복수형을 구분하기 때문에 배열에 one과 other가 포함됩니다.
더 복잡한 규칙을 가진 언어의 경우 배열에 더 많은 범주가 포함됩니다.
const arRules = new Intl.PluralRules('ar-EG');
const options = arRules.resolvedOptions();
console.log(options.pluralCategories);
// Output: ["zero", "one", "two", "few", "many", "other"]
아랍어는 6개의 범주를 모두 사용하므로 배열에 6개의 값이 모두 포함됩니다.
다양한 로케일의 복수 범주 확인하기
언어마다 복수 규칙이 다르므로 서로 다른 범주 집합을 사용합니다. 여러 언어를 비교하여 차이를 확인해 보세요.
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(', ')}]`);
});
// Output:
// 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]
영어는 2개의 범주를 가지고 있습니다. 아랍어는 6개입니다. 러시아어와 폴란드어는 각각 4개씩 가지고 있습니다. 일본어와 중국어는 복수형을 전혀 구분하지 않기 때문에 1개만 가지고 있습니다.
이러한 차이는 모든 언어가 영어처럼 작동한다고 가정할 수 없는 이유를 보여줍니다. 각 로케일이 사용하는 범주를 확인하고 각각에 대해 적절한 번역을 제공해야 합니다.
각 로케일에서 범주가 의미하는 바 이해하기
동일한 범주 이름이 언어마다 다른 의미를 가집니다. 영어의 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);
// Output: Map(2) { 'one' => 'item', 'other' => 'items' }
console.log(arMap);
// Output: 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);
// Output: true
validateTranslations('ar-EG', incompleteArTranslations);
// Output: Locale ar-EG is missing categories: zero, two, few, many
// Output: 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'));
// Output: false
console.log(localeUsesCategory('ar-EG', 'zero'));
// Output: true
console.log(localeUsesCategory('ja-JP', 'one'));
// Output: 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}`);
});
// Output:
// 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);
// Output:
// {
// 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);
// Subsequent calls use cached results
const enCardinal2 = getPluralCategories('en-US', 'cardinal');
// No new PluralRules instance created
이 패턴은 많은 복수형 문자열을 형식화하거나 많은 로케일을 지원하는 애플리케이션에서 특히 중요합니다.
브라우저 지원 및 호환성
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;
}
// Fallback for older browsers
return ['one', 'other'];
}
이 폴백은 간단한 2개 카테고리 시스템을 가정하며, 영어와 많은 유럽 언어에서는 작동하지만 더 복잡한 규칙을 가진 언어에는 올바르지 않을 수 있습니다. 더 나은 호환성을 위해 언어별 폴백을 제공하거나 폴리필을 사용하세요.