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