Skip to content

Instantly share code, notes, and snippets.

@fsubal
Last active June 8, 2025 01:27
Show Gist options
  • Select an option

  • Save fsubal/f31b12d31e71fdc04037266643264f85 to your computer and use it in GitHub Desktop.

Select an option

Save fsubal/f31b12d31e71fdc04037266643264f85 to your computer and use it in GitHub Desktop.
export type UnionToIntersection<Union> = (
Union extends unknown ? (distributedUnion: Union) => void : never
) extends (
(mergedIntersection: infer Intersection) => void
) ? Intersection & Union : never;
type TemplateString<Varname extends string> = UnionToIntersection<`${string}{{${Varname}}}${string}`>
declare const phantom: unique symbol
type VariableRecord = Record<string, string | number | Date | string[]>
interface Locale {
ja: string
en?: string
es?: string
}
type TemplateConfig<Var extends VariableRecord> = {
[K in keyof Locale]: TemplateString<keyof Var & string>
}
type TemplateVocabulary<Var extends VariableRecord> = {
[K in keyof Locale]: TemplateString<keyof Var & string>
} & {
[phantom]: Var
}
type TextVocabulary = {
[K in keyof Locale]: string
}
const template = <Var extends VariableRecord>(config: TemplateConfig<Var>) => config as TemplateVocabulary<Var>
const text = (config: TextVocabulary): TextVocabulary => config
interface Translator {
(vocabulary: TextVocabulary): string
interpolate<Var extends VariableRecord>(
vocabulary: TemplateVocabulary<Var>,
variables: Var
): string
}
const i18n = (defaultLocale: keyof Locale) => (currentLocale: keyof Locale = defaultLocale): Translator => {
function translationMissing(vocabulary: TextVocabulary, currentLocale: keyof Locale): never {
throw new Error(`Translation missing: No vocabulary for "${currentLocale}" in ${JSON.stringify(vocabulary)}`)
}
function t(vocabulary: TextVocabulary) {
return vocabulary[currentLocale] ?? vocabulary[defaultLocale] ?? translationMissing(vocabulary, currentLocale)
}
const listFormat = new Intl.ListFormat([currentLocale, defaultLocale])
const dateFormat = new Intl.DateTimeFormat([currentLocale, defaultLocale])
return Object.assign(t, {
interpolate<Var extends VariableRecord>(
vocabulary: TemplateVocabulary<Var>,
variables: Var
) {
let template = t(vocabulary)
for (const [varname, value] of Object.entries(variables)) {
let formattedValue: string
if (Array.isArray(value)) {
formattedValue = listFormat.format(value)
} else if (value instanceof Date) {
formattedValue = dateFormat.format(value)
} else {
formattedValue = value.toString()
}
template = template.replace(new RegExp(`\{\{${varname}\}\}`, 'gi'), formattedValue)
}
return template
},
l(value: Date, options: Intl.DateTimeFormat) {
return datetimeFormatter([currentLocale, defaultLocale], options).format(value)
},
sentence(value: string[], options: Intl.ListFormat) {
return listFormatter([currentLocale, defaultLocale], options).format(value)
}
})
}
export const こんにちは = template<{ you: string, date: Date }>({
ja: '{{you}}さん、こんにちは。今日は{{date}}です',
en: 'Hello, {{you}}. Today is {{date}}',
es: 'Hola, {{you}}. Hoy es {{date}}'
})
export const さようなら = text({ ja: 'さようなら', en: 'Good bye' })
const t = i18n('ja')('ja')
export const a = t.interpolate(こんにちは, { you: '太郎', date: new Date() })
export const b = t(さようなら)
function stringifySorted<T extends {}>(obj: T) {
return JSON.stringify(obj, (_, v) =>
Object.keys(v).sort().reduce((r, k) => ({
...r,
[k]: v[k]
}), {} as T)
);
}
type CacheKey = string
function cacheKey(locales: Intl.Locale[], options: object): CacheKey {
return JSON.stringify(locales) + "/" + stringifySorted(options);
}
const listFormatterCache = new Map<CacheKey, Intl.ListFormat>()
export function listFormatter(
locales: Intl.Locale[],
options: Intl.ListFormatOptions
) {
const key = cacheKey(locales, options);
const found = listFormatterCache.get(key);
if (found) {
return found;
}
const fresh = new Intl.ListFormat(locales, options);
listFormatterCache.set(key, fresh);
return fresh;
}
const datetimeFormatterCache = new Map<CacheKey, Intl.DateTimeFormat>()
export function datetimeFormatter(
locales: Intl.Locale[],
options: Intl.DateTimeFormatOptions
) {
const key = cacheKey(locales, options);
const found = datetimeFormatterCache.get(key);
if (found) {
return found;
}
const fresh = new Intl.DateTimeFormat(locales, options);
datetimeFormatterCache.set(key, fresh);
return fresh;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment