1- import { format as d3Format , formatLocale , FormatLocaleDefinition } from 'd3-format' ;
1+ import { format as d3Format , formatLocale , FormatLocaleDefinition , FormatLocaleObject } from 'd3-format' ;
22import { timeFormat } from 'd3-time-format' ;
33
44import type { DimensionFormat , MeasureFormat , TCubeMemberType } from './types' ;
@@ -7,25 +7,39 @@ const DEFAULT_NUMBER_FORMAT = ',.2~f';
77const DEFAULT_CURRENCY_FORMAT = '$,.2~f' ;
88const DEFAULT_PERCENT_FORMAT = '.2~%' ;
99
10- // d3-format en-US defaults — serves as the base for all locales
11- const DEFAULT_LOCALE : FormatLocaleDefinition = {
12- decimal : '.' ,
13- thousands : ',' ,
14- grouping : [ 3 ] ,
15- currency : [ '$' , '' ] ,
16- } ;
10+ function getD3LocaleFromIntl ( locale ?: string , currencyCode = 'USD' ) : FormatLocaleDefinition {
11+ const nf = new Intl . NumberFormat ( locale ) ;
12+ const numParts = nf . formatToParts ( 1234567.89 ) ;
13+ const find = ( type : string ) => numParts . find ( ( p ) => p . type === type ) ?. value ?? '' ;
14+
15+ const cf = new Intl . NumberFormat ( locale , { style : 'currency' , currency : currencyCode } ) ;
16+ const currencyParts = cf . formatToParts ( 1 ) ;
17+ const currencySymbol = currencyParts . find ( ( p ) => p . type === 'currency' ) ?. value ?? currencyCode ;
18+ const firstMeaningfulType = currencyParts . find ( ( p ) => ! [ 'literal' , 'nan' ] . includes ( p . type ) ) ?. type ;
19+ const symbolIsPrefix = firstMeaningfulType === 'currency' ;
20+
21+ return {
22+ decimal : find ( 'decimal' ) || '.' ,
23+ thousands : find ( 'group' ) || ',' ,
24+ grouping : [ 3 ] ,
25+ currency : symbolIsPrefix ? [ currencySymbol , '' ] : [ '' , currencySymbol ] ,
26+ } ;
27+ }
1728
18- function getCurrencySymbol ( code : string ) : string {
19- return new Intl . NumberFormat ( 'en-US' , { style : 'currency' , currency : code } )
20- . formatToParts ( 0 )
21- . find ( ( part ) => part . type === 'currency' ) ?. value || code ;
29+ const localeCache : Record < string , FormatLocaleObject > = Object . create ( null ) ;
30+
31+ function getCurrentD3Locale ( locale : string , currencyCode = 'USD' ) : FormatLocaleObject {
32+ const key = `${ locale } :${ currencyCode } ` ;
33+ if ( localeCache [ key ] ) {
34+ return localeCache [ key ] ;
35+ }
36+
37+ localeCache [ key ] = formatLocale ( getD3LocaleFromIntl ( locale , currencyCode ) ) ;
38+ return localeCache [ key ] ;
2239}
2340
24- function createLocale ( currencyCode : string ) {
25- return formatLocale ( {
26- ...DEFAULT_LOCALE ,
27- currency : [ getCurrencySymbol ( currencyCode ) , '' ] ,
28- } ) ;
41+ function getCurrentLocale ( ) : string {
42+ return new Intl . NumberFormat ( ) . resolvedOptions ( ) . locale ;
2943}
3044
3145const DEFAULT_DATETIME_FORMAT = '%Y-%m-%d %H:%M:%S' ;
@@ -61,19 +75,23 @@ function parseNumber(value: any): number {
6175 return parseFloat ( value ) ;
6276}
6377
64- export type FormatValueOptions = {
78+ export type FormatValueMember = {
6579 type : TCubeMemberType ;
6680 format ?: DimensionFormat | MeasureFormat ;
6781 /** ISO 4217 currency code (e.g. 'USD', 'EUR'). Used when format is 'currency'. */
6882 currency ?: string ;
6983 /** Time dimension granularity (e.g. 'day', 'month', 'year'). Used for time formatting when no explicit format is set. */
7084 granularity ?: string ;
85+ } ;
86+
87+ export type FormatValueOptions = FormatValueMember & {
88+ locale ?: string ,
7189 emptyPlaceholder ?: string ;
7290} ;
7391
7492export function formatValue (
7593 value : any ,
76- { type, format, currency = 'USD' , granularity, emptyPlaceholder = '∅' } : FormatValueOptions
94+ { type, format, currency = 'USD' , granularity, locale = getCurrentLocale ( ) , emptyPlaceholder = '∅' } : FormatValueOptions
7795) : string {
7896 if ( value === null || value === undefined ) {
7997 return emptyPlaceholder ;
@@ -95,11 +113,11 @@ export function formatValue(
95113 if ( typeof format === 'string' ) {
96114 switch ( format ) {
97115 case 'currency' :
98- return createLocale ( currency ) . format ( DEFAULT_CURRENCY_FORMAT ) ( parseNumber ( value ) ) ;
116+ return getCurrentD3Locale ( locale , currency ) . format ( DEFAULT_CURRENCY_FORMAT ) ( parseNumber ( value ) ) ;
99117 case 'percent' :
100- return d3Format ( DEFAULT_PERCENT_FORMAT ) ( parseNumber ( value ) ) ;
118+ return getCurrentD3Locale ( locale , currency ) . format ( DEFAULT_PERCENT_FORMAT ) ( parseNumber ( value ) ) ;
101119 case 'number' :
102- return d3Format ( DEFAULT_NUMBER_FORMAT ) ( parseNumber ( value ) ) ;
120+ return getCurrentD3Locale ( locale , currency ) . format ( DEFAULT_NUMBER_FORMAT ) ( parseNumber ( value ) ) ;
103121 case 'imageUrl' :
104122 case 'id' :
105123 case 'link' :
@@ -115,7 +133,7 @@ export function formatValue(
115133 }
116134
117135 if ( type === 'number' ) {
118- return createLocale ( currency ) . format ( DEFAULT_NUMBER_FORMAT ) ( parseNumber ( value ) ) ;
136+ return getCurrentD3Locale ( locale , currency ) . format ( DEFAULT_NUMBER_FORMAT ) ( parseNumber ( value ) ) ;
119137 }
120138
121139 return String ( value ) ;
0 commit comments