Skip to content

Instantly share code, notes, and snippets.

@buren
Created August 25, 2023 14:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save buren/29fccf9c55e2b9fa748439df7fc9bcc2 to your computer and use it in GitHub Desktop.
Save buren/29fccf9c55e2b9fa748439df7fc9bcc2 to your computer and use it in GitHub Desktop.
Super minimalistic I18n lib in TypeScript.
{
"title": "Example title",
"about": {
"title": "Example about title"
}
}
export const flattenObject = (obj: Record<string, any>, parentKey?: string) => {
let result: Record<string, any> = {};
Object.keys(obj).forEach((key) => {
const value = obj[key];
const _key = parentKey ? parentKey + "." + key : key;
if (typeof value === "object") {
result = { ...result, ...flattenObject(value, _key) };
} else {
result[_key] = value;
}
});
return result;
};
import { ObjectLeaves } from "@/types";
import { flattenObject } from "./flatten-object";
import en from "./en.json";
export enum Locale {
en = "en",
}
type TranslationsData = typeof en;
export type I18nKey = ObjectLeaves<TranslationsData>;
type I18nLocaleData = Record<I18nKey, string>;
type I18nData = { [key in Locale]: I18nLocaleData };
const i18nData: I18nData = {
[Locale.en]: flattenObject(en) as I18nLocaleData,
};
type InterpolationValue = string | number;
type Interpolation = Record<string, InterpolationValue>;
type I18nOptions = {
locale?: Locale;
fallback?: string;
};
const interpolate = (
translation: string,
interpolation: Interpolation = {}
): string =>
Object.keys(interpolation).reduce(
(acc, key) =>
acc.replaceAll(`{${key}}`, interpolation[key].toString()),
translation
);
export const currentLocale = () => Locale.en;
export const currentLocaleWithTerritory = () => `${Locale.en}_US`;
export function i18n(
i18nKey: I18nKey,
interpolation: Interpolation = {},
{
fallback,
locale = currentLocale(),
}: I18nOptions = { locale: currentLocale() }) {
const translation = i18nData[locale][i18nKey] || fallback;
if (!translation) {
throw new Error(`Can't find translation for i18nKey: ${i18nKey}`);
}
return interpolate(translation, interpolation);
}
/**
* The `Join` type concatenates two types K and P with a dot (.) separator.
* However, if the second type is an empty string, it avoids adding the dot.
* This type is used for joining nested object keys into a single string.
* It works only if both K and P are either strings or numbers.
*
* Examples:
* Join<'a', 'b'> -> 'a.b'
* Join<'a', ''> -> 'a'
*/
type Join<K, P> = K extends string | number
? P extends string | number
? `${K}${"" extends P ? "" : "."}${P}`
: never
: never;
/**
* The `Prev` type is a tuple where each index contains its value.
* It can be used to decrement a number type. It is a mechanism to implement
* recursion depth control. The ellipsis with 0[] at the end allows it to
* accept numbers greater than 20, but it won't have exact mappings for them.
*/
type Prev = [
never,
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
...0[],
];
/**
* The `ObjectLeaves` type recursively retrieves the paths of all the leaves
* (terminal nodes) of a nested object as strings.
* The recursion depth is controlled by the D parameter (default is 10).
*
* - If D is never, it returns never.
* - If T is an object, it recursively processes the properties of the object,
* joining the keys with dots using the `Join` type.
* - If T is not an object, it returns an empty string.
*
* Examples:
* ObjectLeaves<{a: {b: number}}> -> 'a.b'
* ObjectLeaves<{a: number, b: {c: string}}> -> 'a' | 'b.c'
*/
export type ObjectLeaves<T, D extends number = 10> = [D] extends [never]
? never
: T extends object
? { [K in keyof T]-?: Join<K, ObjectLeaves<T[K], Prev[D]>> }[keyof T]
: "";
import React, { ReactElement, ReactNode } from "react";
import { I18nKey, i18n } from "./i18n";
interface TransProps {
i18nKey: I18nKey;
text?: string;
[x: string]: ReactNode;
}
const Trans = ({ i18nKey, text, ...interpolationData }: TransProps) => {
const i18nText = text ? text : i18n(i18nKey);
const interpolations = Object.entries(interpolationData || {}) as [string, ReactNode][];
// RegExp for matching "{...}" in the text
const interpolationPattern = /(\{.*?\})/g;
// RegExp for removing the "{" and "}" characters
const bracesPattern = /[{}]/g;
return i18nText.split(interpolationPattern).map((part, index) => {
const key = part.replace(bracesPattern, "");
const node = interpolations.find(([iKey]) => iKey === key)?.[1];
return node
? React.cloneElement(node as ReactElement<any>, { key: index })
: part;
});
};
export { Trans };
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment