Skip to content

Commit

Permalink
Added template support and updated docs and tests accordingly
Browse files Browse the repository at this point in the history
  • Loading branch information
m10rten committed Aug 17, 2024
1 parent 54cc433 commit 7aaf39a
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 5 deletions.
5 changes: 5 additions & 0 deletions .changeset/three-garlics-rhyme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"i4n": minor
---

Added support for template-strings through custom user-functions.
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,39 @@ i4n.switch("es");
console.log(i4n.t("hi")); // "Ola"
```

### Templates

When working with `i4n`, you might find that you want to modify the output.

With templates, there are a few small steps required to get started:

**Step 1**: Edit your translations to include a template function.

This can be any function, I chose for it to look like this:

```ts
const translations: TranslationSet = {
en: {
sayHiTo: (name: string) => `Hello ${name}`,
},
es: {
sayHiTo: (name: string) => `Ola ${name}`,
},
};
```

**Step 2**: Call your new template with custom parameters.

You will then call the function `t` the same way, but with the parameters you defined:

```ts
i4n.t("sayHiTo", "John"); // "Hello John"
```

> 💡 As mentioned, this works with the parameters **you** defined. Objects and custom classes will also work!
This may seem small, but this is fully typed, making it very easy to define and use your templates in your code.

## `I4nException`

When using this package, be sure not to force types.
Expand Down
35 changes: 30 additions & 5 deletions src/i4n.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { I4nException } from "./errors";
import type { Path, Value } from "./types";

/**
* @prop {translations} - the data required for the translation module to return text.
*
* @link https://github.com/m10rten/i4n/ - for more information
*
* Should be in [lang: string]: {key: string | object} structure
* @example
* ```ts
Expand Down Expand Up @@ -41,19 +44,41 @@ export class I4n<T extends Record<string, unknown>, L extends keyof T & string>
* @param path the path to the translation, this is is typed for you based on the translations.
* @returns string or undefined based on the path you provide.
*/
public t<P extends Path<T[L], L>>(path: P): Value<T[L], L, P> {
const keys = String(path).split(".") as Array<keyof T[L]>;
public t<P extends Path<T[L], L>, V extends Value<T[L], L, P>, R extends V>(path: P): R;

/**
* You added parameters, expecting to find a function, do not force extra arguments if you are not expecting to find a function since that will confuse the type system and give you as a user the wrong types.
* @param path the path to the function
* @param args the typed parameters for this function
*/
public t<
P extends Path<T[L], L>,
V extends Value<T[L], L, P>,
A extends V extends (...args: infer A) => any ? A : never,
R extends V extends (...args: any[]) => any ? ReturnType<V> : V,
>(path: P, ...args: A): R;

let result = this._data[this._lang];
/**
* `t` function on the `I4n` class.
* @param path string to find in your translations.
* @param args optional arguments to execute a template function.
* @returns a string from the translations, a function or object, or the result of a function.
*/
public t(path: string, ...args: unknown[]): unknown {
const keys = String(path).split(".");

let result: any = this._data[this._lang];

for (const key of keys) {
result = result?.[key] as T[L];
result = result?.[key];
if (result === undefined) {
return undefined as never;
}
}

return result as Value<T[L], L, P>;
if (typeof result === "function" && args.length !== 0) return result(...args);

return result;
}

/**
Expand Down
24 changes: 24 additions & 0 deletions tests/i4n.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import I4n, { I4nException } from "../src";

type TranslationData = {
earth: string;
nested: {
key: string;
};
single_template: (name: string) => string;

object_template: ({ type, count }: { type: string; count: number }) => string;
};
type TranslationSet = Record<string, TranslationData>;

Expand All @@ -15,13 +19,21 @@ const testTranslations: TranslationSet = {
nested: {
key: "english key",
},

single_template: (name) => `Hello ${name}`,

object_template: ({ type, count }) => `[${type}]: has ${count}`,
},
es: {
earth: "Mundo",

nested: {
key: "key espanol",
},

single_template: (name) => `Ola ${name}`,

object_template: ({ type, count }) => `[${type}]: a ${count}`,
},
};

Expand Down Expand Up @@ -76,4 +88,16 @@ describe("I4n", () => {
expect(e).toBeInstanceOf(I4nException);
}
});

test("If the module can handle templating with a single string parameter", () => {
expect(i4n.t("single_template")).toBeInstanceOf(Function);
const test_data = "john";
expect(i4n.t("single_template", test_data)).toBe(testTranslations["en"]?.single_template(test_data));
});

test("If the module can handle templating with an object parameter", () => {
expect(i4n.t("object_template")).toBeInstanceOf(Function);
const test_input = { count: 4, type: "ice" };
expect(i4n.t("object_template", test_input)).toBe(testTranslations["en"]?.object_template(test_input));
});
});

0 comments on commit 7aaf39a

Please sign in to comment.