Skip to content

Commit

Permalink
feat(types): infer valid translation paths from source
Browse files Browse the repository at this point in the history
  • Loading branch information
davidwarrington committed Oct 17, 2023
1 parent 3772e38 commit 274378d
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 2 deletions.
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,59 @@ Then you can use it in your Vue templates like so:
<button>{{ 'add_to_cart' | t }}</button>
</template>
```

### TypeScript

`translate` accepts a generic that can be used to provide type safety to your translation keys. If you're passing the translations directly you get this for free.

```ts
const t = translate({
collection: {
pagination: {
page_x_of_y: 'Page {{ x }} of {{ y }}',
},
},
product: {
price: '£{{ price }}',
},
});

t('collection.pagination.page_x_of_y'); // Typescript knows this is valid
t('collection.related_collections'); // Typescript knows this is not valid
```

If you're not passing the translations directly, for example if you're passing them in via `window.translations`, you can use a generic to help TypeScript.

```ts
type Translations = {
collection: {
pagination: {
page_x_of_y: string;
};
};
product: {
price: string;
};
};

const t = translate<Translations>(window.translations);
```

If you're using JSDoc you can still benefit from the generic, it's just ~~a little~~ ugly.

```js
/**
* @typedef {object} Translations
*
* @property {object} collection
* @property {object} collection.pagination
* @property {string} collection.pagination.page_x_of_y
*
* @property {object} product
* @property {string} product.price
*/

const t = /** @type {typeof translate<Translations>} */ (translate)(
window.translations
);
```
17 changes: 15 additions & 2 deletions src/translate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ interface TranslateSource {

type TranslateVariables = Record<string, unknown>;

/**
* @note Credit to Pedro Figueiredo for the original type - [Source](https://dev.to/pffigueiredo/typescript-utility-keyof-nested-object-2pa3).
* This implementation has been modified slightly to require that the translation path is complete, e.g `a.b` is not valid if `a.b.c` is.
*/
type NestedKeyOf<ObjectType extends object> = {
[Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
? `${Key}.${NestedKeyOf<ObjectType[Key]>}`
: `${Key}`;
}[keyof ObjectType & (string | number)];

export function renderString(
renderTarget: string,
variables: TranslateVariables,
Expand All @@ -33,9 +43,12 @@ export function renderString(
}, renderTarget);
}

export function translate(source: TranslateSource, options?: TranslateOptions) {
export function translate<T extends TranslateSource>(
source: T,
options?: TranslateOptions
) {
return function (
translationPath: string,
translationPath: NestedKeyOf<T>,
variables: TranslateVariables = {}
) {
const NO_TRANSLATION_ERROR =
Expand Down

0 comments on commit 274378d

Please sign in to comment.