diff --git a/apps/pxweb2/public/locales/en/translation.json b/apps/pxweb2/public/locales/en/translation.json
index 53fbe24a..e27af843 100644
--- a/apps/pxweb2/public/locales/en/translation.json
+++ b/apps/pxweb2/public/locales/en/translation.json
@@ -7,5 +7,15 @@
"date": {
"simple_date": "{{value, datetime}}",
"simple_date_with_time": "{{value, datetime(year: 'numeric'; month: 'numeric'; day: 'numeric'; hour: 'numeric'; minute: 'numeric')}}"
+ },
+ "number": {
+ "simple_number": "{{value, pxNumber}}",
+ "simple_number_with_zero_decimal": "{{value, pxNumber(minimumFractionDigits: 0; maximumFractionDigits: 0;)}}",
+ "simple_number_with_one_decimal": "{{value, pxNumber(minimumFractionDigits: 1; maximumFractionDigits: 1;)}}",
+ "simple_number_with_two_decimals": "{{value, pxNumber(minimumFractionDigits: 2; maximumFractionDigits: 2;)}}",
+ "simple_number_with_three_decimals": "{{value, pxNumber(minimumFractionDigits: 3; maximumFractionDigits: 3;)}}",
+ "simple_number_with_four_decimals": "{{value, pxNumber(minimumFractionDigits: 4; maximumFractionDigits: 4;)}}",
+ "simple_number_with_five_decimals": "{{value, pxNumber(minimumFractionDigits: 5; maximumFractionDigits: 5;)}}",
+ "simple_number_with_default_formatter": "{{value, number(minimumFractionDigits: 5; maximumFractionDigits: 5; roundingMode: 'halfExpand')}}"
}
}
diff --git a/apps/pxweb2/src/@types/resources.d.ts b/apps/pxweb2/src/@types/resources.d.ts
index e82d89e7..9bf25492 100644
--- a/apps/pxweb2/src/@types/resources.d.ts
+++ b/apps/pxweb2/src/@types/resources.d.ts
@@ -8,6 +8,16 @@ interface Resources {
"date": {
"simple_date": "{{value, datetime}}",
"simple_date_with_time": "{{value, datetime(year: 'numeric'; month: 'numeric'; day: 'numeric'; hour: 'numeric'; minute: 'numeric')}}"
+ },
+ "number": {
+ "simple_number": "{{value, pxNumber}}",
+ "simple_number_with_zero_decimal": "{{value, pxNumber(minimumFractionDigits: 0; maximumFractionDigits: 0;)}}",
+ "simple_number_with_one_decimal": "{{value, pxNumber(minimumFractionDigits: 1; maximumFractionDigits: 1;)}}",
+ "simple_number_with_two_decimals": "{{value, pxNumber(minimumFractionDigits: 2; maximumFractionDigits: 2;)}}",
+ "simple_number_with_three_decimals": "{{value, pxNumber(minimumFractionDigits: 3; maximumFractionDigits: 3;)}}",
+ "simple_number_with_four_decimals": "{{value, pxNumber(minimumFractionDigits: 4; maximumFractionDigits: 4;)}}",
+ "simple_number_with_five_decimals": "{{value, pxNumber(minimumFractionDigits: 5; maximumFractionDigits: 5;)}}",
+ "simple_number_with_default_formatter": "{{value, number(minimumFractionDigits: 5; maximumFractionDigits: 5; roundingMode: 'halfExpand')}}"
}
}
}
diff --git a/apps/pxweb2/src/app/app.tsx b/apps/pxweb2/src/app/app.tsx
index a71e77dc..94bc525e 100644
--- a/apps/pxweb2/src/app/app.tsx
+++ b/apps/pxweb2/src/app/app.tsx
@@ -7,9 +7,10 @@ import {
BodyLong,
Ingress,
Label,
- Tag
+ Tag,
} from '@pxweb2/pxweb2-ui';
import useLocalizeDocumentAttributes from '../i18n/useLocalizeDocumentAttributes';
+import { NumberFormatter } from '../i18n/formatters';
function test(event: React.MouseEvent
+
Test custom number formatter: {NumberFormatter(2000.6666666, 2)}
++ Simple number:{' '} + {t('number.simple_number', { + value: 2000.066666666, + })} +
++ Simple number with custom decimals:{' '} + {t('number.simple_number', { + value: 2000.00007, + minimumFractionDigits: customMinDecimals, + maximumFractionDigits: customMaxDecimals, + roundingMode: customRoundingMode, + })} +
++ Simple number with 0 decimals:{' '} + {t('number.simple_number_with_zero_decimal', { + value: 2000.044444444, + })} +
++ Simple number with 1 decimal:{' '} + {t('number.simple_number_with_one_decimal', { + value: 2000.044444444, + })} +
++ Simple number with 2 decimals:{' '} + {t('number.simple_number_with_two_decimals', { + value: 2000.044444444, + })} +
++ Simple number with 3 decimals:{' '} + {t('number.simple_number_with_three_decimals', { + value: 2000.044444444, + })} +
++ Simple number with 4 decimals:{' '} + {t('number.simple_number_with_four_decimals', { + value: 2000.044444444, + })} +
++ Simple number with 5 decimals:{' '} + {t('number.simple_number_with_five_decimals', { + value: 2000.044447444, + })} +
++ Round test:{' '} + {t('number.simple_number_with_one_decimal', { + value: 2.23, + })} +
++ {' '} + {t('number.simple_number_with_one_decimal', { + value: 2.25, + })} +
++ {' '} + {t('number.simple_number_with_one_decimal', { + value: 2.28, + })} +
++ {' '} + {t('number.simple_number_with_one_decimal', { + value: -2.23, + })} +
++ {' '} + {t('number.simple_number_with_one_decimal', { + value: -2.25, + })} +
++ {' '} + {t('number.simple_number_with_one_decimal', { + value: -2.28, + })} +
> ); } diff --git a/apps/pxweb2/src/i18n/config.ts b/apps/pxweb2/src/i18n/config.ts index 7a47c991..8ab8ca7c 100644 --- a/apps/pxweb2/src/i18n/config.ts +++ b/apps/pxweb2/src/i18n/config.ts @@ -2,6 +2,8 @@ import i18n from 'i18next'; import HttpApi from 'i18next-http-backend'; import { initReactI18next } from 'react-i18next'; +import { pxNumber } from './formatters'; + export const defaultNS = 'translation'; i18n @@ -10,7 +12,7 @@ i18n .init({ backend: { requestOptions: { - // Do not cache the response from the server. This is needed because site administrators + // Do not cache the response from the server. This is needed because site administrators // may want to change the translations without having to wait for the cache to expire. cache: 'no-store', }, @@ -27,4 +29,6 @@ i18n }, }); +i18n.services.formatter?.add('pxNumber', pxNumber); + export default i18n; diff --git a/apps/pxweb2/src/i18n/formatters.tsx b/apps/pxweb2/src/i18n/formatters.tsx new file mode 100644 index 00000000..0c90f23c --- /dev/null +++ b/apps/pxweb2/src/i18n/formatters.tsx @@ -0,0 +1,71 @@ +import { useTranslation } from 'react-i18next'; + +const customRoundingMode = 'halfExpand'; + +/* + * Custom number formatter + * @param value - number to format + * @param decimals - number of decimals, or minimum number of decimals if maxDecimals is set + * @param maxDecimals - maximum number of decimals + * @returns formatted number + * + * Example usage: + * NumberFormatter(2000.6666666, 2) + * NumberFormatter(2000.6666666, 2, 4) + * + * This can be used to format a number, and could be easier to use than + * the built-in number formatter in i18next when dealing with a large + * amount of numbers. + * + * It also set the rounding mode to the value of the variable customRoundingMode, + * which should be read from a configuration file. + */ +export function NumberFormatter( + value: number, + decimals: number, + maxDecimals?: number +): string { + const { i18n } = useTranslation(); + const max = maxDecimals ? maxDecimals : decimals; + + const nf = new Intl.NumberFormat(i18n.resolvedLanguage, { + minimumFractionDigits: decimals, + maximumFractionDigits: max, + + // Missing in the type definition in typescript, but a commit was merged to fix this + // in end of Feb, early March. So it should be available soon. + // @ts-expect-error Remove when typescript is updated with fix + roundingMode: customRoundingMode, + }); + + return nf.format(value); +} + +/* + * Custom number formatter + * @param value - number to format + * @param lng - language + * @param options - number format options + * @returns formatted number + * + * This custom formatter is to be used in translation files, in the same way as the built-in + * number formatter in i18next. But it also sets the rounding mode to the value of the variable + * customRoundingMode, which should be read from a configuration file. + */ +export function pxNumber( + value: number, + lng: string | undefined, + options?: Intl.NumberFormatOptions +): string { + if (!options) { + return new Intl.NumberFormat(lng, { + // @ts-expect-error See earlier comment. Remove when typescript is updated with fix + roundingMode: customRoundingMode, + }).format(value); + } + + // @ts-expect-error See earlier comment. Remove when typescript is updated with fix + options.roundingMode = customRoundingMode; + + return new Intl.NumberFormat(lng, options).format(value); +}