mirror of
https://github.com/cupcakearmy/svelte-i18n.git
synced 2024-11-16 18:10:43 +01:00
271 lines
6.9 KiB
TypeScript
271 lines
6.9 KiB
TypeScript
import {
|
|
convertNumberSkeletonToNumberFormatOptions,
|
|
isArgumentElement,
|
|
isDateElement,
|
|
isDateTimeSkeleton,
|
|
isLiteralElement,
|
|
isNumberElement,
|
|
isNumberSkeleton,
|
|
isPluralElement,
|
|
isPoundElement,
|
|
isSelectElement,
|
|
isTimeElement,
|
|
MessageFormatElement,
|
|
parseDateTimeSkeleton,
|
|
} from './deps/parser.ts'
|
|
|
|
export interface Formats {
|
|
number: Record<string, Intl.NumberFormatOptions>
|
|
date: Record<string, Intl.DateTimeFormatOptions>
|
|
time: Record<string, Intl.DateTimeFormatOptions>
|
|
}
|
|
|
|
export interface FormatterCache {
|
|
number: Record<string, Intl.NumberFormat>
|
|
dateTime: Record<string, Intl.DateTimeFormat>
|
|
pluralRules: Record<string, Intl.PluralRules>
|
|
}
|
|
|
|
export interface Formatters {
|
|
getNumberFormat(
|
|
...args: ConstructorParameters<typeof Intl.NumberFormat>
|
|
): Intl.NumberFormat
|
|
getDateTimeFormat(
|
|
...args: ConstructorParameters<typeof Intl.DateTimeFormat>
|
|
): Intl.DateTimeFormat
|
|
getPluralRules(
|
|
...args: ConstructorParameters<typeof Intl.PluralRules>
|
|
): Intl.PluralRules
|
|
}
|
|
|
|
export const enum PART_TYPE {
|
|
literal,
|
|
argument,
|
|
}
|
|
|
|
export interface LiteralPart {
|
|
type: PART_TYPE.literal
|
|
value: string
|
|
}
|
|
|
|
export interface ArgumentPart {
|
|
type: PART_TYPE.argument
|
|
value: any
|
|
}
|
|
|
|
export type MessageFormatPart = LiteralPart | ArgumentPart
|
|
|
|
export type PrimitiveType = string | number | boolean | null | undefined | Date
|
|
|
|
class FormatError extends Error {
|
|
public readonly variableId?: string
|
|
constructor(msg?: string, variableId?: string) {
|
|
super(msg)
|
|
this.variableId = variableId
|
|
}
|
|
}
|
|
|
|
function mergeLiteral(parts: MessageFormatPart[]): MessageFormatPart[] {
|
|
if (parts.length < 2) {
|
|
return parts
|
|
}
|
|
return parts.reduce((all, part) => {
|
|
const lastPart = all[all.length - 1]
|
|
if (
|
|
!lastPart ||
|
|
lastPart.type !== PART_TYPE.literal ||
|
|
part.type !== PART_TYPE.literal
|
|
) {
|
|
all.push(part)
|
|
} else {
|
|
lastPart.value += part.value
|
|
}
|
|
return all
|
|
}, [] as MessageFormatPart[])
|
|
}
|
|
|
|
// TODO(skeleton): add skeleton support
|
|
export function formatToParts(
|
|
els: MessageFormatElement[],
|
|
locales: string | string[],
|
|
formatters: Formatters,
|
|
formats: Formats,
|
|
values?: Record<string, any>,
|
|
currentPluralValue?: number,
|
|
// For debugging
|
|
originalMessage?: string
|
|
): MessageFormatPart[] {
|
|
// Hot path for straight simple msg translations
|
|
if (els.length === 1 && isLiteralElement(els[0])) {
|
|
return [
|
|
{
|
|
type: PART_TYPE.literal,
|
|
value: els[0].value,
|
|
},
|
|
]
|
|
}
|
|
const result: MessageFormatPart[] = []
|
|
for (const el of els) {
|
|
// Exit early for string parts.
|
|
if (isLiteralElement(el)) {
|
|
result.push({
|
|
type: PART_TYPE.literal,
|
|
value: el.value,
|
|
})
|
|
continue
|
|
}
|
|
// TODO: should this part be literal type?
|
|
// Replace `#` in plural rules with the actual numeric value.
|
|
if (isPoundElement(el)) {
|
|
if (typeof currentPluralValue === 'number') {
|
|
result.push({
|
|
type: PART_TYPE.literal,
|
|
value: formatters.getNumberFormat(locales).format(currentPluralValue),
|
|
})
|
|
}
|
|
continue
|
|
}
|
|
|
|
const { value: varName } = el
|
|
|
|
// Enforce that all required values are provided by the caller.
|
|
if (!(values && varName in values)) {
|
|
throw new FormatError(
|
|
`The intl string context variable "${varName}" was not provided to the string "${originalMessage}"`
|
|
)
|
|
}
|
|
|
|
let value = values[varName]
|
|
if (isArgumentElement(el)) {
|
|
if (!value || typeof value === 'string' || typeof value === 'number') {
|
|
value =
|
|
typeof value === 'string' || typeof value === 'number'
|
|
? String(value)
|
|
: ''
|
|
}
|
|
result.push({
|
|
type: PART_TYPE.argument,
|
|
value,
|
|
})
|
|
continue
|
|
}
|
|
|
|
// Recursively format plural and select parts' option — which can be a
|
|
// nested pattern structure. The choosing of the option to use is
|
|
// abstracted-by and delegated-to the part helper object.
|
|
if (isDateElement(el)) {
|
|
const style =
|
|
typeof el.style === 'string' ? formats.date[el.style] : undefined
|
|
result.push({
|
|
type: PART_TYPE.literal,
|
|
value: formatters
|
|
.getDateTimeFormat(locales, style)
|
|
.format(value as number),
|
|
})
|
|
continue
|
|
}
|
|
if (isTimeElement(el)) {
|
|
const style =
|
|
typeof el.style === 'string'
|
|
? formats.time[el.style]
|
|
: isDateTimeSkeleton(el.style)
|
|
? parseDateTimeSkeleton(el.style.pattern)
|
|
: undefined
|
|
result.push({
|
|
type: PART_TYPE.literal,
|
|
value: formatters
|
|
.getDateTimeFormat(locales, style)
|
|
.format(value as number),
|
|
})
|
|
continue
|
|
}
|
|
if (isNumberElement(el)) {
|
|
const style =
|
|
typeof el.style === 'string'
|
|
? formats.number[el.style]
|
|
: isNumberSkeleton(el.style)
|
|
? convertNumberSkeletonToNumberFormatOptions(el.style.tokens)
|
|
: undefined
|
|
result.push({
|
|
type: PART_TYPE.literal,
|
|
value: formatters
|
|
.getNumberFormat(locales, style)
|
|
.format(value as number),
|
|
})
|
|
continue
|
|
}
|
|
if (isSelectElement(el)) {
|
|
const opt = el.options[value as string] || el.options.other
|
|
if (!opt) {
|
|
throw new RangeError(
|
|
`Invalid values for "${
|
|
el.value
|
|
}": "${value}". Options are "${Object.keys(el.options).join('", "')}"`
|
|
)
|
|
}
|
|
result.push(
|
|
...formatToParts(opt.value, locales, formatters, formats, values)
|
|
)
|
|
continue
|
|
}
|
|
if (isPluralElement(el)) {
|
|
let opt = el.options[`=${value}`]
|
|
if (!opt) {
|
|
if (!Intl.PluralRules) {
|
|
throw new FormatError(`Intl.PluralRules is not available in this environment.
|
|
Try polyfilling it using "@formatjs/intl-pluralrules"
|
|
`)
|
|
}
|
|
const rule = formatters
|
|
.getPluralRules(locales, { type: el.pluralType })
|
|
.select((value as number) - (el.offset || 0))
|
|
opt = el.options[rule] || el.options.other
|
|
}
|
|
if (!opt) {
|
|
throw new RangeError(
|
|
`Invalid values for "${
|
|
el.value
|
|
}": "${value}". Options are "${Object.keys(el.options).join('", "')}"`
|
|
)
|
|
}
|
|
result.push(
|
|
...formatToParts(
|
|
opt.value,
|
|
locales,
|
|
formatters,
|
|
formats,
|
|
values,
|
|
value - (el.offset || 0)
|
|
)
|
|
)
|
|
continue
|
|
}
|
|
}
|
|
return mergeLiteral(result)
|
|
}
|
|
|
|
export function formatToString(
|
|
els: MessageFormatElement[],
|
|
locales: string | string[],
|
|
formatters: Formatters,
|
|
formats: Formats,
|
|
values?: Record<string, PrimitiveType>,
|
|
// For debugging
|
|
originalMessage?: string
|
|
): string {
|
|
const parts = formatToParts(
|
|
els,
|
|
locales,
|
|
formatters,
|
|
formats,
|
|
values,
|
|
undefined,
|
|
originalMessage
|
|
)
|
|
// Hot path for straight simple msg translations
|
|
if (parts.length === 1) {
|
|
return parts[0].value
|
|
}
|
|
return parts.reduce((all, part) => (all += part.value), '')
|
|
}
|