Skip to content

Commit

Permalink
feat(i18n): support type-safe consuming translations
Browse files Browse the repository at this point in the history
  • Loading branch information
aralroca committed Oct 7, 2023
1 parent d381d9c commit f9447e1
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,32 @@ Generally a Locale Identifier is made up of a language, region, and script separ
If user locale is `nl-BE` and it is not listed in your configuration, they will be redirected to `nl` if available, or to the default locale otherwise.
If you don't plan to support all regions of a country, it is therefore a good practice to include country locales that will act as fallbacks.

```js filename="src/i18n.js"
```ts filename="src/i18n.ts" switcher
import { I18nConfig } from "brisa";

const i18nConfig: I18nConfig = {
// These are all the locales you want to support in
// your application
locales: ["en-US", "fr", "nl-NL"],
// This is the default locale you want to be used when visiting
// a non-locale prefixed path e.g. `/hello`
defaultLocale: "en-US",
// This is a list of locale domains and the default locale they
// should handle (these are only required when setting up domain routing)
domains: {
"example.com": {
defaultLocale: "en-US",
},
"example.nl": {
defaultLocale: "nl-NL",
},
},
};

export default i18nConfig;
```

```js filename="src/i18n.js" switcher
export default {
// These are all the locales you want to support in
// your application
Expand Down Expand Up @@ -170,14 +195,18 @@ Brisa supports to consume translations inspired by libraries such as [i18next](h
In order to consume translations, you need first to define the `messages` property in `src/i18n.js` file:

```ts filename="src/i18n/index.ts" switcher
import { I18nConfig } from "brisa";

import en from "./messages/en.json";
import es from "./messages/es.json";

export default {
const i18nConfig: I18nConfig<typeof en> = {
defaultLocale: "en",
locales: ["en", "es"],
messages: { en, es },
};

export default i18nConfig;
```

```json filename="src/i18n/messages/en.json" switcher
Expand All @@ -196,6 +225,10 @@ export default {

After this, you can consume translations in every part of your app through the [request context](/docs/building-your-application/data-fetching/request-context): `middleware`, `api` routes, `page` routes, all page components, `responseHeaders`, `layout`, `Head` of each page...

> **Important in TypeScript**: The generic type `<typeof en>` in `I18nConfig` enables type-safe consumption of translations with the `t` function by resolving the keys, keys with plurals and nested keys from the preferred locale. This allows IDE autocompletion and type checking of translation keys throughout the codebase, improving productivity and avoiding translation bugs due to typos or invalid keys.
The generic `I18nConfig<typeof en>` allows you to activate type-safe consuming translations with the `t` function. Displaying to you all the keys from the preferred locale messages, resolving plurals and nested values.

Example in a component:

```tsx filename="src/components/hello.tsx" switcher
Expand Down Expand Up @@ -296,13 +329,34 @@ For example it helps to transform values with decimals, currencies, etc, dependi

Sample adding the `number` format:

```js filename="src/i18n.js"
```ts filename="src/i18n.ts" switcher
import { I18nConfig } from "brisa";

const formatters = {
es: new Intl.NumberFormat("es-ES"),
en: new Intl.NumberFormat("en-EN"),
};

return {
const i18nConfig: I18nConfig = {
// ...
interpolation: {
format: (value, format, lang) => {
if (format === "number") return formatters[lang].format(value);
return value;
},
},
};

export default i18nConfig;
```

```js filename="src/i18n.js" switcher
const formatters = {
es: new Intl.NumberFormat("es-ES"),
en: new Intl.NumberFormat("en-EN"),
};

export default {
// ...
interpolation: {
format: (value, format, lang) => {
Expand Down Expand Up @@ -534,11 +588,19 @@ If omitted or passed as `true` _(By default is `true`)_, it returns an empty str
If passed as `false`, returns the key name itself.

```ts filename="src/i18n/index.ts"
export default {
import { I18nConfig } from "brisa";

import en from './messages/en.json';
import es from './messages/es.json';

const i18nConfig: I18nConfig<typeof en> = {
defaultLocale: "en",
locales: ["en", "es"],
messages: { en, es }
allowEmptyStrings: false,
};

export default i18nConfig;
```

Now `t('hello')` returns `"hello"` instead of an empty string `""`.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"module": "./out/core/index.js",
"main": "./out/core/index.js",
"types": "./out/core/index.d.ts",
"version": "0.0.5",
"version": "0.0.6",
"description": "lightweight and flexible front-end library based on Bun.js for modern web apps",
"license": "MIT",
"type": "module",
Expand Down
8 changes: 7 additions & 1 deletion src/create-brisa-app/create-brisa-app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ echo "{

echo "{
\"compilerOptions\": {
\"baseUrl\": \".\",
\"lib\": [
\"ESNext\"
],
Expand All @@ -56,7 +57,12 @@ echo "{
\"allowJs\": true,
\"types\": [
\"bun-types\" // add Bun global
]
],
// Please, do not modify this path alias configuration.
// It's internally used in Brisa "types.ts" file to enable type-safe
\"paths\": {
\"@/*\": [\"src/*\"],
}
}
}" > tsconfig.json

Expand Down
35 changes: 32 additions & 3 deletions src/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,11 @@ type i18nPages = {
[pageName: string]: Translations;
};

export type I18nConfig = {
export type I18nConfig<T = I18nDictionary> = {
defaultLocale: string;
locales: string[];
domains?: Record<string, I18nDomainConfig>;
messages?: Record<string, I18nDictionary>;
messages?: Record<string, T>;
interpolation?: {
prefix: string;
suffix: string;
Expand All @@ -87,8 +87,37 @@ type RouterType = {
reservedRoutes: Record<string, MatchedRoute | null>;
};

type RemovePlural<Key extends string> = Key extends `${infer Prefix}${
| "_zero"
| "_one"
| "_two"
| "_few"
| "_many"
| "_other"
| `_${number}`}`
? Prefix
: Key;

type Join<S1, S2> = S1 extends string
? S2 extends string
? `${S1}.${S2}`
: never
: never;

export type Paths<T> = RemovePlural<
{
[K in Extract<keyof T, string>]: T[K] extends Record<string, unknown>
? Join<K, Paths<T[K]>>
: K;
}[Extract<keyof T, string>]
>;

type I18nKey = typeof import("@/i18n").default extends I18nConfig<infer T>
? Paths<T extends object ? T : I18nDictionary>
: string;

export type Translate = <T extends unknown = string>(
i18nKey: string | TemplateStringsArray,
i18nKey: I18nKey | TemplateStringsArray,
query?: TranslationQuery | null,
options?: {
returnObjects?: boolean;
Expand Down

0 comments on commit f9447e1

Please sign in to comment.