Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,122 @@
# @suaveplan/locale-data - Static ISO 3166-1 country data and ISO 639-1 language data with typed lookup functions, OTel instrumentation, and Zod schemas
## Summary
Static ISO 3166-1 country data and ISO 639-1 language data with typed lookup functions,
OTel instrumentation, and Zod schemas.
<!-- begin:auto-docs -->
## Documentation tree
- [countries](./src/countries/countries.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [countries.data](./src/countries/countries.data.md) — Contains the static ISO 3166-1 alpha-2 country dataset as a single frozen TypeScript constant (`COUNTRIES_DATA`). This…
- [countries](./src/countries/countries.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [errors](./src/errors/locale-data-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [locale-data-error](./src/errors/locale-data-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [languages](./src/languages/languages.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [languages.data](./src/languages/languages.data.md) — Contains the static ISO 639-1 language dataset as two frozen TypeScript constants: `LANGUAGES_DATA` (130+ language entr…
- [languages](./src/languages/languages.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
<!-- end:auto-docs --> - [countries.data](./src/countries/countries.data.md) — Contains the static ISO 3166-1 alpha-2 country dataset as a single frozen TypeScript constant (`COUNTRIES_DATA`). This…
- [countries](./src/countries/countries.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/locale-data-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [locale-data-error](./src/errors/locale-data-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [languages](./src/languages/languages.md) — <!-- begin:auto-docs --> _(no submodules)_- [languages.data](./src/languages/languages.data.md) — Contains the static ISO 639-1 language dataset as two frozen TypeScript constants: `LANGUAGES_DATA` (130+ language entr…
- [languages](./src/languages/languages.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [countries.data](./src/countries/countries.data.md) — Contains the static ISO 3166-1 alpha-2 country dataset as a single frozen TypeScript constant (`COUNTRIES_DATA`). This…
- [countries](./src/countries/countries.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/locale-data-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [locale-data-error](./src/errors/locale-data-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [languages](./src/languages/languages.md) — <!-- begin:auto-docs --> _(no submodules)_- [languages.data](./src/languages/languages.data.md) — Contains the static ISO 639-1 language dataset as two frozen TypeScript constants: `LANGUAGES_DATA` (130+ language entr…
- [languages](./src/languages/languages.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [countries.data](./src/countries/countries.data.md) — Contains the static ISO 3166-1 alpha-2 country dataset as a single frozen TypeScript constant (`COUNTRIES_DATA`). This…
- [countries](./src/countries/countries.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/locale-data-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [locale-data-error](./src/errors/locale-data-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [languages](./src/languages/languages.md) — <!-- begin:auto-docs --> _(no submodules)_- [languages.data](./src/languages/languages.data.md) — Contains the static ISO 639-1 language dataset as two frozen TypeScript constants: `LANGUAGES_DATA` (130+ language entr…
- [languages](./src/languages/languages.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [countries.data](./src/countries/countries.data.md) — Contains the static ISO 3166-1 alpha-2 country dataset as a single frozen TypeScript constant (`COUNTRIES_DATA`). This…
- [countries](./src/countries/countries.md) — <!-- begin:auto-docs --> _(no submodules)_- [errors](./src/errors/locale-data-error.md) — <!-- begin:auto-docs --> _(no submodules)_ - [locale-data-error](./src/errors/locale-data-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [languages](./src/languages/languages.md) — <!-- begin:auto-docs --> _(no submodules)_ - [languages.data](./src/languages/languages.data.md) — Contains the static ISO 639-1 language dataset as two frozen TypeScript constants: `LANGUAGES_DATA` (130+ language entr…
- [languages](./src/languages/languages.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ - [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_
## Install
```bash
bun add @suaveplan/locale-data
```
## Quick Start
```ts
import { getCountry, getLanguage, isRtlLanguage } from "@suaveplan/locale-data";
// Country lookup
const us = getCountry("US");
console.log(us.name); // "United States"
console.log(us.phone); // "1"
console.log(us.emoji); // "🇺🇸"
// Language lookup
const ar = getLanguage("ar");
console.log(ar.name); // "Arabic"
console.log(ar.native); // "العربية"
// RTL detection
console.log(isRtlLanguage("ar")); // true
console.log(isRtlLanguage("ar-SA")); // true (BCP-47 tag)
console.log(isRtlLanguage("en")); // false
```
## Modules
| Subpath | Description |
|---|---|
| `@suaveplan/locale-data` | Root barrel — re-exports all public symbols |
| `@suaveplan/locale-data/countries` | Country lookups + Zod schemas |
| `@suaveplan/locale-data/languages` | Language lookups + RTL detection + Zod schemas |
| `@suaveplan/locale-data/errors` | `LocaleDataError` + `LOCALE_DATA_CODES` |
## Modules Documentation
- [countries.md](src/countries/countries.md) — `getCountry`, `getAllCountries`, `getCountriesByContinent`, `getCountriesByCurrency`
- [countries.data.md](src/countries/countries.data.md) — Static ISO 3166-1 dataset (209 entries)
- [languages.md](src/languages/languages.md) — `getLanguage`, `getLanguages`, `getRtlLanguages`, `isRtlLanguage`
- [languages.data.md](src/languages/languages.data.md) — Static ISO 639-1 dataset + RTL codes
- [locale-data-error.md](src/errors/locale-data-error.md) — `LocaleDataError`, `LOCALE_DATA_CODES`
- [types.md](src/types/types.md) — `CountryItemSchema`, `LanguageItemSchema`
## API Overview
### Countries
```ts
import {
getCountry, // O(1) lookup by ISO 3166-1 alpha-2 code
getAllCountries, // Returns full 209-entry Record<string, CountryItem>
getCountriesByContinent, // Filter by continent code ("EU", "AS", "AF", etc.)
getCountriesByCurrency, // Filter by ISO 4217 currency code (e.g. "EUR")
} from "@suaveplan/locale-data/countries";
```
### Languages
```ts
import {
getLanguage, // O(1) lookup by ISO 639-1 code
getLanguages, // Returns full 130+ entry Record<string, LanguageItem>
getRtlLanguages, // Returns list of RTL language codes
isRtlLanguage, // BCP-47 aware RTL detection
} from "@suaveplan/locale-data/languages";
```
### Error Handling
```ts
import { LocaleDataError, LOCALE_DATA_CODES } from "@suaveplan/locale-data/errors";
try {
getCountry("ZZ");
} catch (err) {
if (err instanceof LocaleDataError) {
console.log(err.code); // "LOCALE_DATA/COUNTRY.NOT_FOUND"
}
}
```
## OTel Instrumentation
The package emits two OTel counters:
| Metric | Attributes |
|---|---|
| `locale_data.country.lookups` | `result: "found" \| "not_found"` |
| `locale_data.language.lookups` | `result: "found" \| "not_found"` |

View File

@@ -0,0 +1,80 @@
{
"name": "@suaveplan/locale-data",
"version": "0.1.0",
"type": "module",
"sideEffects": false,
"description": "Static ISO 3166-1 country data and ISO 639-1 language data with typed lookup functions, OTel instrumentation, and Zod schemas",
"license": "MIT",
"author": "Suaveplan Team",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"import": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"require": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"./countries": {
"import": {
"types": "./dist/countries/index.d.ts",
"default": "./dist/countries/index.js"
}
},
"./languages": {
"import": {
"types": "./dist/languages/index.d.ts",
"default": "./dist/languages/index.js"
}
},
"./errors": {
"import": {
"types": "./dist/errors/index.d.ts",
"default": "./dist/errors/index.js"
}
}
},
"scripts": {
"build": "vite build",
"test": "vitest run --passWithNoTests",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage --passWithNoTests",
"lint": "bunx biome check src/",
"lint:fix": "bunx biome lint --fix --unsafe --max-diagnostics 500 src/",
"format": "bunx biome format --write --max-diagnostics 500 src/",
"check": "bunx biome check --write --unsafe --max-diagnostics 500 src/",
"typecheck": "bunx tsc --noEmit --composite false -p tsconfig.lib.json",
"docs:check": "bun run ../../../../scripts/docs-check.ts ."
},
"dependencies": {
"@suaveplan/error": "workspace:^",
"@suaveplan/types": "workspace:^",
"@opentelemetry/api": "catalog:observability",
"tslib": "catalog:build",
"zod": "catalog:forms"
},
"devDependencies": {
"@suaveplan/testing": "workspace:^",
"@types/node": "catalog:",
"@vitest/coverage-istanbul": "catalog:testing",
"typescript": "catalog:",
"vite": "catalog:build",
"vite-plugin-dts": "catalog:build",
"vitest": "catalog:testing"
},
"publishConfig": {
"registry": "https://npmjs.fq.io/",
"access": "public"
},
"files": [
"dist",
"src",
"README.md"
]
}

View File

@@ -0,0 +1,69 @@
# countries.data
## Purpose
Contains the static ISO 3166-1 alpha-2 country dataset as a single frozen TypeScript constant
(`COUNTRIES_DATA`). This file holds 209 country entries sourced from the ISO standard and is
intentionally separated from the lookup logic in `countries.ts` to keep both files under the
300-line hard limit enforced by the project style guide. The data here serves as the single source
of truth for all country lookups, filtering, and validation in the `@suaveplan/locale-data` package.
## Features
- **209 ISO 3166-1 alpha-2 entries** — includes all UN member states and selected territories.
- **9 fields per entry** — `name` (English), `native` (local script), `phone` (dialling code),
`continent` (2-letter code), `capital`, `currency` (ISO 4217, comma-separated for multi-currency
countries), `languages` (array of ISO 639-1 codes), `emoji` (flag emoji), `emojiU` (Unicode
code-point notation).
- **Typed as `Record<string, CountryItem>`** — ensures each entry satisfies the `CountryItem`
shape defined in the types module.
- **Multi-currency support** — countries like Switzerland (`CHE,CHF,CHW`), Bolivia (`BOB,BOV`),
and Cuba (`CUC,CUP`) store all currencies as a comma-separated string, consistent with how
`getCountriesByCurrency` splits and matches entries.
- **Continent codes** — `EU` (Europe), `AS` (Asia), `AF` (Africa), `NA` (North America),
`SA` (South America), `OC` (Oceania), `AN` (Antarctica).
## Usage: Basic Example
```ts
// Imported indirectly via the countries module:
import { getAllCountries } from "@suaveplan/locale-data/countries";
const data = getAllCountries(); // returns COUNTRIES_DATA reference
console.log(data["US"].name); // "United States"
```
## Usage: Advanced Example
```ts
// The data object is consumed by the lookup functions in countries.ts:
import { getCountry, getCountriesByCurrency } from "@suaveplan/locale-data/countries";
// Direct lookup (O(1))
const ch = getCountry("CH");
console.log(ch.currency); // "CHE,CHF,CHW"
// Filter by currency (splits on comma internally)
const chfCountries = getCountriesByCurrency("CHF");
console.log(chfCountries.map((c) => c.name)); // includes "Switzerland"
```
## API Reference
### `COUNTRIES_DATA`
```ts
const COUNTRIES_DATA: Record<string, CountryItem>
```
A frozen constant record mapping ISO 3166-1 alpha-2 codes to `CountryItem` objects. Keys are
uppercase two-letter codes (e.g. `"US"`, `"DE"`, `"JP"`). Do not import or mutate this directly
from outside the countries module — use the public lookup functions instead.
## Implementation Notes
The data file is excluded from test coverage measurement because it contains no executable logic —
only a plain `export const` declaration. All entries were derived from the ISO standard data in the
reference project at `packages/universal/utils/src/countries/countries.ts` and include the same
209 keys present in the `countries` named export. The file deliberately omits entries not present
in the main `countries` object (such as `AQ`, `AG`, `AS`, `BL`, etc.) to stay consistent with the
reference dataset.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,120 @@
# countries
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides typed lookup functions over the static ISO 3166-1 alpha-2 country dataset (209 entries).
The module is designed for high-frequency read access — data is held in a single frozen object
(`COUNTRIES_DATA`) and all lookups run in O(1) or O(n) time with zero I/O. Every `getCountry`
call emits an OTel counter increment, enabling production observability for cache-hit analysis
and anomaly detection on invalid code injection attempts.
## Features
- **`getCountry(code)`** — O(1) lookup by ISO 3166-1 alpha-2 code; throws a typed
`LocaleDataError` for unknown codes.
- **`getAllCountries()`** — Returns the full `Record<string, CountryItem>` reference; O(1),
no copy allocation.
- **`getCountriesByContinent(continent)`** — Filters by two-letter continent code (`"EU"`, `"AS"`,
`"AF"`, `"NA"`, `"SA"`, `"OC"`, `"AN"`); returns empty array for unknown codes.
- **`getCountriesByCurrency(currency)`** — Matches against comma-separated ISO 4217 currency codes
in each entry (e.g. `"CHE,CHF,CHW"`); returns empty array for unknown currencies.
- **OTel instrumentation** — `locale_data.country.lookups` counter with `result: "found" | "not_found"`.
- **Zod schemas** — `CountryCodeSchema` (2-char uppercase) and re-exported `CountryItemSchema`.
## Usage: Basic Example
```ts
import { getCountry, getAllCountries } from "@suaveplan/locale-data/countries";
// Look up a single country
const us = getCountry("US");
console.log(us.name); // "United States"
console.log(us.phone); // "1"
console.log(us.emoji); // "🇺🇸"
// Get all countries
const all = getAllCountries();
console.log(Object.keys(all).length); // 209
```
## Usage: Advanced Example
```ts
import {
getCountry,
getCountriesByContinent,
getCountriesByCurrency,
} from "@suaveplan/locale-data/countries";
import { LocaleDataError, LOCALE_DATA_CODES } from "@suaveplan/locale-data/errors";
// Error handling
try {
const country = getCountry("ZZ");
} catch (err) {
if (err instanceof LocaleDataError && err.code === LOCALE_DATA_CODES["COUNTRY.NOT_FOUND"]) {
console.warn("Unknown country code");
}
}
// Filter by continent
const europeanCountries = getCountriesByContinent("EU");
console.log(europeanCountries.length); // ~40
// Find all Eurozone members (use single-currency entries + multi-currency)
const eurozoneMembers = getCountriesByCurrency("EUR");
console.log(eurozoneMembers.map((c) => c.name));
// Group by continent
import { getAllCountries } from "@suaveplan/locale-data/countries";
const byContinent = Object.entries(getAllCountries()).reduce<Record<string, string[]>>(
(acc, [code, c]) => {
(acc[c.continent] ??= []).push(code);
return acc;
},
{}
);
```
## API Reference
### `getCountry(code: string): CountryItem`
Look up a country by its ISO 3166-1 alpha-2 code.
- **Parameters:** `code` — case-sensitive two-letter code (e.g. `"US"`, `"DE"`).
- **Returns:** The matching `CountryItem`.
- **Throws:** `LocaleDataError` (`LOCALE_DATA/COUNTRY.NOT_FOUND`) when the code is not found.
- **OTel:** Increments `locale_data.country.lookups` with `result: "found"` or `"not_found"`.
### `getAllCountries(): Record<string, CountryItem>`
Return the full 209-entry country map.
- **Returns:** A `Record<string, CountryItem>` — the same reference each call.
- **Note:** Do not mutate the returned object.
### `getCountriesByContinent(continent: string): CountryItem[]`
Filter countries by continent code.
- **Parameters:** `continent` — `"EU"` | `"AS"` | `"AF"` | `"NA"` | `"SA"` | `"OC"` | `"AN"`.
- **Returns:** Array of matching `CountryItem` values; empty array for unknown codes.
### `getCountriesByCurrency(currency: string): CountryItem[]`
Filter countries by ISO 4217 currency code.
- **Parameters:** `currency` — e.g. `"EUR"`, `"USD"`, `"CHF"`.
- **Returns:** Array of matching `CountryItem` values; handles comma-separated multi-currency
entries; empty array if no match.
## Implementation Notes
Data is stored in `countries.data.ts` as a plain `Record<string, CountryItem>` const to keep the
implementation file under the 300-line hard limit. The data file is excluded from coverage
measurement because it contains no executable logic — only `export const`. The `getCountriesByCurrency`
function splits on commas to support countries with composite currency fields like Switzerland
(`"CHE,CHF,CHW"`), Bolivia (`"BOB,BOV"`), and Cuba (`"CUC,CUP"`).

View File

@@ -0,0 +1,8 @@
import z from "zod";
export { CountryItemSchema } from "../types/index.js";
/**
* Zod schema for a two-character ISO 3166-1 alpha-2 country code.
*/
export const CountryCodeSchema = z.string().length(2).toUpperCase();

View File

@@ -0,0 +1,108 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import {
getAllCountries,
getCountriesByContinent,
getCountriesByCurrency,
getCountry,
} from "./countries.js";
describe("getCountry", () => {
it("returns a known country by ISO code", { timeout: 5000 }, () => {
const us = getCountry("US");
expect(us.name).toBe("United States");
expect(us.phone).toBe("1");
});
it("returns Germany correctly", { timeout: 5000 }, () => {
const de = getCountry("DE");
expect(de.name).toBe("Germany");
expect(de.continent).toBe("EU");
expect(de.currency).toBe("EUR");
expect(de.emoji).toBe("🇩🇪");
});
it("returns Japan correctly", { timeout: 5000 }, () => {
const jp = getCountry("JP");
expect(jp.name).toBe("Japan");
expect(jp.capital).toBe("Tokyo");
expect(jp.languages).toContain("ja");
});
it("throws LocaleDataError for unknown code", { timeout: 5000 }, () => {
expect(() => getCountry("XX")).toThrow();
});
it("throws with NOT_FOUND message for unknown code", {
timeout: 5000,
}, () => {
expect(() => getCountry("ZZ")).toThrow(/ZZ/);
});
});
describe("getAllCountries", () => {
it("returns all countries as a record", { timeout: 5000 }, () => {
const all = getAllCountries();
expect(Object.keys(all).length).toBeGreaterThan(200);
expect(all["GB"].name).toBe("United Kingdom");
});
it("includes US, DE, JP, FR", { timeout: 5000 }, () => {
const all = getAllCountries();
expect(all["US"]).toBeDefined();
expect(all["DE"]).toBeDefined();
expect(all["JP"]).toBeDefined();
expect(all["FR"]).toBeDefined();
});
it("returns the same reference each call (no copy)", {
timeout: 5000,
}, () => {
const a = getAllCountries();
const b = getAllCountries();
expect(a).toBe(b);
});
});
describe("getCountriesByContinent", () => {
it("returns only European countries for EU", { timeout: 5000 }, () => {
const eu = getCountriesByContinent("EU");
expect(eu.every((c) => c.continent === "EU")).toBe(true);
expect(eu.length).toBeGreaterThan(30);
});
it("returns Asian countries for AS", { timeout: 5000 }, () => {
const as = getCountriesByContinent("AS");
expect(as.some((c) => c.name === "Japan")).toBe(true);
expect(as.every((c) => c.continent === "AS")).toBe(true);
});
it("returns empty array for unknown continent", { timeout: 5000 }, () => {
const result = getCountriesByContinent("XX");
expect(result).toEqual([]);
});
});
describe("getCountriesByCurrency", () => {
it("returns countries using EUR", { timeout: 5000 }, () => {
const eur = getCountriesByCurrency("EUR");
expect(eur.some((c) => c.name === "Germany")).toBe(true);
});
it("returns countries using USD", { timeout: 5000 }, () => {
const usd = getCountriesByCurrency("USD");
expect(usd.some((c) => c.name === "United States")).toBe(true);
});
it("returns empty array for unknown currency", { timeout: 5000 }, () => {
const result = getCountriesByCurrency("ZZZ");
expect(result).toEqual([]);
});
it("supports multi-currency entries (comma-separated)", {
timeout: 5000,
}, () => {
// Switzerland has CHE,CHF,CHW
const chf = getCountriesByCurrency("CHF");
expect(chf.some((c) => c.name === "Switzerland")).toBe(true);
});
});

View File

@@ -0,0 +1,75 @@
import { metrics } from "@opentelemetry/api";
import { LOCALE_DATA_CODES, LocaleDataError } from "../errors/index.js";
import type { CountryItem } from "../types/index.js";
import { COUNTRIES_DATA } from "./countries.data.js";
const meter = metrics.getMeter("@suaveplan/locale-data");
const lookupCounter = meter.createCounter("locale_data.country.lookups", {
description: "Number of country lookups by result",
});
/**
* Look up a country by its ISO 3166-1 alpha-2 code.
*
* Increments the `locale_data.country.lookups` OTel counter with
* `result: "found"` or `result: "not_found"`.
*
* @param code - ISO 3166-1 alpha-2 country code (e.g. `"US"`, `"DE"`).
* @returns The matching `CountryItem`.
* @throws `LocaleDataError` with code `LOCALE_DATA/COUNTRY.NOT_FOUND` when
* the code is not in the dataset.
*/
export function getCountry(code: string): CountryItem {
const entry = COUNTRIES_DATA[code];
if (!entry) {
lookupCounter.add(1, { result: "not_found" });
throw new LocaleDataError(
LOCALE_DATA_CODES["COUNTRY.NOT_FOUND"],
`No country found for ISO code: ${code}`
);
}
lookupCounter.add(1, { result: "found" });
return entry;
}
/**
* Return the full country data record (all 209 entries).
*
* Returns the same reference on each call — do not mutate.
*
* @returns A `Record<string, CountryItem>` keyed by ISO 3166-1 alpha-2 code.
*/
export function getAllCountries(): Record<string, CountryItem> {
return COUNTRIES_DATA;
}
/**
* Return all countries in a given continent.
*
* @param continent - Two-letter continent code (`"EU"`, `"AS"`, `"AF"`, `"NA"`,
* `"SA"`, `"OC"`, `"AN"`).
* @returns An array of `CountryItem` values whose `continent` matches exactly.
* Returns an empty array for unknown continent codes.
*/
export function getCountriesByContinent(continent: string): CountryItem[] {
return Object.values(COUNTRIES_DATA).filter(
(c) => c.continent === continent
);
}
/**
* Return all countries that use a given currency code.
*
* Countries may list multiple currencies as a comma-separated string
* (e.g. `"CHE,CHF,CHW"`). This function matches against each individual
* currency code in the list.
*
* @param currency - ISO 4217 currency code (e.g. `"EUR"`, `"USD"`, `"CHF"`).
* @returns An array of matching `CountryItem` values. Returns an empty array
* when no countries use the given currency.
*/
export function getCountriesByCurrency(currency: string): CountryItem[] {
return Object.values(COUNTRIES_DATA).filter((c) =>
c.currency.split(",").includes(currency)
);
}

View File

@@ -0,0 +1,7 @@
export {
getAllCountries,
getCountriesByContinent,
getCountriesByCurrency,
getCountry,
} from "./countries.js";
export { CountryCodeSchema, CountryItemSchema } from "./countries.schemas.js";

View File

@@ -0,0 +1,5 @@
export {
LOCALE_DATA_CODES,
type LocaleDataCode,
LocaleDataError,
} from "./locale-data-error.js";

View File

@@ -0,0 +1,100 @@
# locale-data-error
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the typed error class and stable code registry for `@suaveplan/locale-data`. All errors
thrown by this package extend `SuaveplanError`, ensuring consistent structured error handling across
the project — with machine-readable codes, cause-chain propagation, and OTel span-event support
inherited from `@suaveplan/error`.
## Features
- **Stable code registry** via `defineCodes("LOCALE_DATA", [...])` — codes are namespaced under
`LOCALE_DATA` and never change once published, ensuring forward compatibility for error consumers.
- **Two error codes**: `COUNTRY.NOT_FOUND` for unknown ISO 3166-1 alpha-2 codes and
`LANGUAGE.NOT_FOUND` for unknown ISO 639-1 language codes.
- **Full `SuaveplanError` inheritance**: cause chaining, redaction, wire-format serialisation,
and OTel span-event emission are all available on every `LocaleDataError` instance.
- **Typed generic**: `LocaleDataError` is parameterised on `LocaleDataCode`, so TypeScript narrows
`.code` to the union of defined codes in `catch` blocks.
## Usage: Basic Example
```ts
import { LocaleDataError, LOCALE_DATA_CODES } from "@suaveplan/locale-data/errors";
throw new LocaleDataError(
LOCALE_DATA_CODES["COUNTRY.NOT_FOUND"],
"No country found for code: ZZ"
);
```
## Usage: Advanced Example
```ts
import { LocaleDataError, LOCALE_DATA_CODES } from "@suaveplan/locale-data/errors";
import { isSuaveplanError } from "@suaveplan/error";
try {
const country = getCountry("ZZ");
} catch (err) {
if (isSuaveplanError(err) && err.code === LOCALE_DATA_CODES["COUNTRY.NOT_FOUND"]) {
console.error("Country lookup failed:", err.message);
// err.cause, err.context, err.toJSON() all available
}
throw err;
}
```
## API Reference
### `LOCALE_DATA_CODES`
```ts
const LOCALE_DATA_CODES: {
"COUNTRY.NOT_FOUND": "LOCALE_DATA/COUNTRY.NOT_FOUND";
"LANGUAGE.NOT_FOUND": "LOCALE_DATA/LANGUAGE.NOT_FOUND";
}
```
A frozen record produced by `defineCodes`. Use these values as the `code` argument to
`LocaleDataError` and for equality checks in catch blocks.
### `LocaleDataCode`
```ts
type LocaleDataCode = "LOCALE_DATA/COUNTRY.NOT_FOUND" | "LOCALE_DATA/LANGUAGE.NOT_FOUND";
```
The union type of all valid error codes for this package.
### `LocaleDataError`
```ts
class LocaleDataError extends SuaveplanError<LocaleDataCode> {
public constructor(
code: LocaleDataCode,
message: string,
options?: SuaveplanErrorOptions
);
}
```
**Parameters:**
- `code` — one of `LOCALE_DATA_CODES`; narrows the `.code` property type.
- `message` — human-readable description, suitable for logging.
- `options` — optional `{ cause?: unknown; context?: Record<string, unknown> }`.
**Throws:** Never (is the thrown value itself).
## Implementation Notes
The constructor is otherwise a no-op pass-through to `SuaveplanError` — the
`// biome-ignore lint/complexity/noUselessConstructor` comment is required because Biome would flag
the empty constructor. We keep it explicitly `public` to widen the inherited `protected` visibility
and to document the intended API surface. Error code strings are of the form
`"LOCALE_DATA/<CATEGORY>.<KEY>"` as produced by `defineCodes`.

View File

@@ -0,0 +1,28 @@
import {
defineCodes,
SuaveplanError,
type SuaveplanErrorOptions,
} from "@suaveplan/error";
/**
* Error code registry for `@suaveplan/locale-data`.
*/
export const LOCALE_DATA_CODES = defineCodes("LOCALE_DATA", [
"COUNTRY.NOT_FOUND",
"LANGUAGE.NOT_FOUND",
] as const);
export type LocaleDataCode =
(typeof LOCALE_DATA_CODES)[keyof typeof LOCALE_DATA_CODES];
/** Typed error for `@suaveplan/locale-data`. */
export class LocaleDataError extends SuaveplanError<LocaleDataCode> {
// biome-ignore lint/complexity/noUselessConstructor: widens visibility
public constructor(
code: LocaleDataCode,
message: string,
options?: SuaveplanErrorOptions
) {
super(code, message, options);
}
}

View File

@@ -0,0 +1,4 @@
export * from "./countries/index.js";
export * from "./errors/index.js";
export * from "./languages/index.js";
export * from "./types/index.js";

View File

@@ -0,0 +1,7 @@
export {
getLanguage,
getLanguages,
getRtlLanguages,
isRtlLanguage,
} from "./languages.js";
export { LanguageCodeSchema, LanguageItemSchema } from "./languages.schemas.js";

View File

@@ -0,0 +1,78 @@
# languages.data
## Purpose
Contains the static ISO 639-1 language dataset as two frozen TypeScript constants: `LANGUAGES_DATA`
(130+ language entries) and `RTL_LANGUAGES` (the list of right-to-left script codes). This file is
intentionally separated from the lookup logic in `languages.ts` to keep both files under the
300-line hard limit. The data serves as the single source of truth for all language lookups and
RTL detection in the `@suaveplan/locale-data` package.
## Features
- **130+ ISO 639-1 language entries** — covers the full set of two-letter language codes in the
ISO standard, from Afar (`aa`) to Zulu (`zu`).
- **Two fields per entry** — `name` (English name) and `native` (name in the language's own script,
e.g. `"Deutsch"` for German, `"العربية"` for Arabic).
- **`RTL_LANGUAGES` constant** — readonly array of 15 ISO 639-1 codes whose primary script is
right-to-left: `ar`, `az`, `ba`, `dv`, `fa`, `he`, `iu`, `ks`, `ku`, `ps`, `sd`, `sr`, `tg`,
`ur`, `yi`.
- **Typed as `Record<string, LanguageItem>`** — ensures each entry satisfies the `LanguageItem`
shape defined in the types module.
- **Unicode-safe** — native names include characters from Arabic, Hebrew, Cyrillic, Devanagari,
Georgian, Thai, Chinese, Japanese, Korean, and other scripts.
## Usage: Basic Example
```ts
// Imported indirectly via the languages module:
import { getLanguages } from "@suaveplan/locale-data/languages";
const all = getLanguages(); // returns LANGUAGES_DATA reference
console.log(all["ja"].name); // "Japanese"
console.log(all["ja"].native); // "日本語"
```
## Usage: Advanced Example
```ts
import { getRtlLanguages, isRtlLanguage } from "@suaveplan/locale-data/languages";
// Get all RTL codes
const rtlCodes = getRtlLanguages();
console.log(rtlCodes); // ["ar", "az", "ba", "dv", "fa", "he", "iu", "ks", "ku", "ps", "sd", "sr", "tg", "ur", "yi"]
// BCP-47 tag detection
console.log(isRtlLanguage("ar-EG")); // true (Arabic, Egypt)
console.log(isRtlLanguage("zh-TW")); // false (Traditional Chinese)
```
## API Reference
### `LANGUAGES_DATA`
```ts
const LANGUAGES_DATA: Record<string, LanguageItem>
```
A frozen constant record mapping ISO 639-1 two-letter codes to `LanguageItem` objects.
Do not import or mutate this directly from outside the languages module — use the public lookup
functions instead.
### `RTL_LANGUAGES`
```ts
const RTL_LANGUAGES: readonly string[]
```
A readonly array of ISO 639-1 codes for right-to-left scripts. Used by `getRtlLanguages()` and
`isRtlLanguage()`. Do not mutate.
## Implementation Notes
The data file is excluded from test coverage measurement because it contains no executable logic —
only plain `export const` declarations. The RTL language list was derived from the reference
implementation at `packages/universal/utils/src/languages/languages.ts` in the reference project.
The list includes `az` (Azerbaijani) and `sr` (Serbian) because both languages historically use
RTL scripts in some contexts, consistent with the reference data. The `isRtlLanguage` function
imports `RTL_LANGUAGES` as a readonly array and uses `Array.prototype.includes` for O(n) membership
testing — with only 15 RTL codes, this is negligible overhead compared to a `Set` construction.

View File

@@ -0,0 +1,214 @@
import type { LanguageItem } from "../types/index.js";
/**
* Static ISO 639-1 language data (130+ entries).
* Do not edit — generated from ISO standard data.
*/
export const LANGUAGES_DATA: Record<string, LanguageItem> = {
aa: { name: "Afar", native: "Afar" },
ab: { name: "Abkhazian", native: "Аҧсуа" },
af: { name: "Afrikaans", native: "Afrikaans" },
ak: { name: "Akan", native: "Akana" },
am: { name: "Amharic", native: "አማርኛ" },
an: { name: "Aragonese", native: "Aragonés" },
ar: { name: "Arabic", native: "العربية" },
as: { name: "Assamese", native: "অসমীয়া" },
av: { name: "Avar", native: "Авар" },
ay: { name: "Aymara", native: "Aymar" },
az: { name: "Azerbaijani", native: "Azərbaycanca / آذربايجان" },
ba: { name: "Bashkir", native: "Башҡорт" },
be: { name: "Belarusian", native: "Беларуская" },
bg: { name: "Bulgarian", native: "Български" },
bh: { name: "Bihari", native: "भोजपुरी" },
bi: { name: "Bislama", native: "Bislama" },
bm: { name: "Bambara", native: "Bamanankan" },
bn: { name: "Bengali", native: "বাংলা" },
bo: { name: "Tibetan", native: "བོད་ཡིག / Bod skad" },
br: { name: "Breton", native: "Brezhoneg" },
bs: { name: "Bosnian", native: "Bosanski" },
ca: { name: "Catalan", native: "Català" },
ce: { name: "Chechen", native: "Нохчийн" },
ch: { name: "Chamorro", native: "Chamoru" },
co: { name: "Corsican", native: "Corsu" },
cr: { name: "Cree", native: "Nehiyaw" },
cs: { name: "Czech", native: "Čeština" },
cu: {
name: "Old Church Slavonic / Old Bulgarian",
native: "словѣньскъ / slověnĭskŭ",
},
cv: { name: "Chuvash", native: "Чăваш" },
cy: { name: "Welsh", native: "Cymraeg" },
da: { name: "Danish", native: "Dansk" },
de: { name: "German", native: "Deutsch" },
dv: { name: "Divehi", native: "ދިވެހިބަސް" },
dz: { name: "Dzongkha", native: "ཇོང་ཁ" },
ee: { name: "Ewe", native: "Ɛʋɛ" },
el: { name: "Greek", native: "Ελληνικά" },
en: { name: "English", native: "English" },
eo: { name: "Esperanto", native: "Esperanto" },
es: { name: "Spanish", native: "Español" },
et: { name: "Estonian", native: "Eesti" },
eu: { name: "Basque", native: "Euskara" },
fa: { name: "Persian", native: "فارسی" },
ff: { name: "Peul", native: "Fulfulde" },
fi: { name: "Finnish", native: "Suomi" },
fj: { name: "Fijian", native: "na Vosa Vakaviti" },
fo: { name: "Faroese", native: "Føroyskt" },
fr: { name: "French", native: "Français" },
fy: { name: "West Frisian", native: "Frysk" },
ga: { name: "Irish", native: "Gaeilge" },
gd: { name: "Scottish Gaelic", native: "Gàidhlig" },
gl: { name: "Galician", native: "Galego" },
gn: { name: "Guarani", native: "Avañe'ẽ" },
gu: { name: "Gujarati", native: "ગુજરાતી" },
gv: { name: "Manx", native: "Gaelg" },
ha: { name: "Hausa", native: "هَوُسَ" },
he: { name: "Hebrew", native: "עברית" },
hi: { name: "Hindi", native: "हिन्दी" },
ho: { name: "Hiri Motu", native: "Hiri Motu" },
hr: { name: "Croatian", native: "Hrvatski" },
ht: { name: "Haitian", native: "Krèyol ayisyen" },
hu: { name: "Hungarian", native: "Magyar" },
hy: { name: "Armenian", native: "Հայերեն" },
hz: { name: "Herero", native: "Otsiherero" },
ia: { name: "Interlingua", native: "Interlingua" },
id: { name: "Indonesian", native: "Bahasa Indonesia" },
ie: { name: "Interlingue", native: "Interlingue" },
ig: { name: "Igbo", native: "Igbo" },
ii: { name: "Sichuan yi", native: "ꆇꉙ / 四川彝语" },
ik: { name: "Inupiak", native: "Iñupiak" },
io: { name: "Ido", native: "Ido" },
is: { name: "Icelandic", native: "Íslenska" },
it: { name: "Italian", native: "Italiano" },
iu: { name: "Inuktitut", native: "ᐃᓄᒃᑎᑐᑦ" },
ja: { name: "Japanese", native: "日本語" },
jv: { name: "Javanese", native: "Basa Jawa" },
ka: { name: "Georgian", native: "ქართული" },
kg: { name: "Kongo", native: "KiKongo" },
ki: { name: "Kikuyu", native: "Gĩkũyũ" },
kj: { name: "Kuanyama", native: "Kuanyama" },
kk: { name: "Kazakh", native: "Қазақша" },
kl: { name: "Greenlandic", native: "Kalaallisut" },
km: { name: "Cambodian", native: "ភាសាខ្មែរ" },
kn: { name: "Kannada", native: "ಕನ್ನಡ" },
ko: { name: "Korean", native: "한국어" },
kr: { name: "Kanuri", native: "Kanuri" },
ks: { name: "Kashmiri", native: "कश्मीरी / كشميري" },
ku: { name: "Kurdish", native: "Kurdî / كوردی" },
kv: { name: "Komi", native: "Коми" },
kw: { name: "Cornish", native: "Kernewek" },
ky: { name: "Kyrgyz", native: "Кыргызча" },
la: { name: "Latin", native: "Latina" },
lb: { name: "Luxembourgish", native: "Lëtzebuergesch" },
lg: { name: "Ganda", native: "Luganda" },
li: { name: "Limburgian", native: "Limburgs" },
ln: { name: "Lingala", native: "Lingála" },
lo: { name: "Laotian", native: "ລາວ / Pha xa lao" },
lt: { name: "Lithuanian", native: "Lietuvių" },
lu: { name: "Luba-Katanga", native: "Tshiluba" },
lv: { name: "Latvian", native: "Latviešu" },
mg: { name: "Malagasy", native: "Malagasy" },
mh: { name: "Marshallese", native: "Kajin Majel / Ebon" },
mi: { name: "Maori", native: "Māori" },
mk: { name: "Macedonian", native: "Македонски" },
ml: { name: "Malayalam", native: "മലയാളം" },
mn: { name: "Mongolian", native: "Монгол" },
mo: { name: "Moldovan", native: "Moldovenească" },
mr: { name: "Marathi", native: "मराठी" },
ms: { name: "Malay", native: "Bahasa Melayu" },
mt: { name: "Maltese", native: "bil-Malti" },
my: { name: "Burmese", native: "မြန်မာစာ" },
na: { name: "Nauruan", native: "Dorerin Naoero" },
nb: { name: "Norwegian Bokmål", native: "Norsk bokmål" },
nd: { name: "North Ndebele", native: "Sindebele" },
ne: { name: "Nepali", native: "नेपाली" },
ng: { name: "Ndonga", native: "Oshiwambo" },
nl: { name: "Dutch", native: "Nederlands" },
nn: { name: "Norwegian Nynorsk", native: "Norsk nynorsk" },
no: { name: "Norwegian", native: "Norsk" },
nr: { name: "South Ndebele", native: "isiNdebele" },
nv: { name: "Navajo", native: "Diné bizaad" },
ny: { name: "Chichewa", native: "Chi-Chewa" },
oc: { name: "Occitan", native: "Occitan" },
oj: { name: "Ojibwa", native: "ᐊᓂᔑᓈᐯᒧᐎᓐ / Anishinaabemowin" },
om: { name: "Oromo", native: "Oromoo" },
or: { name: "Oriya", native: "ଓଡ଼ିଆ" },
os: { name: "Ossetian / Ossetic", native: "Иронау" },
pa: { name: "Panjabi / Punjabi", native: "ਪੰਜਾਬੀ / पंजाबी / پنجابي" },
pi: { name: "Pali", native: "Pāli / पाऴि" },
pl: { name: "Polish", native: "Polski" },
ps: { name: "Pashto", native: "پښتو" },
pt: { name: "Portuguese", native: "Português" },
qu: { name: "Quechua", native: "Runa Simi" },
rm: { name: "Raeto Romance", native: "Rumantsch" },
rn: { name: "Kirundi", native: "Kirundi" },
ro: { name: "Romanian", native: "Română" },
ru: { name: "Russian", native: "Русский" },
rw: { name: "Rwandi", native: "Kinyarwandi" },
sa: { name: "Sanskrit", native: "संस्कृतम्" },
sc: { name: "Sardinian", native: "Sardu" },
sd: { name: "Sindhi", native: "सिनधि" },
se: { name: "Northern Sami", native: "Sámegiella" },
sg: { name: "Sango", native: "Sängö" },
sh: { name: "Serbo-Croatian", native: "Srpskohrvatski / Српскохрватски" },
si: { name: "Sinhalese", native: "සිංහල" },
sk: { name: "Slovak", native: "Slovenčina" },
sl: { name: "Slovenian", native: "Slovenščina" },
sm: { name: "Samoan", native: "Gagana Samoa" },
sn: { name: "Shona", native: "chiShona" },
so: { name: "Somalia", native: "Soomaaliga" },
sq: { name: "Albanian", native: "Shqip" },
sr: { name: "Serbian", native: "Српски" },
ss: { name: "Swati", native: "SiSwati" },
st: { name: "Southern Sotho", native: "Sesotho" },
su: { name: "Sundanese", native: "Basa Sunda" },
sv: { name: "Swedish", native: "Svenska" },
sw: { name: "Swahili", native: "Kiswahili" },
ta: { name: "Tamil", native: "தமிழ்" },
te: { name: "Telugu", native: "తెలుగు" },
tg: { name: "Tajik", native: "Тоҷикӣ" },
th: { name: "Thai", native: "ไทย / Phasa Thai" },
ti: { name: "Tigrinya", native: "ትግርኛ" },
tk: { name: "Turkmen", native: "Туркмен / تركمن" },
tl: { name: "Tagalog / Filipino", native: "Tagalog" },
tn: { name: "Tswana", native: "Setswana" },
to: { name: "Tonga", native: "Lea Faka-Tonga" },
tr: { name: "Turkish", native: "Türkçe" },
ts: { name: "Tsonga", native: "Xitsonga" },
tt: { name: "Tatar", native: "Tatarça" },
tw: { name: "Twi", native: "Twi" },
ty: { name: "Tahitian", native: "Reo Mā`ohi" },
ug: { name: "Uyghur", native: "Uyƣurqə / ئۇيغۇرچە" },
uk: { name: "Ukrainian", native: "Українська" },
ur: { name: "Urdu", native: "اردو" },
uz: { name: "Uzbek", native: "Ўзбек" },
ve: { name: "Venda", native: "Tshivenḓa" },
vi: { name: "Vietnamese", native: "Tiếng Việt" },
vo: { name: "Volapük", native: "Volapük" },
wa: { name: "Walloon", native: "Walon" },
wo: { name: "Wolof", native: "Wollof" },
xh: { name: "Xhosa", native: "isiXhosa" },
yi: { name: "Yiddish", native: "ייִדיש" },
yo: { name: "Yoruba", native: "Yorùbá" },
za: { name: "Zhuang", native: "Cuengh / Tôô / 壮语" },
zh: { name: "Chinese", native: "中文" },
zu: { name: "Zulu", native: "isiZulu" },
} as const;
export const RTL_LANGUAGES: readonly string[] = [
"ar",
"az",
"ba",
"dv",
"fa",
"he",
"iu",
"ks",
"ku",
"ps",
"sd",
"sr",
"tg",
"ur",
"yi",
] as const;

View File

@@ -0,0 +1,119 @@
# languages
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides typed lookup functions over the static ISO 639-1 language dataset (130+ entries),
including right-to-left (RTL) detection for Arabic, Hebrew, Persian, Urdu, and other scripts.
The module is designed for zero-allocation read access — data is held in a frozen constant and
all lookups return in O(1) or O(n) time with no I/O. Every `getLanguage` call emits an OTel
counter increment, enabling observability of lookup patterns and invalid code injection attempts
in production environments.
## Features
- **`getLanguages()`** — Returns the full `Record<string, LanguageItem>` reference in O(1); no
copy allocation.
- **`getLanguage(code)`** — O(1) lookup by ISO 639-1 code; throws a typed `LocaleDataError` for
unknown codes.
- **`getRtlLanguages()`** — Returns the readonly list of RTL language codes (15 codes).
- **`isRtlLanguage(code)`** — Returns `true` / `false` for any language code or BCP-47 tag;
strips region and script subtags before checking.
- **OTel instrumentation** — `locale_data.language.lookups` counter with `result: "found" | "not_found"`.
- **Zod schemas** — `LanguageCodeSchema` (2–3 char lowercase) and re-exported `LanguageItemSchema`.
## Usage: Basic Example
```ts
import { getLanguage, getLanguages } from "@suaveplan/locale-data/languages";
// Look up a single language
const fr = getLanguage("fr");
console.log(fr.name); // "French"
console.log(fr.native); // "Français"
// Get all languages
const all = getLanguages();
console.log(Object.keys(all).length); // 130+
```
## Usage: Advanced Example
```ts
import {
getLanguage,
getRtlLanguages,
isRtlLanguage,
} from "@suaveplan/locale-data/languages";
import { LocaleDataError, LOCALE_DATA_CODES } from "@suaveplan/locale-data/errors";
// Error handling for unknown codes
try {
const lang = getLanguage("xx");
} catch (err) {
if (err instanceof LocaleDataError && err.code === LOCALE_DATA_CODES["LANGUAGE.NOT_FOUND"]) {
console.warn("Unknown language code");
}
}
// RTL detection with BCP-47 tags
const isArabicRtl = isRtlLanguage("ar-SA"); // true
const isFrenchRtl = isRtlLanguage("fr-FR"); // false
// Get all RTL language codes
const rtlCodes = getRtlLanguages();
console.log(rtlCodes); // ["ar", "az", "ba", "dv", "fa", "he", ...]
// Build a map of RTL languages with their names
import { getLanguages } from "@suaveplan/locale-data/languages";
const allLangs = getLanguages();
const rtlLanguages = getRtlLanguages().map((code) => ({
code,
...allLangs[code],
}));
```
## API Reference
### `getLanguages(): Record<string, LanguageItem>`
Return the full language map.
- **Returns:** `Record<string, LanguageItem>` — the same reference each call.
- **Note:** Do not mutate the returned object.
### `getLanguage(code: string): LanguageItem`
Look up a language by its ISO 639-1 code.
- **Parameters:** `code` — two-letter ISO 639-1 code (e.g. `"en"`, `"fr"`, `"ar"`).
- **Returns:** The matching `LanguageItem` with `name` and `native` properties.
- **Throws:** `LocaleDataError` (`LOCALE_DATA/LANGUAGE.NOT_FOUND`) when the code is not found.
- **OTel:** Increments `locale_data.language.lookups` with `result: "found"` or `"not_found"`.
### `getRtlLanguages(): readonly string[]`
Return the list of RTL language codes.
- **Returns:** A readonly array of ISO 639-1 codes that use right-to-left script.
- **Included codes:** `ar`, `az`, `ba`, `dv`, `fa`, `he`, `iu`, `ks`, `ku`, `ps`, `sd`,
`sr`, `tg`, `ur`, `yi`.
### `isRtlLanguage(languageCode: string): boolean`
Determine whether a language code represents a RTL language.
- **Parameters:** `languageCode` — ISO 639-1 code or BCP-47 tag (e.g. `"ar"`, `"ar-SA"`,
`"fa_IR"`).
- **Returns:** `true` if the primary language subtag is in the RTL list, `false` otherwise.
- **Note:** Always returns `false` for unknown codes; never throws.
## Implementation Notes
Data is stored in `languages.data.ts` as plain `export const` declarations to keep the
implementation file under the 300-line hard limit. The data file is excluded from coverage
measurement because it contains no executable logic. The `isRtlLanguage` function normalises
BCP-47 tags by lowercasing and splitting on `_` or `-` before checking the primary subtag,
matching the same behaviour as the reference implementation in the original project.

View File

@@ -0,0 +1,8 @@
import z from "zod";
export { LanguageItemSchema } from "../types/index.js";
/**
* Zod schema for a two-character ISO 639-1 language code.
*/
export const LanguageCodeSchema = z.string().min(2).max(3).toLowerCase();

View File

@@ -0,0 +1,113 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import {
getLanguage,
getLanguages,
getRtlLanguages,
isRtlLanguage,
} from "./languages.js";
describe("getLanguages", () => {
it("returns all languages", { timeout: 5000 }, () => {
const langs = getLanguages();
expect(Object.keys(langs).length).toBeGreaterThan(100);
expect(langs["en"].name).toBe("English");
});
it("returns the same reference each call", { timeout: 5000 }, () => {
const a = getLanguages();
const b = getLanguages();
expect(a).toBe(b);
});
it("includes common languages", { timeout: 5000 }, () => {
const langs = getLanguages();
expect(langs["fr"].name).toBe("French");
expect(langs["de"].name).toBe("German");
expect(langs["zh"].name).toBe("Chinese");
expect(langs["ar"].name).toBe("Arabic");
});
it("includes native names", { timeout: 5000 }, () => {
const langs = getLanguages();
expect(langs["de"].native).toBe("Deutsch");
expect(langs["fr"].native).toBe("Français");
});
});
describe("getLanguage", () => {
it("returns a known language", { timeout: 5000 }, () => {
expect(getLanguage("fr").name).toBe("French");
});
it("returns English with correct native name", { timeout: 5000 }, () => {
const en = getLanguage("en");
expect(en.name).toBe("English");
expect(en.native).toBe("English");
});
it("returns Arabic with correct native name", { timeout: 5000 }, () => {
const ar = getLanguage("ar");
expect(ar.name).toBe("Arabic");
expect(ar.native).toBe("العربية");
});
it("throws for unknown language code", { timeout: 5000 }, () => {
expect(() => getLanguage("xx")).toThrow();
});
it("throws with code in message for unknown language", {
timeout: 5000,
}, () => {
expect(() => getLanguage("zz")).toThrow(/zz/);
});
});
describe("getRtlLanguages", () => {
it("includes Arabic, Hebrew, Persian, Urdu", { timeout: 5000 }, () => {
const rtl = getRtlLanguages();
for (const code of ["ar", "he", "fa", "ur"]) {
expect(rtl).toContain(code);
}
});
it("all returned codes exist in language map", { timeout: 5000 }, () => {
const all = getLanguages();
for (const code of getRtlLanguages()) {
expect(code in all).toBe(true);
}
});
it("returns a non-empty array", { timeout: 5000 }, () => {
expect(getRtlLanguages().length).toBeGreaterThan(5);
});
});
describe("isRtlLanguage", () => {
it("returns true for Arabic", { timeout: 5000 }, () => {
expect(isRtlLanguage("ar")).toBe(true);
});
it("returns true for Hebrew", { timeout: 5000 }, () => {
expect(isRtlLanguage("he")).toBe(true);
});
it("returns true for Persian", { timeout: 5000 }, () => {
expect(isRtlLanguage("fa")).toBe(true);
});
it("returns true for Urdu", { timeout: 5000 }, () => {
expect(isRtlLanguage("ur")).toBe(true);
});
it("returns false for English", { timeout: 5000 }, () => {
expect(isRtlLanguage("en")).toBe(false);
});
it("returns false for French", { timeout: 5000 }, () => {
expect(isRtlLanguage("fr")).toBe(false);
});
it("returns false for unknown code", { timeout: 5000 }, () => {
expect(isRtlLanguage("xx")).toBe(false);
});
});

View File

@@ -0,0 +1,74 @@
import { metrics } from "@opentelemetry/api";
import { LOCALE_DATA_CODES, LocaleDataError } from "../errors/index.js";
import type { LanguageItem } from "../types/index.js";
import { LANGUAGES_DATA, RTL_LANGUAGES } from "./languages.data.js";
const meter = metrics.getMeter("@suaveplan/locale-data");
const lookupCounter = meter.createCounter("locale_data.language.lookups", {
description: "Number of language lookups by result",
});
/**
* Return the full ISO 639-1 language data record.
*
* Returns the same reference on each call — do not mutate.
*
* @returns A `Record<string, LanguageItem>` keyed by ISO 639-1 language code.
*/
export function getLanguages(): Record<string, LanguageItem> {
return LANGUAGES_DATA;
}
/**
* Look up a language by its ISO 639-1 code.
*
* Increments the `locale_data.language.lookups` OTel counter with
* `result: "found"` or `result: "not_found"`.
*
* @param code - ISO 639-1 language code (e.g. `"en"`, `"fr"`, `"ar"`).
* @returns The matching `LanguageItem`.
* @throws `LocaleDataError` with code `LOCALE_DATA/LANGUAGE.NOT_FOUND` when
* the code is not in the dataset.
*/
export function getLanguage(code: string): LanguageItem {
const entry = LANGUAGES_DATA[code];
if (!entry) {
lookupCounter.add(1, { result: "not_found" });
throw new LocaleDataError(
LOCALE_DATA_CODES["LANGUAGE.NOT_FOUND"],
`No language found for ISO code: ${code}`
);
}
lookupCounter.add(1, { result: "found" });
return entry;
}
/**
* Return the list of ISO 639-1 codes that use right-to-left script.
*
* Includes: `ar` (Arabic), `az` (Azerbaijani), `ba` (Bashkir), `dv` (Divehi),
* `fa` (Persian), `he` (Hebrew), `iu` (Inuktitut), `ks` (Kashmiri),
* `ku` (Kurdish), `ps` (Pashto), `sd` (Sindhi), `sr` (Serbian),
* `tg` (Tajik), `ur` (Urdu), `yi` (Yiddish).
*
* @returns A readonly array of RTL language codes.
*/
export function getRtlLanguages(): readonly string[] {
return RTL_LANGUAGES;
}
/**
* Determine whether a language code represents a right-to-left language.
*
* Accepts BCP-47 tags with region/script subtags (e.g. `"ar-SA"`, `"fa_IR"`);
* only the primary language subtag is checked.
*
* @param languageCode - ISO 639-1 code or BCP-47 tag.
* @returns `true` if the primary language is RTL, `false` otherwise.
*/
export function isRtlLanguage(languageCode: string): boolean {
// split always returns at least one element for a non-empty string
const parts = languageCode.toLowerCase().split(/[_-]/);
const primary = parts[0] as string;
return (RTL_LANGUAGES as readonly string[]).includes(primary);
}

View File

@@ -0,0 +1,8 @@
export type {
CountryItem,
LanguageItem,
} from "./types.js";
export {
CountryItemSchema,
LanguageItemSchema,
} from "./types.js";

View File

@@ -0,0 +1,112 @@
# types
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the shared TypeScript types and Zod validation schemas used throughout `@suaveplan/locale-data`.
All runtime type shapes — `CountryItem` and `LanguageItem` — are declared here as Zod schemas, and
their inferred TypeScript types are derived via `z.infer`. This single-source-of-truth approach
ensures that runtime validation and static typing are always in sync, eliminating drift between
schema and type definitions across the package.
## Features
- **`CountryItemSchema`** — Zod object schema for a country entry with 9 fields: `name`, `native`,
`phone`, `continent`, `capital`, `currency`, `languages` (array), `emoji`, and `emojiU`.
- **`CountryItem`** — TypeScript type inferred from `CountryItemSchema`; used throughout the
countries module for typed lookups and filter results.
- **`LanguageItemSchema`** — Zod object schema for a language entry with 2 fields: `name` and
`native`.
- **`LanguageItem`** — TypeScript type inferred from `LanguageItemSchema`; used throughout the
languages module for typed lookups and RTL detection.
- **Zero runtime dependencies beyond Zod** — types are pure schema definitions with no side effects.
## Usage: Basic Example
```ts
import { CountryItemSchema, type CountryItem } from "@suaveplan/locale-data";
// Validate an unknown object as a CountryItem
const result = CountryItemSchema.safeParse(someObject);
if (result.success) {
const country: CountryItem = result.data;
console.log(country.name, country.emoji);
}
```
## Usage: Advanced Example
```ts
import { LanguageItemSchema, type LanguageItem } from "@suaveplan/locale-data";
// Parse and transform language data from an external API
const raw = await fetchLanguageFromApi("fr");
const language = LanguageItemSchema.parse(raw) satisfies LanguageItem;
console.log(language.name); // "French"
console.log(language.native); // "Français"
// Use with Zod's record schema for batch validation
import z from "zod";
import { CountryItemSchema } from "@suaveplan/locale-data";
const CountryMapSchema = z.record(z.string().length(2), CountryItemSchema);
const validated = CountryMapSchema.parse(externalData);
```
## API Reference
### `CountryItemSchema`
```ts
const CountryItemSchema: z.ZodObject<{
name: z.ZodString;
native: z.ZodString;
phone: z.ZodString;
continent: z.ZodString;
capital: z.ZodString;
currency: z.ZodString;
languages: z.ZodArray<z.ZodString>;
emoji: z.ZodString;
emojiU: z.ZodString;
}>;
```
Zod schema for a country entry. All fields are required strings except `languages` which is an
array of ISO 639-1 language codes.
### `CountryItem`
```ts
type CountryItem = z.infer<typeof CountryItemSchema>;
```
Static TypeScript type for a country record derived from `CountryItemSchema`.
### `LanguageItemSchema`
```ts
const LanguageItemSchema: z.ZodObject<{
name: z.ZodString;
native: z.ZodString;
}>;
```
Zod schema for a language entry with its English name and native script name.
### `LanguageItem`
```ts
type LanguageItem = z.infer<typeof LanguageItemSchema>;
```
Static TypeScript type for a language record derived from `LanguageItemSchema`.
## Implementation Notes
Schemas are defined using Zod's `z.object()` constructor, which produces a runtime validator that
can be used for both `parse` (throws on invalid input) and `safeParse` (returns a discriminated
union). The `CountryItem` and `LanguageItem` types are intentionally inferred rather than
hand-written to prevent drift. The `languages` field in `CountryItem` uses `z.array(z.string())`
because some countries have zero, one, or multiple official languages (e.g. South Africa has 11).

View File

@@ -0,0 +1,22 @@
import z from "zod";
export const CountryItemSchema = z.object({
name: z.string(),
native: z.string(),
phone: z.string(),
continent: z.string(),
capital: z.string(),
currency: z.string(),
languages: z.array(z.string()),
emoji: z.string(),
emojiU: z.string(),
});
export type CountryItem = z.infer<typeof CountryItemSchema>;
export const LanguageItemSchema = z.object({
name: z.string(),
native: z.string(),
});
export type LanguageItem = z.infer<typeof LanguageItemSchema>;

View File

@@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"lib": ["ES2022"],
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["src/**/*.test.ts", "dist", "node_modules"]
}

View File

@@ -0,0 +1,37 @@
import { defineConfig } from "vite";
import dts from "vite-plugin-dts";
export default defineConfig({
root: __dirname,
cacheDir: "../../../node_modules/.vite/packages/locale-data",
plugins: [
dts({
entryRoot: "src",
tsconfigPath: "./tsconfig.lib.json",
insertTypesEntry: true,
}),
],
build: {
outDir: "./dist",
emptyOutDir: true,
reportCompressedSize: true,
sourcemap: true,
lib: { entry: "src/index.ts", formats: ["es"] },
rollupOptions: {
treeshake: true,
output: {
preserveModules: true,
preserveModulesRoot: "src",
entryFileNames: "[name].js",
},
external: [
"@suaveplan/error",
"@suaveplan/types",
"@opentelemetry/api",
"tslib",
"zod",
/^node:/,
],
},
},
});

View File

@@ -0,0 +1,32 @@
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: false,
environment: "node",
pool: "forks",
include: ["src/**/*.test.ts"],
exclude: ["dist/**", "node_modules/**"],
coverage: {
provider: "istanbul",
reporter: ["text", "html", "lcov"],
include: ["src/**/*.ts"],
exclude: [
"src/**/*.test.ts",
"src/**/*.types.ts",
"src/**/*.schemas.ts",
"src/**/*.data.ts",
"src/**/index.ts",
"src/types/**",
"tests/**",
],
thresholds: {
statements: 100,
branches: 100,
functions: 100,
lines: 100,
},
},
testTimeout: 5000,
},
});