1-3개 항목과 같은 범위에 대한 복수형 선택 방법

JavaScript를 사용하여 숫자 범위를 표시할 때 올바른 복수형 선택하기

소개

범위는 값이 두 끝점 사이에 있음을 나타냅니다. 사용자 인터페이스는 "10-15개 일치 항목 발견"과 같은 검색 결과, "1-3개 항목 사용 가능"과 같은 재고 시스템, 또는 "2-5개 옵션 선택"과 같은 필터 등의 컨텍스트에서 범위를 표시합니다. 이러한 범위는 두 개의 숫자와 범위와 문법적으로 일치해야 하는 설명 텍스트를 결합합니다.

단일 개수를 표시할 때는 단수형과 복수형 중에서 선택합니다: "1개 항목" 대 "2개 항목". 언어에는 개수에 따라 어떤 형태를 적용할지 결정하는 규칙이 있습니다. 이러한 규칙은 언어마다 다릅니다. 영어는 1에 대해 단수형을 사용하고 다른 모든 개수에 대해 복수형을 사용합니다. 폴란드어는 1, 2-4, 5 이상에 대해 서로 다른 형태를 사용합니다. 아랍어는 개수에 따라 6가지 고유한 형태를 가지고 있습니다.

범위는 다른 과제를 제시합니다. 복수형은 단일 숫자가 아닌 시작 값과 끝 값 모두에 따라 달라집니다. 영어에서 "1-2 items"는 범위가 1에서 시작하더라도 복수형을 사용합니다. 언어마다 범위에 적용되는 복수형을 결정하는 규칙이 다릅니다. Intl.PluralRulesselectRange() 메서드는 이러한 언어별 규칙을 자동으로 처리합니다.

범위에 다른 복수형 규칙이 필요한 이유

범위의 단일 숫자에 select() 메서드를 사용하는 것은 모든 언어에서 올바르게 작동하지 않습니다. 범위의 끝 값을 사용하려고 생각할 수 있지만, 이는 많은 언어에서 잘못된 결과를 생성합니다.

0-1 범위의 영어를 고려해 보세요. 끝 값에 select()를 사용하면 "one"을 반환하여 "0-1 item"을 표시해야 한다고 제안합니다. 이는 문법적으로 올바르지 않습니다. 올바른 형태는 복수형인 "0-1 items"입니다.

const rules = new Intl.PluralRules("en-US");

console.log(rules.select(1));
// Output: "one"

// But "0-1 item" is incorrect
// Correct: "0-1 items"

언어마다 범위에 대한 명시적인 규칙이 있으며, 이는 단일 숫자에 대한 규칙과 다릅니다. 슬로베니아어에서 102-201 범위는 "few" 형태를 사용하지만, 해당 범위 내의 개별 숫자는 다른 형태를 사용합니다.

const slRules = new Intl.PluralRules("sl");

console.log(slRules.select(102));
// Output: "few"

console.log(slRules.select(201));
// Output: "few"

console.log(slRules.selectRange(102, 201));
// Output: "few"

일부 언어는 시작 값을 사용하여 형태를 결정하고, 다른 언어는 끝 값을 사용하며, 또 다른 언어는 두 값을 함께 사용합니다. selectRange() 메서드는 이러한 언어별 규칙을 캡슐화하므로 수동으로 구현할 필요가 없습니다.

범위에 대한 PluralRules 인스턴스 생성

단일 개수에 대해 수행하는 것과 동일한 방식으로 Intl.PluralRules 인스턴스를 생성합니다. 인스턴스는 단일 숫자에 대한 select()와 범위에 대한 selectRange()를 모두 제공합니다.

const rules = new Intl.PluralRules("en-US");

인스턴스를 생성할 때 옵션을 지정할 수 있습니다. 이러한 옵션은 단일 숫자와 범위 모두에 적용됩니다.

const rules = new Intl.PluralRules("en-US", {
  type: "cardinal"
});

type 옵션은 기본적으로 객체를 세는 "cardinal"로 설정됩니다. 서수 범위가 사용자 인터페이스에서 덜 일반적이지만 위치 번호에 대해 "ordinal"을 사용할 수도 있습니다.

여러 호출에서 동일한 인스턴스를 재사용하세요. 복수화할 때마다 새 인스턴스를 생성하는 것은 낭비입니다. 인스턴스를 변수에 저장하거나 로케일별로 캐시하세요.

selectRange를 사용하여 범위의 복수 범주 결정

selectRange() 메서드는 범위의 시작과 끝을 나타내는 두 개의 숫자를 받습니다. 적용되는 복수 범주를 나타내는 문자열을 반환합니다: "zero", "one", "two", "few", "many" 또는 "other".

const rules = new Intl.PluralRules("en-US");

console.log(rules.selectRange(0, 1));
// Output: "other"

console.log(rules.selectRange(1, 2));
// Output: "other"

console.log(rules.selectRange(5, 10));
// Output: "other"

영어에서 범위는 거의 항상 복수형에 해당하는 "other" 범주를 사용합니다. 이는 영어 화자가 복수 명사로 범위를 자연스럽게 표현하는 방식과 일치합니다.

더 많은 복수 형태를 가진 언어는 특정 규칙에 따라 다른 범주를 반환합니다.

const arRules = new Intl.PluralRules("ar-EG");

console.log(arRules.selectRange(0, 0));
// Output: "zero"

console.log(arRules.selectRange(1, 1));
// Output: "one"

console.log(arRules.selectRange(2, 2));
// Output: "two"

console.log(arRules.selectRange(3, 10));
// Output: "few"

반환 값은 항상 6가지 표준 복수 범주 이름 중 하나입니다. 코드는 이러한 범주를 적절한 현지화된 텍스트에 매핑합니다.

범위 범주를 현지화된 문자열에 매핑

각 복수 범주에 대한 텍스트 형식을 데이터 구조에 저장하세요. selectRange()가 반환한 범주를 사용하여 적절한 텍스트를 조회합니다.

const rules = new Intl.PluralRules("en-US");
const forms = new Map([
  ["one", "item"],
  ["other", "items"]
]);

function formatRange(start, end) {
  const category = rules.selectRange(start, end);
  const form = forms.get(category);
  return `${start}-${end} ${form}`;
}

console.log(formatRange(1, 3));
// Output: "1-3 items"

console.log(formatRange(0, 1));
// Output: "0-1 items"

console.log(formatRange(5, 10));
// Output: "5-10 items"

이 패턴은 복수화 로직을 현지화된 텍스트와 분리합니다. Intl.PluralRules 인스턴스는 언어 규칙을 처리합니다. Map은 번역을 보유합니다. 함수는 이들을 결합합니다.

더 많은 복수 범주를 가진 언어의 경우 해당 언어가 사용하는 각 범주에 대한 항목을 추가합니다.

const arRules = new Intl.PluralRules("ar-EG");
const arForms = new Map([
  ["zero", "عناصر"],
  ["one", "عنصر"],
  ["two", "عنصران"],
  ["few", "عناصر"],
  ["many", "عنصرًا"],
  ["other", "عنصر"]
]);

function formatRange(start, end) {
  const category = arRules.selectRange(start, end);
  const form = arForms.get(category);
  return `${start}-${end} ${form}`;
}

console.log(formatRange(0, 0));
// Output: "0-0 عناصر"

console.log(formatRange(1, 1));
// Output: "1-1 عنصر"

항상 언어가 사용하는 모든 범주에 대한 텍스트를 제공합니다. Unicode CLDR 복수 규칙을 확인하거나 API를 사용하여 다양한 범위에서 테스트하여 필요한 범주를 식별합니다.

다양한 로케일이 범위 복수화를 처리하는 방법

각 언어는 범위의 복수 형태를 결정하는 고유한 규칙을 가지고 있습니다. 이러한 규칙은 원어민이 해당 언어로 범위를 자연스럽게 표현하는 방식을 반영합니다.

const enRules = new Intl.PluralRules("en-US");
console.log(enRules.selectRange(1, 3));
// Output: "other"

const slRules = new Intl.PluralRules("sl");
console.log(slRules.selectRange(102, 201));
// Output: "few"

