Add HoudiniIntl

This commit is contained in:
Eric Schultz 2020-07-31 15:48:15 -05:00 committed by Eric Schultz
parent 79fe9042ac
commit e1979d809f
4 changed files with 204 additions and 0 deletions

View file

@ -0,0 +1,40 @@
// License: LGPL-3.0-or-later
import { createHoudiniIntl, FormatMoneyOptions } from "./HoudiniIntl";
import { Money } from "../../common/money";
const NBSP = '\xa0';
let tests:Array<[Money, FormatMoneyOptions, string]>;
describe('formatMoney', () => {
describe('en', () => {
const intl = createHoudiniIntl({locale: 'en'});
const oneDollar = Money.fromCents(100, 'usd');
const oneThousandDollars = Money.fromCents(100000, 'usd');
const oneThousandDollarsTenCents = Money.fromCents(100010, 'usd');
const oneEuro = Money.fromCents(100, 'eur');
const oneHundredYen = Money.fromCents(100, 'jpy');
tests = [
[oneDollar, {}, "$1.00"],
[oneDollar, {currencyDisplay: 'code'}, `USD${NBSP}1.00`],
[oneDollar, {currencyDisplay: "name"}, "1.00 US dollars"],
[oneThousandDollars, {}, '$1,000.00'],
[oneThousandDollars, {currencyDisplay: 'code'}, `USD${NBSP}1,000.00`],
[oneThousandDollars, {currencyDisplay: "name"}, `1,000.00 US dollars`],
[oneThousandDollars, {minimumFractionDigits: 0}, "$1,000"],
[oneThousandDollarsTenCents, {minimumFractionDigits: 2}, "$1,000.10"],
[oneEuro, {}, "€1.00"],
[oneEuro, {currencyDisplay: 'code'}, `EUR${NBSP}1.00`],
[oneEuro, {currencyDisplay: "name"}, "1.00 euros"],
[oneHundredYen, {}, "¥100"],
[oneHundredYen, {currencyDisplay: 'code'}, `JPY${NBSP}100`],
[oneHundredYen, {currencyDisplay: "name"}, "100 Japanese yen"]
];
it.each(tests)('money representing %j with opts %j returns %s', (money, opts, expected ) => {
expect.assertions(1);
const output = intl.formatMoney(money, opts);
expect(output).toBe(expected);
});
});
});

View file

@ -0,0 +1,57 @@
// License: LGPL-3.0-or-later
import * as React from "react";
import { useIntl, FormatNumberOptions, IntlShape, IntlProvider, createIntl} from "react-intl";
import { Money } from "../../common/money";
export declare type FormatMoneyOptions = Omit<FormatNumberOptions,'style'|'unit'|'unitDisplay'|'currency'>;
export declare type HoudiniIntlShape = IntlShape & {formatMoney:(amount:Money, opts?:FormatMoneyOptions) => string}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const HoudiniIntlContext = React.createContext<HoudiniIntlShape>(null as any);
function rawFormatMoney(intl:IntlShape, amount:Money, opts?:FormatMoneyOptions) : string {
const formatter = intl.formatters.getNumberFormat(intl.locale, {...opts,
style: 'currency',
currency: amount.currency.toUpperCase(),
});
const adjustedAmount = amount.amount / Math.pow(10, formatter.resolvedOptions().maximumFractionDigits);
return formatter.format(adjustedAmount);
}
export function useHoudiniIntl() : HoudiniIntlShape {
const context = React.useContext(HoudiniIntlContext);
return context;
}
export function HoudiniIntlProvider(props:ConstructorParameters<typeof IntlProvider>[0]) : JSX.Element {
return <IntlProvider {...props}>
<InnerProvider>
{props.children}
</InnerProvider>
</IntlProvider>;
}
// eslint-disable-next-line @typescript-eslint/ban-types
function InnerProvider({children}:React.PropsWithChildren<{}>) : JSX.Element {
const intl = useIntl();
const formatMoney = React.useCallback((amount:Money, opts?:FormatMoneyOptions) => {
return rawFormatMoney(intl, amount, opts);
}, [intl]);
const houdiniIntl = { ...intl, formatMoney};
return <HoudiniIntlContext.Provider value={houdiniIntl}>
{children}
</HoudiniIntlContext.Provider>;
}
export function createHoudiniIntl(...props:Parameters<typeof createIntl>) : HoudiniIntlShape {
const intl = createIntl(...props);
const formatMoney = (amount:Money, opts?:FormatMoneyOptions) => rawFormatMoney(intl, amount, opts);
return {...intl, formatMoney};
}

View file

@ -0,0 +1,3 @@
// License: LGPL-3.0-or-later
export {useHoudiniIntl, HoudiniIntlProvider} from './HoudiniIntl';
export type {HoudiniIntlShape} from './HoudiniIntl';

View file

@ -0,0 +1,104 @@
import React from 'react';
import addons from '@storybook/addons';
import omit from 'lodash/omit';
import { HoudiniIntlProvider, HoudiniIntlShape} from '../../intl';
export let _config:any = null;
const EVENT_SET_CONFIG_ID = "intl/set_config";
const EVENT_GET_LOCALE_ID = "intl/get_locale";
const EVENT_SET_LOCALE_ID = "intl/set_locale";
class WithIntl extends React.Component<any,any> {
constructor (props:any) {
super(props);
this.state = {
locale: props.intlConfig.defaultLocale || null
};
this.setLocale = this.setLocale.bind(this);
const { channel } = this.props;
// Listen for change of locale
channel.on(EVENT_SET_LOCALE_ID, this.setLocale);
// Request the current locale
channel.emit(EVENT_GET_LOCALE_ID);
}
setLocale (locale:string) {
this.setState({
locale: locale
});
}
componentWillUnmount () {
this.props.channel.removeListener(EVENT_SET_LOCALE_ID, this.setLocale);
}
render () {
// If the component is not initialized we don't want to render anything
if (!this.state.locale) {
return null;
}
const {
children,
getMessages,
getFormats,
intlConfig
} = this.props;
const { locale } = this.state;
const messages = getMessages(locale);
const customProps:any = {
key: locale,
locale,
messages,
};
// if getFormats is not defined, we don't want to specify the formats property
if(getFormats) {
customProps.formats = getFormats(locale);
}
return (
<HoudiniIntlProvider {...intlConfig} {...customProps}>
{children}
</HoudiniIntlProvider>
);
}
}
export const setIntlConfig = (config:any) => {
_config = config;
const channel = addons.getChannel();
channel.emit(EVENT_SET_CONFIG_ID, {
locales: config.locales,
defaultLocale: config.defaultLocale
});
};
export const withIntl = (story:any) => {
const channel = addons.getChannel();
const intlConfig = omit(_config, ['locales', 'getMessages', 'getFormats']);
return (
<WithIntl intlConfig={intlConfig}
locales={_config.locales}
getMessages={_config.getMessages}
getFormats={_config.getFormats}
channel={channel}>
{story()}
</WithIntl>
);
};