Add HoudiniIntl
This commit is contained in:
parent
79fe9042ac
commit
e1979d809f
4 changed files with 204 additions and 0 deletions
40
app/javascript/components/intl/HoudiniIntl.spec.tsx
Normal file
40
app/javascript/components/intl/HoudiniIntl.spec.tsx
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
57
app/javascript/components/intl/HoudiniIntl.tsx
Normal file
57
app/javascript/components/intl/HoudiniIntl.tsx
Normal 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};
|
||||||
|
}
|
3
app/javascript/components/intl/index.ts
Normal file
3
app/javascript/components/intl/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
// License: LGPL-3.0-or-later
|
||||||
|
export {useHoudiniIntl, HoudiniIntlProvider} from './HoudiniIntl';
|
||||||
|
export type {HoudiniIntlShape} from './HoudiniIntl';
|
104
app/javascript/components/tests/intl/index.tsx
Normal file
104
app/javascript/components/tests/intl/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue