UNPKG

react-intl

Version:

Internationalize React apps. This library provides React components and an API to format dates, numbers, and strings, including pluralization and handling translations.

237 lines (220 loc) 6.02 kB
/* * Copyright 2015, Yahoo Inc. * Copyrights licensed under the New BSD License. * See the accompanying LICENSE file for terms. */ import * as React from 'react'; // Since rollup cannot deal with namespace being a function, // this is to interop with TypeScript since `invariant` // does not export a default // https://github.com/rollup/rollup/issues/1267 import * as invariant_ from 'invariant'; const invariant: typeof invariant_ = (invariant_ as any).default || invariant_; import { Formatters, IntlConfig, MessageDescriptor, CustomFormats, } from '../types'; import {createError, escape} from '../utils'; import IntlMessageFormat, { FormatXMLElementFn, PrimitiveType, } from 'intl-messageformat'; function setTimeZoneInOptions( opts: Record<string, Intl.DateTimeFormatOptions>, timeZone: string ) { return Object.keys(opts).reduce( (all: Record<string, Intl.DateTimeFormatOptions>, k) => { all[k] = { timeZone, ...opts[k], }; return all; }, {} ); } function deepMergeOptions( opts1: Record<string, Intl.DateTimeFormatOptions>, opts2: Record<string, Intl.DateTimeFormatOptions> ) { const keys = Object.keys({...opts1, ...opts2}); return keys.reduce((all: Record<string, Intl.DateTimeFormatOptions>, k) => { all[k] = { ...(opts1[k] || {}), ...(opts2[k] || {}), }; return all; }, {}); } function deepMergeFormatsAndSetTimeZone( f1: CustomFormats, timeZone?: string ): CustomFormats { if (!timeZone) { return f1; } const mfFormats = IntlMessageFormat.formats; return { ...mfFormats, ...f1, date: deepMergeOptions( setTimeZoneInOptions(mfFormats.date, timeZone), setTimeZoneInOptions(f1.date || {}, timeZone) ), time: deepMergeOptions( setTimeZoneInOptions(mfFormats.time, timeZone), setTimeZoneInOptions(f1.time || {}, timeZone) ), }; } export function formatMessage( { locale, formats, messages, defaultLocale, defaultFormats, onError, }: Pick< IntlConfig, | 'locale' | 'formats' | 'messages' | 'defaultLocale' | 'defaultFormats' | 'onError' >, state: Formatters, messageDescriptor?: MessageDescriptor, values?: Record<string, PrimitiveType> ): string; export function formatMessage( { locale, formats, messages, defaultLocale, defaultFormats, onError, timeZone, }: Pick< IntlConfig, | 'locale' | 'formats' | 'messages' | 'defaultLocale' | 'defaultFormats' | 'onError' | 'timeZone' >, state: Formatters, messageDescriptor: MessageDescriptor = {id: ''}, values: Record< string, PrimitiveType | React.ReactElement | FormatXMLElementFn > = {} ): string | React.ReactNodeArray { const {id, defaultMessage} = messageDescriptor; // `id` is a required field of a Message Descriptor. invariant(id, '[React Intl] An `id` must be provided to format a message.'); const message = messages && messages[id]; formats = deepMergeFormatsAndSetTimeZone(formats, timeZone); defaultFormats = deepMergeFormatsAndSetTimeZone(defaultFormats, timeZone); let formattedMessageParts: Array<string | object> = []; if (message) { try { let formatter = state.getMessageFormat(message, locale, formats, { formatters: state, }); formattedMessageParts = formatter.formatHTMLMessage(values); } catch (e) { onError( createError( `Error formatting message: "${id}" for locale: "${locale}"` + (defaultMessage ? ', using default message as fallback.' : ''), e ) ); } } else { // This prevents warnings from littering the console in development // when no `messages` are passed into the <IntlProvider> for the // default locale, and a default message is in the source. if ( !defaultMessage || (locale && locale.toLowerCase() !== defaultLocale.toLowerCase()) ) { onError( createError( `Missing message: "${id}" for locale: "${locale}"` + (defaultMessage ? ', using default message as fallback.' : '') ) ); } } if (!formattedMessageParts.length && defaultMessage) { try { let formatter = state.getMessageFormat( defaultMessage, defaultLocale, defaultFormats ); formattedMessageParts = formatter.formatHTMLMessage(values); } catch (e) { onError( createError(`Error formatting the default message for: "${id}"`, e) ); } } if (!formattedMessageParts.length) { onError( createError( `Cannot format message: "${id}", ` + `using message ${ message || defaultMessage ? 'source' : 'id' } as fallback.` ) ); if (typeof message === 'string') { return message || defaultMessage || id; } return defaultMessage || id; } if ( formattedMessageParts.length === 1 && typeof formattedMessageParts[0] === 'string' ) { return (formattedMessageParts[0] as string) || defaultMessage || id; } return formattedMessageParts; } export function formatHTMLMessage( config: Pick< IntlConfig, | 'locale' | 'formats' | 'messages' | 'defaultLocale' | 'defaultFormats' | 'onError' >, state: Formatters, messageDescriptor: MessageDescriptor = {id: ''}, rawValues: Record<string, PrimitiveType> = {} ) { // Process all the values before they are used when formatting the ICU // Message string. Since the formatted message might be injected via // `innerHTML`, all String-based values need to be HTML-escaped. let escapedValues = Object.keys(rawValues).reduce( (escaped: Record<string, any>, name) => { let value = rawValues[name]; escaped[name] = typeof value === 'string' ? escape(value) : value; return escaped; }, {} ); return formatMessage(config, state, messageDescriptor, escapedValues); }