Skip to content

Instantly share code, notes, and snippets.

@ephrin
Last active July 18, 2022 10:16
Show Gist options
  • Save ephrin/1abb540c83cd4af43934af0b9c2794e1 to your computer and use it in GitHub Desktop.
Save ephrin/1abb540c83cd4af43934af0b9c2794e1 to your computer and use it in GitHub Desktop.
type AnyObject = Record<string, unknown>;
export type Join<K, P> = K extends string | number
? P extends string | number
? `${K}${'' extends P ? '' : '.'}${P}`
: never
: never;
export 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[]
];
export type Leaves<T, D extends number = 10> = [D] extends [never]
? never
: T extends AnyObject
? { [K in keyof T]-?: Join<K, Leaves<T[K], Prev[D]>> }[keyof T]
: '';
import { translate as t } from './translate';
import { languageMessages } from './messages';
describe('t', (): void => {
it('can translate messages', (): void => {
expect(t('lang')).toBe('de');
});
it('can translate messages with args for custom lang', (): void => {
expect(t('hello', { language: 'eng', args: { who: 'World' } })).toBe('Hello World!');
});
it('understands nested keys', (): void => {
expect(t('login.maxAttemptsReached', { language: 'eng' })).toBe(
languageMessages.eng.login.maxAttemptsReached,
);
});
it('wrong key leads to same string', (): void => {
expect(t('not.existent.place')).toBe('not.existent.place');
});
it('raises error on incomplete key. so no automation only constants', (): void => {
const wrongKeyCall = (): string => {
return t('login');
};
expect(wrongKeyCall).toThrow("Wrong translation path provided: 'login'.");
});
});
import { i18nConfig } from '../config/i18n';
import { path, memoizeWith } from 'ramda';
import * as format from 'string-format';
import { Leaves } from '../types/paths';
const eng = {
lang: 'eng',
hello: 'Hello {who}!',
};
export const languageMessages = { eng };
export type LangKey = keyof typeof languageMessages;
export type Messages = typeof languageMessages[LangKey];
export type MessageKey = Leaves<Messages>;
type Args = Record<string, string | number>;
type TranslationOpts = {
language?: LangKey;
args?: Args[] | Args;
};
const getTranslations = (langCode: LangKey): Messages => {
return languageMessages[langCode] || {};
};
const cachedTranslations = memoizeWith(String, getTranslations);
const cachedFormat = memoizeWith((a, b) => JSON.stringify([a, b]), format);
export const translate = (textKey: MessageKey | string, options?: TranslationOpts): string => {
const langCode = options?.language || i18nConfig.defaultLanguage || 'de';
const t10s = cachedTranslations(langCode as LangKey);
let tpl = path(textKey.split('.'), t10s);
if (typeof tpl === 'object') {
throw new Error(`Wrong translation path provided: '${textKey}'.`);
}
if (!tpl) {
// console.warn(`Missing translation for key: '${textKey}', lang: '${langCode}'`);
tpl = textKey;
}
const args = (
options?.args ? (options.args?.length ? options.args : [options.args]) : []
) as Args[];
return cachedFormat(tpl as unknown as string, ...args);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment