Last active
June 8, 2025 01:27
-
-
Save fsubal/f31b12d31e71fdc04037266643264f85 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(さようなら) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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