From 1463d448edc46e1a5bfc8a62907e3f51856cc63e Mon Sep 17 00:00:00 2001 From: Mati Date: Sun, 10 Mar 2024 23:48:20 +0100 Subject: [PATCH] Update i18n: Support custom translation dict as argument (#12) * Update i18n to take dict as input * Remove defaultLocaleKey * Update i18n config type * Add list of locale codes * Only keep primary in list of locale codes * Add locales & default locale types * Export i18n config with const type * Fix locales list type to generalize * Use Locales type for i18n function * Add tranlations for "or" * Refactor Tail * Refactor recursive type functions * Fix types to support function overloads type * Use explicit dict for some translations in index --- app/app/config/i18n.ts | 28 ++++++++++------ app/app/src/i18n/i18n.ts | 61 ++++++++++++++++++++++++++++++---- app/app/src/i18n/index.ts | 3 +- app/app/src/i18n/locales/en.ts | 1 + app/app/src/i18n/locales/fr.ts | 1 + app/app/src/i18n/type.ts | 60 ++++++++++++++++++++++++++++++--- app/app/src/pages/index.astro | 20 ++++++++--- 7 files changed, 144 insertions(+), 30 deletions(-) diff --git a/app/app/config/i18n.ts b/app/app/config/i18n.ts index 9bd4f46..c3ace6a 100644 --- a/app/app/config/i18n.ts +++ b/app/app/config/i18n.ts @@ -1,14 +1,4 @@ -const i18n = -{ - defaultLocale: 'en', - locales: [ - 'en', - 'fr', - ], - routing: { - prefixDefaultLocale: false, - }, -} as { +type I18nConfig = { readonly defaultLocale: string, readonly locales: readonly ( | string @@ -22,4 +12,20 @@ const i18n = } } +const i18n = +{ + defaultLocale: 'en', + locales: [ + 'en', + 'fr', + ], + routing: { + prefixDefaultLocale: false, + }, +} as const satisfies I18nConfig + export default i18n + +export type { + I18nConfig, +} diff --git a/app/app/src/i18n/i18n.ts b/app/app/src/i18n/i18n.ts index 1eb95e6..2146d23 100644 --- a/app/app/src/i18n/i18n.ts +++ b/app/app/src/i18n/i18n.ts @@ -1,16 +1,33 @@ import en from './locales/en' import fr from './locales/fr' -import { defaultLocaleKey, type I18n } from './type' +import { defaultLocale, type I18n } from './type' +import type { Locales } from './type' -const locales = { en, fr } as const +const globalDictionary = { + 'en': en, + 'fr': fr, +} as const satisfies Partial> function i18n( - locale: keyof typeof locales, + locale: Locales, key: keyof I18n, ...args: string[] ) +function i18n( + locale: Locales, + dict: Partial>, + ...args: string[] +) +function i18n( + locale: Locales, + data: keyof I18n | Partial>, + ...args: string[] +) { - const value = (locales[locale]?.[key] ?? key) as I18n[keyof I18n] + const value = + typeof data === 'object' + ? data[locale] + : (globalDictionary[locale]?.[data] ?? data) satisfies I18n[keyof I18n] if (typeof value !== 'string') { @@ -25,11 +42,41 @@ function i18n( ) } -type Tail = ((...args: T) => any) extends (arg: any, ...tail: infer U) => any ? U : never +// From `ToTuple`: https://stackoverflow.com/a/70061272 +type UnionToParm = U extends any ? (k: U) => void : never +type UnionToSect = UnionToParm extends ((k: infer I) => void) ? I : never +type ExtractParm = F extends { (a: infer A): void } ? A : never +type SpliceOne = Exclude> +type ExtractOne = ExtractParm>> +type UnionToTuple = + SpliceOne extends never + ? [ExtractOne, ...Rslt] + : UnionToTuple, [ExtractOne, ...Rslt]> + +type TupleToUnion = + Tuple extends [infer Head, ...infer Tail] + ? TupleToUnion + : Rslt + +// From https://stackoverflow.com/a/52761156 +type OverloadedParameters = + T extends { (...args: infer A1): any; (...args: infer A2): any } ? A1 | A2 : + T extends (...args: infer A) => any ? A : + any + +type ListTail = List extends [any, ...infer Tail] ? Tail : never + +type ListRecursiveTail = + List extends [infer Head extends any[], ...infer Tail] + ? ListRecursiveTail]> + : Rslt +type UnionRecursiveTail = + TupleToUnion, Rslt>> -function i18nFactory(locale: Parameters[0] | undefined) +function i18nFactory(locale: OverloadedParameters[0] | undefined) { - return (...args: Tail>) => i18n(locale ?? defaultLocaleKey, ...args) + return (...args: UnionRecursiveTail>) => + i18n.apply(i18n, [locale ?? defaultLocale, ...args]) } export default i18n diff --git a/app/app/src/i18n/index.ts b/app/app/src/i18n/index.ts index a414737..f62362e 100644 --- a/app/app/src/i18n/index.ts +++ b/app/app/src/i18n/index.ts @@ -3,13 +3,12 @@ import getLocaleByUrl from './getLocaleByUrl' import getLocaleUrlList from './getLocaleUrlList' import getUrlWithoutLocale from './getUrlWithoutLocale' import i18n, { i18nFactory } from './i18n' -import { defaultLocale, defaultLocaleKey } from './type' +import { defaultLocale } from './type' export default i18n export { defaultLocale, - defaultLocaleKey, i18nFactory, getLocaleByPath, getLocaleByUrl, diff --git a/app/app/src/i18n/locales/en.ts b/app/app/src/i18n/locales/en.ts index c9d937f..0c843bc 100644 --- a/app/app/src/i18n/locales/en.ts +++ b/app/app/src/i18n/locales/en.ts @@ -5,6 +5,7 @@ const defaultLocale = // Global 'Back to home page', 'and', + 'or', // Footer 'Open-source repository', diff --git a/app/app/src/i18n/locales/fr.ts b/app/app/src/i18n/locales/fr.ts index 1db2dfc..41ed29c 100644 --- a/app/app/src/i18n/locales/fr.ts +++ b/app/app/src/i18n/locales/fr.ts @@ -7,6 +7,7 @@ const locale = // Global 'Back to home page': 'Retour à la page d\'accueil', 'and': 'et', + 'or': 'ou', // Footer 'Open-source repository': 'Dépôt open-source', diff --git a/app/app/src/i18n/type.ts b/app/app/src/i18n/type.ts index cee4dbc..f47c9ab 100644 --- a/app/app/src/i18n/type.ts +++ b/app/app/src/i18n/type.ts @@ -1,21 +1,71 @@ import i18nConfig from '/config/i18n' +import type { I18nConfig } from '/config/i18n' import defaultLocaleData from './locales/en' -const defaultLocale = i18nConfig.defaultLocale - -const defaultLocaleKey = 'en' as const - type I18n = Readonly> type Diff = T extends U ? never : T +type DeepWriteable = { -readonly [P in keyof T]: DeepWriteable } + +// Inspired from `ToTuple`: https://stackoverflow.com/a/70061272 +type LocalesToUnion< + Locales extends any[] | readonly any[], + Rslt extends any = never, +> = + DeepWriteable extends [infer Head, ...infer Tail] + ? LocalesToUnion< + Tail, + Rslt | ( + DeepWriteable extends { codes: string[] } + ? DeepWriteable['codes'][0] + : Head + ) + > + : Rslt + +type Locales = LocalesToUnion + +type LocalesToList< + Locales extends any[] | readonly any[], + Rslt extends readonly any[] = [], +> = + DeepWriteable extends [infer Head, ...infer Tail] + ? LocalesToList< + Tail, + [ + ...Rslt, + ( + DeepWriteable extends { codes: string[] } + ? DeepWriteable['codes'][0] + : Head + ), + ] + > + : Rslt + +type LocalesList = LocalesToList + +const locales: LocalesList = + (i18nConfig.locales satisfies I18nConfig['locales'] as I18nConfig['locales']) + .map( + locale => typeof locale === 'string' ? locale : locale.codes[0] + ) satisfies string[] as LocalesList + +type DefaultLocale = Locales & typeof i18nConfig.defaultLocale + +const defaultLocale: DefaultLocale = i18nConfig.defaultLocale + export { defaultLocale, - defaultLocaleKey, + locales, } export type { I18n, Diff, + Locales, + LocalesList, + DefaultLocale, } diff --git a/app/app/src/pages/index.astro b/app/app/src/pages/index.astro index 36d4b44..3b0ce41 100644 --- a/app/app/src/pages/index.astro +++ b/app/app/src/pages/index.astro @@ -4,7 +4,8 @@ import yaml from 'js-yaml' import Default from '~/layouts/Default.astro' -import { defaultLocale } from '~/i18n' +import { i18nFactory, defaultLocale } from '~/i18n' +const _ = i18nFactory(Astro.currentLocale as any) const _url = (path?: string) => getAbsoluteLocaleUrl(Astro.currentLocale ?? defaultLocale, path) import aboutYml from '~/data/about.yml?raw' @@ -49,10 +50,19 @@ const title = 'Mathieu GUÉRIN' )}

- You can contact me via:
- Telegram, or
- Discord ({about.social.discord.handle}), or
- Email, for professional inquiries +
+ Telegram, +
+ Discord ({about.social.discord.handle}), +
+ Email + ()