Files
suaveplan/openspec/archive/2026-05-02-locale-data-spec-conformance/specs/locale-data/spec.md
2026-05-27 23:29:53 +01:00

7.8 KiB

Spec: @suaveplan/locale-data

Package: @suaveplan/locale-data Domain: i18n/locale-data Version: 0.1.0 Status: Published. Spec heading conformance: locale-data-spec-conformance (2026-04-26).

Overview

Static ISO 3166-1 country data (209 entries) and ISO 639-1 language data (130+ entries) with typed lookup functions, OTel instrumentation, and Zod schemas.

Requirements

Requirement: Country lookup by ISO 3166-1 alpha-2 code

The package SHALL export getCountry(code: string): CountryItem performing an O(1) lookup over the static country map. Unknown codes MUST throw LocaleDataError with code LOCALE_DATA.COUNTRY.NOT_FOUND. The function MUST emit an OTel counter locale_data.country.lookups with attribute result: "found" | "not_found".

Scenario: Known code returns the country record

  • WHEN getCountry("US"), getCountry("DE"), or getCountry("JP") is called
  • THEN the corresponding CountryItem is returned with all 9 fields populated

Scenario: Unknown code throws LocaleDataError

  • WHEN getCountry is called with a code not in the registry
  • THEN a LocaleDataError is thrown whose code is LOCALE_DATA.COUNTRY.NOT_FOUND AND whose message names the unknown code

Verified by: src/countries/countries.test.ts::"returns a known country by ISO code" AND src/countries/countries.test.ts::"returns Germany correctly" AND src/countries/countries.test.ts::"returns Japan correctly" AND src/countries/countries.test.ts::"throws LocaleDataError for unknown code" AND src/countries/countries.test.ts::"throws with NOT_FOUND message for unknown code"

Requirement: Bulk country accessors

The package SHALL export getAllCountries(): Record<string, CountryItem> returning the entire registry by stable reference (no copy), getCountriesByContinent(continent: string): CountryItem[] filtering by 2-letter continent code, and getCountriesByCurrency(currency: string): CountryItem[] filtering by currency code with multi-currency entries split on comma.

Scenario: getAllCountries returns the full registry by reference

  • WHEN getAllCountries() is called twice
  • THEN both calls return the same object reference (no defensive copy)
  • AND the registry includes US, DE, JP, FR

Scenario: Continent filter narrows by 2-letter code

  • WHEN getCountriesByContinent("EU") or getCountriesByContinent("AS") is called
  • THEN only countries on that continent are returned
  • WHEN an unknown continent is given
  • THEN an empty array is returned

Scenario: Currency filter splits comma-separated entries

  • WHEN getCountriesByCurrency("EUR") or getCountriesByCurrency("USD") is called
  • THEN every country whose currency field contains the code (including in a comma-separated list) is returned
  • WHEN an unknown currency is given
  • THEN an empty array is returned

Verified by: src/countries/countries.test.ts::"returns all countries as a record" AND src/countries/countries.test.ts::"includes US, DE, JP, FR" AND src/countries/countries.test.ts::"returns the same reference each call (no copy)" AND src/countries/countries.test.ts::"returns only European countries for EU" AND src/countries/countries.test.ts::"returns Asian countries for AS" AND src/countries/countries.test.ts::"returns empty array for unknown continent" AND src/countries/countries.test.ts::"returns countries using EUR" AND src/countries/countries.test.ts::"returns countries using USD" AND src/countries/countries.test.ts::"returns empty array for unknown currency" AND src/countries/countries.test.ts::"supports multi-currency entries (comma-separated)"

Requirement: Language lookup and bulk accessor

The package SHALL export getLanguage(code: string): LanguageItem (O(1)) and getLanguages(): Record<string, LanguageItem> returning the same reference each call. Unknown codes MUST throw LocaleDataError with code LOCALE_DATA.LANGUAGE.NOT_FOUND. The lookup MUST emit an OTel counter locale_data.language.lookups with result: "found" | "not_found".

Scenario: getLanguages returns the full registry by reference

  • WHEN getLanguages() is called twice
  • THEN both calls return the same object reference
  • AND the registry includes common languages with native names populated

Scenario: Known language returns its record

  • WHEN getLanguage("en") or getLanguage("ar") is called
  • THEN the language record is returned with the correct native name

Scenario: Unknown language throws

  • WHEN getLanguage is called with an unknown code
  • THEN a LocaleDataError(LOCALE_DATA.LANGUAGE.NOT_FOUND) is thrown with the code in the message

Verified by: src/languages/languages.test.ts::"returns all languages" AND src/languages/languages.test.ts::"returns the same reference each call" AND src/languages/languages.test.ts::"includes common languages" AND src/languages/languages.test.ts::"includes native names" AND src/languages/languages.test.ts::"returns a known language" AND src/languages/languages.test.ts::"returns English with correct native name" AND src/languages/languages.test.ts::"returns Arabic with correct native name" AND src/languages/languages.test.ts::"throws for unknown language code" AND src/languages/languages.test.ts::"throws with code in message for unknown language"

Requirement: RTL language detection

The package SHALL export getRtlLanguages(): readonly string[] listing the canonical RTL language codes, and isRtlLanguage(code: string): boolean which is BCP-47 aware (subtags MUST be stripped before lookup, so "ar-EG" matches "ar").

Scenario: RTL list includes the canonical RTL languages

  • WHEN getRtlLanguages() is called
  • THEN it returns a non-empty array containing at least "ar", "he", "fa", "ur"
  • AND every code in the list resolves via getLanguage

Scenario: isRtlLanguage classifies known RTL languages

  • WHEN isRtlLanguage("ar"), isRtlLanguage("he"), isRtlLanguage("fa"), or isRtlLanguage("ur") is called
  • THEN the result is true

Scenario: isRtlLanguage classifies non-RTL and unknown codes

  • WHEN isRtlLanguage("en"), isRtlLanguage("fr"), or isRtlLanguage is called with an unknown code
  • THEN the result is false

Verified by: src/languages/languages.test.ts::"includes Arabic, Hebrew, Persian, Urdu" AND src/languages/languages.test.ts::"all returned codes exist in language map" AND src/languages/languages.test.ts::"returns a non-empty array" AND src/languages/languages.test.ts::"returns true for Arabic" AND src/languages/languages.test.ts::"returns true for Hebrew" AND src/languages/languages.test.ts::"returns true for Persian" AND src/languages/languages.test.ts::"returns true for Urdu" AND src/languages/languages.test.ts::"returns false for English" AND src/languages/languages.test.ts::"returns false for French" AND src/languages/languages.test.ts::"returns false for unknown code"

Errors

  • LOCALE_DATA_CODES — defineCodes("LOCALE_DATA", ["COUNTRY.NOT_FOUND", "LANGUAGE.NOT_FOUND"]).
  • LocaleDataError extends SuaveplanError<LocaleDataCode>.

Types / Schemas

  • CountryItemSchema (Zod) — 9 fields: name, native, phone, continent, capital, currency, languages[], emoji, emojiU.
  • LanguageItemSchema (Zod) — 2 fields: name, native.
  • CountryCodeSchema — z.string().length(2).toUpperCase().
  • LanguageCodeSchema — z.string().min(2).max(3).toLowerCase().

Data Coverage

  • Countries: 209 ISO 3166-1 alpha-2 entries, 7 continent codes.
  • Languages: 130+ ISO 639-1 entries, 15 RTL codes (ar, az, ba, dv, fa, he, iu, ks, ku, ps, sd, sr, tg, ur, yi).

Quality Gates

  • 100% Istanbul coverage (all 4 thresholds)
  • 100% docs coverage (6/6 implementation files)
  • Biome lint clean
  • TypeScript clean