Skip to content

Commit

Permalink
feat: introduce international-types (#20)
Browse files Browse the repository at this point in the history
* feat: introduce international-types

* chore(docs): mention international-types in the README

* chore(docs): add international-types README

* refactor: import type instead of import

* chore(docs): replace relative to absolute links

* fix: correct files

* chore: bump to 0.1.1
  • Loading branch information
QuiiBz authored Aug 2, 2022
1 parent ccb3973 commit 6eab0e6
Show file tree
Hide file tree
Showing 14 changed files with 201 additions and 51 deletions.
6 changes: 3 additions & 3 deletions examples/next/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "next-i18n",
"version": "0.1.0",
"name": "example-next",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "next dev",
Expand All @@ -10,7 +10,7 @@
},
"dependencies": {
"next": "12.2.2",
"next-international": "workspace:*",
"next-international": "workspace:0.1.1",
"react": "18.2.0",
"react-dom": "18.2.0"
},
Expand Down
105 changes: 105 additions & 0 deletions packages/international-types/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./assets/logo-white.png">
<source media="(prefers-color-scheme: light)" srcset="./assets/logo-black.png" />
<img alt="" height="100px" src="./assets/logo-white.png">
</picture>
<br />
Type-safe internationalization (i18n) utility types
</p>

---

- [Features](#features)
- [Usage](#usage)
- [Type-safe keys](#type-safe-keys)
- [License](#license)

## Features

- **Autocompletion**: For locale keys, scopes and params!
- **Extensible**: Designed to be used with any library

> **Note**: Using Next.js? Check out [next-international](https://github.com/QuiiBz/next-international)!
## Usage

```bash
pnpm install international-types
```

### Type-safe keys

```ts
import type { LocaleKeys } from 'international-types'

type Locale = {
hello: 'Hello'
'hello.world': 'Hello World!'
}

function t<Key extends LocaleKeys<Locale, undefined>>(key: Key) {
// ...
}

t('')
// hello | hello.world
```

### Type-safe scopes with keys

```ts
import type { LocaleKeys, Scopes } from 'international-types'

type Locale = {
hello: 'Hello'
'scope.nested.demo': 'Nested scope'
'scope.nested.another.demo': 'Another nested scope'
}

function scopedT<Scope extends Scopes<Locale>>(scope: Scope) {
function t<Key extends LocaleKeys<Locale, Scope>>(key: Key) {
// ...
}
}

const t = scopedT('')
// scope | scope.nested

t('')
// For scope: nested.demo | nested.another.demo
// For scope.nested: demo | another.demo
```

### Type-safe params

```ts
import type { LocaleKeys, LocaleValue } from 'international-types'

type Locale = {
param: 'This is a {value}'
'hello.people': 'Hello {name}! You are {age} years old.'
}

function t<
Key extends LocaleKeys<Locale, undefined>,
Value extends LocaleValue = ScopedValue<Locale, undefined, Key>,
>(key: Key, param: ParamsObject<Value>) {
// ...
}

t('param', {
value: ''
// value is required
})

t('hello.people', {
name: '',
age: ''
// name and age are required
})
```

## License

[MIT](./LICENSE)
35 changes: 35 additions & 0 deletions packages/international-types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export type LocaleValue = string | number;
export type BaseLocale = Record<string, LocaleValue>;

export type LocaleKeys<
Locale extends BaseLocale,
Scope extends Scopes<Locale> | undefined,
Key extends string = Extract<keyof Locale, string>,
> = Scope extends undefined ? Key : Key extends `${Scope}.${infer Test}` ? Test : never;

export type Params<Value extends LocaleValue> = Value extends ''
? []
: // eslint-disable-next-line @typescript-eslint/no-unused-vars
Value extends `${infer Head}{${infer Param}}${infer Tail}`
? [Param, ...Params<Tail>]
: [];

export type ParamsObject<Value extends LocaleValue> = Record<Params<Value>[number], LocaleValue>;

export type ExtractScopes<
Value extends string,
Prev extends string | undefined = undefined,
> = Value extends `${infer Head}.${infer Tail}`
? [
Prev extends string ? `${Prev}.${Head}` : Head,
...ExtractScopes<Tail, Prev extends string ? `${Prev}.${Head}` : Head>,
]
: [];

export type Scopes<Locale extends BaseLocale> = ExtractScopes<Extract<keyof Locale, string>>[number];

export type ScopedValue<
Locale extends BaseLocale,
Scope extends Scopes<Locale> | undefined,
Key extends LocaleKeys<Locale, Scope>,
> = Scope extends undefined ? Locale[Key] : Locale[`${Scope}.${Key}`];
27 changes: 27 additions & 0 deletions packages/international-types/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "international-types",
"version": "0.1.1",
"description": "Type-safe internationalization (i18n) utility types",
"types": "dist/index.d.ts",
"keywords": [
"i18n",
"types",
"typescript",
"translate",
"internationalization"
],
"files": [
"dist"
],
"repository": {
"type": "git",
"url": "git+https://github.com/QuiiBz/next-international.git"
},
"bugs": {
"url": "https://github.com/QuiiBz/next-international/issues"
},
"homepage": "https://github.com/QuiiBz/next-international#readme",
"scripts": {
"build": "tsc --declaration --emitDeclarationOnly --outDir dist index.ts"
}
}
11 changes: 9 additions & 2 deletions packages/next-international/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="./assets/logo-white.png">
<source media="(prefers-color-scheme: light)" srcset="./assets/logo-black.png" />
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/QuiiBz/next-international/blob/feat/introduce-international-types/assets/logo-white.png">
<source media="(prefers-color-scheme: light)" srcset="https://github.com/QuiiBz/next-international/blob/feat/introduce-international-types/assets/logo-black.png" />
<img alt="" height="100px" src="./assets/logo-white.png">
</picture>
<br />
Expand All @@ -20,6 +20,7 @@
- [Explicitly typing the locales](#explicitly-typing-the-locales)
- [Load initial locales client-side](#load-initial-locales-client-side)
- [Type-safety on locales files](#type-safety-on-locales-files)
- [Use the types for my own library](#use-the-types-for-my-own-library)
- [License](#license)

## Features
Expand All @@ -29,6 +30,8 @@
- **Simple**: No webpack configuration, no CLI, just pure TypeScript
- **SSR**: Load only the required locale, SSRed

> **Note**: You can now build on top of the types used by next-international using [international-types](https://github.com/QuiiBz/next-international/tree/main/packages/international-types)!
## Usage

```bash
Expand Down Expand Up @@ -260,6 +263,10 @@ export default defineLocale({
})
```

### Use the types for my own library

We also provide a separate package called [international-types](https://github.com/QuiiBz/next-international/tree/main/packages/international-types) that contains the utility types for next-international. You can build a library on top of it and get the same awesome type-safety.

## License

[MIT](./LICENSE)
5 changes: 4 additions & 1 deletion packages/next-international/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "next-international",
"version": "0.1.0",
"version": "0.1.1",
"description": "Type-safe internationalization (i18n) for Next.js",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down Expand Up @@ -34,5 +34,8 @@
"peerDependencies": {
"next": "12.x | 11.x",
"react": "18.x | 17.x"
},
"dependencies": {
"international-types": "0.1.1"
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BaseLocale, LocaleValue } from '../types';
import type { BaseLocale, LocaleValue } from 'international-types';

export function createDefineLocale<Locale extends BaseLocale>() {
return function defineLocale(locale: { [key in keyof Locale]: LocaleValue }) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Locales } from '../types';
import type { Locales } from '../types';
import type { GetStaticProps } from 'next';
import { error } from '../helpers/log';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { Context, ReactElement, ReactNode, useEffect, useState } from 'react';
import { LocaleContext, Locales, BaseLocale } from '../types';
import type { LocaleContext, Locales } from '../types';
import type { BaseLocale } from 'international-types';
import { useRouter } from 'next/router';
import { error, warn } from '../helpers/log';

Expand Down
3 changes: 2 additions & 1 deletion packages/next-international/src/i18n/create-i18n.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { createContext } from 'react';
import type { Locales, BaseLocale, LocaleContext } from '../types';
import type { Locales, LocaleContext } from '../types';
import type { BaseLocale } from 'international-types';
import { createDefineLocale } from './create-define-locale';
import { createGetLocaleStaticProps } from './create-get-locale-static-props';
import { createI18nProvider } from './create-i18n-provider';
Expand Down
6 changes: 3 additions & 3 deletions packages/next-international/src/i18n/create-use-i18n.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import React, { useContext, Context } from 'react';
import {
import type {
BaseLocale,
LocaleContext,
LocaleKeys,
LocaleValue,
Params,
ParamsObject,
ScopedValue,
Scopes,
} from '../types';
} from 'international-types';
import type { LocaleContext } from '../types';

export function createUsei18n<Locale extends BaseLocale>(I18nContext: Context<LocaleContext<Locale> | null>) {
return function useI18n() {
Expand Down
3 changes: 1 addition & 2 deletions packages/next-international/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export { createI18n } from './i18n/create-i18n';
/* c8 ignore next */
export { BaseLocale, LocaleValue } from './types';
export type { BaseLocale, LocaleValue } from 'international-types';
36 changes: 1 addition & 35 deletions packages/next-international/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,8 @@
export type LocaleValue = string | number;
export type BaseLocale = Record<string, LocaleValue>;

export type LocaleKeys<
Locale extends BaseLocale,
Scope extends Scopes<Locale> | undefined,
Key extends string = Extract<keyof Locale, string>,
> = Scope extends undefined ? Key : Key extends `${Scope}.${infer Test}` ? Test : never;
import type { BaseLocale } from 'international-types';

export type Locales = Record<string, () => Promise<any>>;

export type LocaleContext<Locale extends BaseLocale> = {
localeContent: Locale;
fallbackLocale?: Locale;
};

export type Params<Value extends LocaleValue> = Value extends ''
? []
: // eslint-disable-next-line @typescript-eslint/no-unused-vars
Value extends `${infer Head}{${infer Param}}${infer Tail}`
? [Param, ...Params<Tail>]
: [];

export type ParamsObject<Value extends LocaleValue> = Record<Params<Value>[number], LocaleValue>;

export type ExtractScopes<
Value extends string,
Prev extends string | undefined = undefined,
> = Value extends `${infer Head}.${infer Tail}`
? [
Prev extends string ? `${Prev}.${Head}` : Head,
...ExtractScopes<Tail, Prev extends string ? `${Prev}.${Head}` : Head>,
]
: [];

export type Scopes<Locale extends BaseLocale> = ExtractScopes<Extract<keyof Locale, string>>[number];

export type ScopedValue<
Locale extends BaseLocale,
Scope extends Scopes<Locale> | undefined,
Key extends LocaleKeys<Locale, Scope>,
> = Scope extends undefined ? Locale[Key] : Locale[`${Scope}.${Key}`];
8 changes: 7 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 6eab0e6

Please sign in to comment.