const ptRules = new Intl.PluralRules("pt");
console.log(ptRules.selectRange(102, 102));
// Output: "other"

const ruRules = new Intl.PluralRules("ru");
console.log(ruRules.selectRange(1, 2));
// Output: "few"

영어는 범위에 대해 일관되게 "other"를 사용하여 범위를 항상 복수로 만듭니다. 슬로베니아어는 범위의 특정 숫자를 기반으로 더 복잡한 규칙을 적용합니다. 포르투갈어는 대부분의 범위에 대해 "other"를 사용합니다. 러시아어는 특정 범위에 대해 "few"를 사용합니다.

이러한 차이점은 국제 애플리케이션에서 복수 로직을 하드코딩하는 것이 실패하는 이유를 보여줍니다. API는 각 언어가 범위를 처리하는 방법에 대한 지식을 캡슐화합니다.

완전한 형식 지정을 위해 Intl.NumberFormat과 결합

실제 애플리케이션에서는 숫자와 텍스트를 모두 형식화해야 합니다. Intl.NumberFormat를 사용하여 로케일 규칙에 따라 범위 끝점을 형식화한 다음 selectRange()를 사용하여 올바른 복수 형식을 선택하세요.

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 formatRange(start, end) {
  const startFormatted = numberFormat.format(start);
  const endFormatted = numberFormat.format(end);
  const category = pluralRules.selectRange(start, end);
  const form = forms.get(category);
  return `${startFormatted}-${endFormatted} ${form}`;
}

console.log(formatRange(1, 3));
// Output: "1-3 items"

console.log(formatRange(1000, 5000));
// Output: "1,000-5,000 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 formatRange(start, end) {
  const startFormatted = numberFormat.format(start);
  const endFormatted = numberFormat.format(end);
  const category = pluralRules.selectRange(start, end);
  const form = forms.get(category);
  return `${startFormatted}-${endFormatted} ${form}`;
}

console.log(formatRange(1000, 5000));
// Output: "1.000-5.000 Artikel"

독일어는 쉼표 대신 마침표를 천 단위 구분 기호로 사용합니다. 숫자 포맷터는 이를 자동으로 처리합니다. 복수형 규칙은 "Artikel"의 어떤 형태를 사용할지 결정합니다.

단일 값에 대한 selectRange와 select 비교

select() 메서드는 단일 개수를 처리하고 selectRange()는 범위를 처리합니다. 단일 수량을 표시할 때는 select()를 사용하고 두 값 사이의 범위를 표시할 때는 selectRange()를 사용하세요.

const rules = new Intl.PluralRules("en-US");

// Single count
console.log(rules.select(1));
// Output: "one"

console.log(rules.select(2));
// Output: "other"

// Range
console.log(rules.selectRange(1, 2));
// Output: "other"

console.log(rules.selectRange(0, 1));
// Output: "other"

단일 개수의 경우 규칙은 해당 숫자 하나에만 의존합니다. 범위의 경우 규칙은 양쪽 끝점을 모두 고려합니다. 영어에서는 1로 시작하는 범위도 복수형을 사용하지만, 단일 개수 1은 단수형을 사용합니다.

일부 언어는 단일 개수 규칙과 범위 규칙 사이에 더 극적인 차이를 보입니다.

const slRules = new Intl.PluralRules("sl");

// Single counts in Slovenian
console.log(slRules.select(1));
// Output: "one"

console.log(slRules.select(2));
// Output: "two"

console.log(slRules.select(5));
// Output: "few"

// Range in Slovenian
console.log(slRules.selectRange(102, 201));
// Output: "few"

슬로베니아어는 복잡한 규칙에 따라 서로 다른 단일 개수에 대해 "one", "two", "few"를 사용합니다. 범위의 경우 두 숫자를 함께 고려하는 다른 논리를 적용합니다.

시작과 끝이 같은 범위 처리

시작 값과 끝 값이 같을 때는 너비가 없는 범위를 표시하는 것입니다. 일부 애플리케이션은 범위가 예상되는 컨텍스트에서 정확한 값을 나타내기 위해 이를 사용합니다.

const rules = new Intl.PluralRules("en-US");

console.log(rules.selectRange(5, 5));
// Output: "other"

