Skip to content

Commit 40fa69d

Browse files
feat: locales package (#1124)
* chore: added parser functions * feat: implemented async functions * chore: switched to cdn based imports from github * feat: validation functions * chore: implement recommended fixes for locales package * chore: fixed package name * chore: ts error fixes * revert: remove changes from files outside packages/locales * chore: updated pnpm dependancies * chore: added changeset * chore(review): added changes based on the reviews * chore(review): updated package.json and tsconfig.json * chore(review): fixed license in readme * chore(fix): fixed pnpm-lock file --------- Co-authored-by: Max Prilutskiy <[email protected]>
1 parent 6fcf83e commit 40fa69d

File tree

17 files changed

+2688
-2
lines changed

17 files changed

+2688
-2
lines changed

.changeset/real-turkeys-itch.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@lingo.dev/_locales": minor
3+
---
4+
5+
Implemented locales package from #1080
6+
7+
- Added new `@lingo.dev/_locales` package for locale management
8+
- Includes locale name parsing and validation utilities
9+
- Provides integration helpers for various frameworks
10+
- Supports locale code normalization and fallback handling

packages/locales/README.md

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
# @lingo.dev/locales
2+
3+
A JavaScript package that helps developers work with locale codes (like "en-US" or "zh-Hans-CN") and get country/language names in different languages.
4+
5+
## Features
6+
7+
- **Locale Parsing**: Break apart locale strings into language, script, and region components
8+
- **Validation**: Check if locale codes are properly formatted and use real ISO codes
9+
- **Name Resolution**: Get localized names for countries, languages, and scripts in 200+ languages
10+
- **Small Bundle Size**: Core package is ~12KB with on-demand data loading
11+
- **Full TypeScript Support**: Complete type definitions included
12+
13+
## Installation
14+
15+
```bash
16+
npm install @lingo.dev/locales
17+
```
18+
19+
## Usage
20+
21+
### Locale Parsing
22+
23+
```typescript
24+
import {
25+
parseLocale,
26+
getLanguageCode,
27+
getScriptCode,
28+
getRegionCode,
29+
} from "@lingo.dev/locales";
30+
31+
// Parse complete locale
32+
parseLocale("en-US"); // { language: "en", region: "US" }
33+
parseLocale("zh-Hans-CN"); // { language: "zh", script: "Hans", region: "CN" }
34+
parseLocale("sr-Cyrl-RS"); // { language: "sr", script: "Cyrl", region: "RS" }
35+
36+
// Extract individual components
37+
getLanguageCode("en-US"); // "en"
38+
getScriptCode("zh-Hans-CN"); // "Hans"
39+
getRegionCode("en-US"); // "US"
40+
```
41+
42+
### Validation
43+
44+
```typescript
45+
import {
46+
isValidLocale,
47+
isValidLanguageCode,
48+
isValidScriptCode,
49+
isValidRegionCode,
50+
} from "@lingo.dev/locales";
51+
52+
// Validate complete locales
53+
isValidLocale("en-US"); // true
54+
isValidLocale("en-FAKE"); // false
55+
isValidLocale("xyz-US"); // false
56+
57+
// Validate individual components
58+
isValidLanguageCode("en"); // true
59+
isValidLanguageCode("xyz"); // false
60+
isValidScriptCode("Hans"); // true
61+
isValidScriptCode("Fake"); // false
62+
isValidRegionCode("US"); // true
63+
isValidRegionCode("ZZ"); // false
64+
```
65+
66+
### Name Resolution (Async)
67+
68+
```typescript
69+
import {
70+
getCountryName,
71+
getLanguageName,
72+
getScriptName,
73+
} from "@lingo.dev/locales";
74+
75+
// Get country names in different languages
76+
await getCountryName("US"); // "United States"
77+
await getCountryName("US", "es"); // "Estados Unidos"
78+
await getCountryName("CN", "fr"); // "Chine"
79+
80+
// Get language names in different languages
81+
await getLanguageName("en"); // "English"
82+
await getLanguageName("en", "es"); // "inglés"
83+
await getLanguageName("zh", "fr"); // "chinois"
84+
85+
// Get script names in different languages
86+
await getScriptName("Hans"); // "Simplified Han"
87+
await getScriptName("Hans", "es"); // "han simplificado"
88+
await getScriptName("Latn", "zh"); // "拉丁文"
89+
```
90+
91+
## API Reference
92+
93+
### Parsing Functions
94+
95+
#### `parseLocale(locale: string): LocaleComponents`
96+
97+
Breaks apart a locale string into its components.
98+
99+
**Parameters:**
100+
101+
- `locale` (string): The locale string to parse
102+
103+
**Returns:** `LocaleComponents` object with `language`, `script`, and `region` properties
104+
105+
**Examples:**
106+
107+
```typescript
108+
parseLocale("en-US"); // { language: "en", region: "US" }
109+
parseLocale("zh-Hans-CN"); // { language: "zh", script: "Hans", region: "CN" }
110+
parseLocale("es"); // { language: "es" }
111+
```
112+
113+
#### `getLanguageCode(locale: string): string`
114+
115+
Extracts just the language part from a locale string.
116+
117+
#### `getScriptCode(locale: string): string | null`
118+
119+
Extracts the script part from a locale string.
120+
121+
#### `getRegionCode(locale: string): string | null`
122+
123+
Extracts the region/country part from a locale string.
124+
125+
### Validation Functions
126+
127+
#### `isValidLocale(locale: string): boolean`
128+
129+
Checks if a locale string is properly formatted and uses real codes.
130+
131+
#### `isValidLanguageCode(code: string): boolean`
132+
133+
Checks if a language code is valid (ISO 639-1).
134+
135+
#### `isValidScriptCode(code: string): boolean`
136+
137+
Checks if a script code is valid (ISO 15924).
138+
139+
#### `isValidRegionCode(code: string): boolean`
140+
141+
Checks if a region code is valid (ISO 3166-1 alpha-2 or UN M.49).
142+
143+
### Name Resolution Functions
144+
145+
#### `getCountryName(countryCode: string, displayLanguage = "en"): Promise<string>`
146+
147+
Gets a country name in the specified language.
148+
149+
**Parameters:**
150+
151+
- `countryCode` (string): The country code (e.g., "US", "CN")
152+
- `displayLanguage` (string, optional): The language to display the name in (default: "en")
153+
154+
**Returns:** Promise<string> - The localized country name
155+
156+
#### `getLanguageName(languageCode: string, displayLanguage = "en"): Promise<string>`
157+
158+
Gets a language name in the specified language.
159+
160+
#### `getScriptName(scriptCode: string, displayLanguage = "en"): Promise<string>`
161+
162+
Gets a script name in the specified language.
163+
164+
## Supported Formats
165+
166+
The package supports both hyphen (`-`) and underscore (`_`) delimiters:
167+
168+
- `en-US` or `en_US``{ language: "en", region: "US" }`
169+
- `zh-Hans-CN` or `zh_Hans_CN``{ language: "zh", script: "Hans", region: "CN" }`
170+
171+
## Data Sources
172+
173+
- **Locale parsing**: Uses regex-based parsing with ISO standard validation
174+
- **Name resolution**: Uses Unicode CLDR (Common Locale Data Repository) data
175+
- **Validation**: Uses official ISO 639-1, ISO 15924, and ISO 3166-1 standards
176+
177+
## Performance
178+
179+
- **Bundle size**: Core package is ~12KB (ESM) / ~14KB (CJS)
180+
- **Runtime data**: Loaded on-demand from GitHub raw URLs
181+
- **Caching**: In-memory cache to avoid repeated network requests
182+
- **Fallback**: Graceful degradation to English when language data is unavailable
183+
184+
## Error Handling
185+
186+
All functions include comprehensive error handling:
187+
188+
```typescript
189+
try {
190+
parseLocale("invalid");
191+
} catch (error) {
192+
console.log(error.message); // "Invalid locale format: invalid"
193+
}
194+
195+
try {
196+
await getCountryName("XX");
197+
} catch (error) {
198+
console.log(error.message); // "Country code "XX" not found"
199+
}
200+
```
201+
202+
## TypeScript Support
203+
204+
Full TypeScript support with comprehensive type definitions:
205+
206+
```typescript
207+
interface LocaleComponents {
208+
language: string;
209+
script?: string;
210+
region?: string;
211+
}
212+
213+
type LocaleDelimiter = "-" | "_";
214+
215+
interface ParseResult {
216+
components: LocaleComponents;
217+
delimiter: LocaleDelimiter | null;
218+
isValid: boolean;
219+
error?: string;
220+
}
221+
```
222+
223+
## License
224+
225+
Apache 2.0

packages/locales/package.json

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"name": "@lingo.dev/_locales",
3+
"version": "0.0.1",
4+
"description": "Lingo.dev locales",
5+
"private": false,
6+
"publishConfig": {
7+
"access": "public"
8+
},
9+
"type": "module",
10+
"sideEffects": false,
11+
"main": "build/index.cjs",
12+
"module": "build/index.mjs",
13+
"types": "build/index.d.ts",
14+
"files": [
15+
"build",
16+
"localenames-data"
17+
],
18+
"scripts": {
19+
"dev": "tsup --watch",
20+
"build": "pnpm typecheck && tsup",
21+
"typecheck": "tsc --noEmit",
22+
"test": "vitest run",
23+
"test:watch": "vitest"
24+
},
25+
"keywords": [],
26+
"author": "",
27+
"license": "Apache-2.0",
28+
"devDependencies": {
29+
"@types/node": "^22.13.5",
30+
"tsup": "^8.3.5",
31+
"typescript": "^5.8.3",
32+
"vitest": "^3.2.4"
33+
}
34+
}

