Skip to content

Commit

Permalink
Improve typed i18n infrastructure + explicit namespace
Browse files Browse the repository at this point in the history
  • Loading branch information
tokland committed Aug 1, 2024
1 parent 0327389 commit 54fcc22
Show file tree
Hide file tree
Showing 2 changed files with 40 additions and 34 deletions.
38 changes: 38 additions & 0 deletions src/utils/i18n-typed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// @ts-ignore
import i18n from "$/locales";

export function getModuleForNamespace(namespace: string) {
return {
t: function <Str extends string>(...args: I18nTArgs<Str>): string {
const [s, options] = args;
return i18n.t(s, { ...options, ns: namespace });
},
changeLanguage: i18n.changeLanguage.bind(i18n),
setDefaultNamespace: i18n.setDefaultNamespace.bind(i18n),
};
}

type I18nTArgs<Str extends string> = Interpolations<Str> extends Record<string, never>
? [Str] | [Str, Partial<Options>]
: [Str, Interpolations<Str> & Partial<Options>];

interface Options {
ns: string; // namespace
nsSeparator: string | boolean; // By default, ":", which breaks strings containing that char
lng: string; // language
}

type Interpolations<Str extends string> = Record<ExtractVars<Str>, string | number>;

type ExtractVars<Str extends string> = Str extends `${string}{{${infer Var}}}${infer StrRest}`
? Var | ExtractVars<StrRest>
: never;

/* Tests */

type IsEqual<T1, T2> = [T1] extends [T2] ? ([T2] extends [T1] ? true : false) : false;
const assertEqualTypes = <T1, T2>(_eq: IsEqual<T1, T2>): void => {};

assertEqualTypes<ExtractVars<"">, never>(true);
assertEqualTypes<ExtractVars<"name={{name}}">, "name">(true);
assertEqualTypes<ExtractVars<"name={{name}} age={{age}}">, "name" | "age">(true);
36 changes: 2 additions & 34 deletions src/utils/i18n.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,3 @@
// @ts-ignore
import i18n from "$/locales";
import { getModuleForNamespace } from "./i18n-typed";

function t<Str extends string>(s: Str, namespace?: GetNamespace<Str>): string {
return i18n.t(s, namespace);
}

interface Options {
nsSeparator: string | boolean;
lng: string;
}

type GetNamespace<Str extends string> = Record<ExtractVars<Str>, string | number> &
Partial<Options>;

type ExtractVars<Str extends string> = Str extends `${string}{{${infer Var}}}${infer StrRest}`
? Var | ExtractVars<StrRest>
: never;

/* Tests */

type IsEqual<T1, T2> = [T1] extends [T2] ? ([T2] extends [T1] ? true : false) : false;
const assertEqualTypes = <T1, T2>(_eq: IsEqual<T1, T2>): void => {};

assertEqualTypes<ExtractVars<"">, never>(true);
assertEqualTypes<ExtractVars<"name={{name}}">, "name">(true);
assertEqualTypes<ExtractVars<"name={{name}} age={{age}}">, "name" | "age">(true);

const i18nTyped = {
t,
changeLanguage: i18n.changeLanguage.bind(i18n),
setDefaultNamespace: i18n.setDefaultNamespace.bind(i18n),
};

export default i18nTyped;
export default getModuleForNamespace("dhis2-skeleton-app");

0 comments on commit 54fcc22

Please sign in to comment.