Initial Commit
This commit is contained in:
122
packages/localization/i18n/locale-data/README.md
Normal file
122
packages/localization/i18n/locale-data/README.md
Normal 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"` |
|
||||
|
||||
80
packages/localization/i18n/locale-data/package.json
Normal file
80
packages/localization/i18n/locale-data/package.json
Normal 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"
|
||||
]
|
||||
}
|
||||
@@ -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
@@ -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"`).
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export {
|
||||
getAllCountries,
|
||||
getCountriesByContinent,
|
||||
getCountriesByCurrency,
|
||||
getCountry,
|
||||
} from "./countries.js";
|
||||
export { CountryCodeSchema, CountryItemSchema } from "./countries.schemas.js";
|
||||
@@ -0,0 +1,5 @@
|
||||
export {
|
||||
LOCALE_DATA_CODES,
|
||||
type LocaleDataCode,
|
||||
LocaleDataError,
|
||||
} from "./locale-data-error.js";
|
||||
@@ -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`.
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
4
packages/localization/i18n/locale-data/src/index.ts
Normal file
4
packages/localization/i18n/locale-data/src/index.ts
Normal 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";
|
||||
@@ -0,0 +1,7 @@
|
||||
export {
|
||||
getLanguage,
|
||||
getLanguages,
|
||||
getRtlLanguages,
|
||||
isRtlLanguage,
|
||||
} from "./languages.js";
|
||||
export { LanguageCodeSchema, LanguageItemSchema } from "./languages.schemas.js";
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
@@ -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.
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export type {
|
||||
CountryItem,
|
||||
LanguageItem,
|
||||
} from "./types.js";
|
||||
export {
|
||||
CountryItemSchema,
|
||||
LanguageItemSchema,
|
||||
} from "./types.js";
|
||||
112
packages/localization/i18n/locale-data/src/types/types.md
Normal file
112
packages/localization/i18n/locale-data/src/types/types.md
Normal 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).
|
||||
22
packages/localization/i18n/locale-data/src/types/types.ts
Normal file
22
packages/localization/i18n/locale-data/src/types/types.ts
Normal 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>;
|
||||
12
packages/localization/i18n/locale-data/tsconfig.lib.json
Normal file
12
packages/localization/i18n/locale-data/tsconfig.lib.json
Normal 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"]
|
||||
}
|
||||
37
packages/localization/i18n/locale-data/vite.config.ts
Normal file
37
packages/localization/i18n/locale-data/vite.config.ts
Normal 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:/,
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
32
packages/localization/i18n/locale-data/vitest.config.ts
Normal file
32
packages/localization/i18n/locale-data/vitest.config.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user