console.log(rules.selectRange(1, 1));
// Output: "one"

두 값이 모두 1일 때 영어는 "one"을 반환하여 단수형을 사용해야 함을 나타냅니다. 두 값이 다른 숫자일 때 영어는 "other"를 반환하여 복수형을 제안합니다.

이 동작은 범위를 "1-1 item" 또는 단순히 "1 item"으로 표시하는 경우 의미가 있습니다. 1이 아닌 값의 경우 "5-5 items" 또는 "5 items"로 표시합니다.

실제로는 start가 end와 같을 때를 감지하여 범위 대신 단일 값을 표시하는 것이 좋습니다.

const rules = new Intl.PluralRules("en-US");
const forms = new Map([
  ["one", "item"],
  ["other", "items"]
]);

function formatRange(start, end) {
  if (start === end) {
    const category = rules.select(start);
    const form = forms.get(category);
    return `${start} ${form}`;
  }

  const category = rules.selectRange(start, end);
  const form = forms.get(category);
  return `${start}-${end} ${form}`;
}

console.log(formatRange(1, 1));
// Output: "1 item"

console.log(formatRange(5, 5));
// Output: "5 items"

console.log(formatRange(1, 3));
// Output: "1-3 items"

이 접근 방식은 동일한 값에 대해 select()를 사용하고 실제 범위에 대해 selectRange()를 사용합니다. "1-1" 또는 "5-5" 표시를 피하기 때문에 출력이 더 자연스럽게 읽힙니다.

selectRange로 엣지 케이스 처리하기

selectRange() 메서드는 입력을 검증합니다. 매개변수가 undefined, null이거나 유효한 숫자로 변환할 수 없는 경우 메서드는 오류를 발생시킵니다.

const rules = new Intl.PluralRules("en-US");

try {
  console.log(rules.selectRange(1, undefined));
} catch (error) {
  console.log(error.name);
  // Output: "TypeError"
}

try {
  console.log(rules.selectRange(NaN, 5));
} catch (error) {
  console.log(error.name);
  // Output: "RangeError"
}

selectRange()에 전달하기 전에 입력을 검증하세요. 이는 사용자 입력이나 외부 소스의 데이터로 작업할 때 특히 중요합니다.

function formatRange(start, end) {
  if (typeof start !== "number" || typeof end !== "number") {
    throw new Error("Start and end must be numbers");
  }

  if (isNaN(start) || isNaN(end)) {
    throw new Error("Start and end must be valid numbers");
  }

  const category = rules.selectRange(start, end);
  const form = forms.get(category);
  return `${start}-${end} ${form}`;
}

이 메서드는 숫자, BigInt 값 또는 숫자로 파싱할 수 있는 문자열을 허용합니다.

const rules = new Intl.PluralRules("en-US");

console.log(rules.selectRange(1, 5));
// Output: "other"

console.log(rules.selectRange(1n, 5n));
// Output: "other"

console.log(rules.selectRange("1", "5"));
// Output: "other"

문자열 입력은 숫자로 파싱됩니다. 이를 통해 메서드 호출 방식에 유연성을 제공하지만, 명확성을 위해 가능한 경우 실제 숫자 타입을 전달하는 것이 좋습니다.

소수 범위 처리하기

selectRange() 메서드는 십진수와 함께 작동합니다. 이는 측정값이나 통계와 같은 분수 수량의 범위를 표시할 때 유용합니다.

const rules = new Intl.PluralRules("en-US");

console.log(rules.selectRange(1.5, 2.5));
// Output: "other"

console.log(rules.selectRange(0.5, 1.0));
// Output: "other"

console.log(rules.selectRange(1.0, 1.5));
// Output: "other"

영어는 이러한 모든 소수 범위를 복수로 취급합니다. 다른 언어는 소수 범위에 대해 다른 규칙을 가질 수 있습니다.

십진수 범위를 형식화할 때는 selectRange()를 적절한 십진수 정밀도로 구성된 Intl.NumberFormat와 결합합니다.