packages/locales/src/constants.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* Shared constants for locale parsing and validation
3+
*/
4+
5+
/**
6+
* Regular expression for parsing locale strings
7+
*
8+
* This regex is case-sensitive and expects normalized locale strings:
9+
* - Language code: 2-3 lowercase letters (e.g., "en", "zh", "es")
10+
* - Script code: 4 letters with preserved case (e.g., "Hans", "hans", "Cyrl")
11+
* - Region code: 2-3 uppercase letters or digits (e.g., "US", "CN", "123")
12+
*
13+
* Matches locale strings in the format: language[-_]script?[-_]region?
14+
*
15+
* Groups:
16+
* 1. Language code (2-3 lowercase letters)
17+
* 2. Script code (4 letters, optional)
18+
* 3. Region code (2-3 letters or digits, optional)
19+
*
20+
* Examples:
21+
* - "en" -> language: "en"
22+
* - "en-US" -> language: "en", region: "US"
23+
* - "zh-Hans-CN" -> language: "zh", script: "Hans", region: "CN"
24+
* - "sr_Cyrl_RS" -> language: "sr", script: "Cyrl", region: "RS"
25+
*
26+
* Note: The parser automatically normalizes case before applying this regex:
27+
* - Language codes are converted to lowercase
28+
* - Script codes preserve their original case
29+
* - Region codes are converted to uppercase
30+
*/
31+
export const LOCALE_REGEX =
32+
/^([a-z]{2,3})(?:[-_]([A-Za-z]{4}))?(?:[-_]([A-Z]{2}|[0-9]{3}))?$/;

packages/locales/src/index.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Export types
2+
export type { LocaleComponents, LocaleDelimiter, ParseResult } from "./types";
3+
4+
// Export constants
5+
export { LOCALE_REGEX } from "./constants";
6+
7+
// Export parsing functions
8+
export {
9+
parseLocale,
10+
parseLocaleWithDetails,
11+
getLanguageCode,
12+
getScriptCode,
13+
getRegionCode,
14+
} from "./parser";
15+
16+
// Export validation functions
17+
export {
18+
isValidLocale,
19+
isValidLanguageCode,
20+
isValidScriptCode,
21+
isValidRegionCode,
22+
} from "./validation";
23+
24+
// Export async name resolution functions
25+
export { getCountryName, getLanguageName, getScriptName } from "./names";

0 commit comments

Comments
 (0)