const locale = "en-US";
const numberFormat = new Intl.NumberFormat(locale, {
  minimumFractionDigits: 1,
  maximumFractionDigits: 1
});
const pluralRules = new Intl.PluralRules(locale);
const forms = new Map([
  ["one", "kilometer"],
  ["other", "kilometers"]
]);

function formatRange(start, end) {
  const startFormatted = numberFormat.format(start);
  const endFormatted = numberFormat.format(end);
  const category = pluralRules.selectRange(start, end);
  const form = forms.get(category);
  return `${startFormatted}-${endFormatted} ${form}`;
}

console.log(formatRange(1.5, 2.5));
// Output: "1.5-2.5 kilometers"

console.log(formatRange(0.5, 1.0));
// Output: "0.5-1.0 kilometers"

숫자 포매터는 일관된 소수 표시를 보장합니다. 복수 규칙은 소수 값을 기반으로 올바른 형태를 결정합니다.

브라우저 지원 및 호환성

selectRange() 메서드는 Intl API의 나머지 부분에 비해 비교적 새로운 기능입니다. 이 메서드는 Intl.NumberFormat v3 사양의 일부로 2023년에 사용 가능하게 되었습니다.

브라우저 지원에는 Chrome 106 이상, Firefox 116 이상, Safari 15.4 이상, Edge 106 이상이 포함됩니다. 이 메서드는 Internet Explorer나 이전 브라우저 버전에서는 사용할 수 없습니다.

최신 브라우저를 대상으로 하는 애플리케이션의 경우 폴리필 없이 selectRange()를 사용할 수 있습니다. 구형 브라우저를 지원해야 하는 경우 사용하기 전에 메서드의 존재 여부를 확인하세요.

const rules = new Intl.PluralRules("en-US");

if (typeof rules.selectRange === "function") {
  // Use selectRange for range pluralization
  console.log(rules.selectRange(1, 3));
} else {
  // Fall back to select with the end value
  console.log(rules.select(3));
}

이 대체 방식은 selectRange()를 사용할 수 없을 때 종료 값에 select()를 사용합니다. 이는 모든 언어에 대해 언어학적으로 완벽하지는 않지만 구형 브라우저에 대해 합리적인 근사치를 제공합니다.

구형 환경에 대한 포괄적인 지원이 필요한 경우 @formatjs/intl-pluralrules와 같은 패키지를 통해 폴리필을 사용할 수 있습니다.

selectRange와 select를 사용하는 경우

UI에서 시작 값과 종료 값이 모두 사용자에게 표시되는 범위를 명시적으로 표시할 때 selectRange()를 사용합니다. 여기에는 "10-15개의 일치 항목을 찾았습니다"를 표시하는 검색 결과, "1-3개 항목 재고 있음"을 표시하는 재고, "2-5개 옵션 선택"을 표시하는 필터와 같은 컨텍스트가 포함됩니다.

해당 개수가 근사값이나 요약된 값을 나타내는 경우에도 단일 개수를 표시할 때는 select()를 사용합니다. 예를 들어 "약 10개의 결과"는 범위가 아닌 단일 숫자를 표시하기 때문에 select(10)를 사용합니다.

범위가 숫자에 대해 Intl.NumberFormat.formatRange()를 사용하여 표시되는 경우 함께 표시되는 텍스트에는 selectRange()를 사용합니다. 이렇게 하면 숫자 형식화와 텍스트 복수화 간의 일관성이 보장됩니다.

const locale = "en-US";
const numberFormat = new Intl.NumberFormat(locale);
const pluralRules = new Intl.PluralRules(locale);
const forms = new Map([
  ["one", "result"],
  ["other", "results"]
]);

function formatSearchResults(start, end) {
  const rangeFormatted = numberFormat.formatRange(start, end);
  const category = pluralRules.selectRange(start, end);
  const form = forms.get(category);
  return `Found ${rangeFormatted} ${form}`;
}

console.log(formatSearchResults(10, 15));
// Output: "Found 10–15 results"

이 패턴은 Intl.NumberFormatformatRange()를 사용하여 숫자를 형식화하고 Intl.PluralRulesselectRange()를 사용하여 텍스트를 선택합니다. 두 메서드 모두 범위에서 작동하여 모든 언어에 대한 올바른 처리를 보장합니다.