From e8cee14fe97f9f0a65472f61a6e286237276bf7d Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 14 Feb 2026 07:28:17 +0000 Subject: [PATCH 1/3] Initial plan From 37ee1465b7efaddc22d8a2d5f8aab299bb1f4c36 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 14 Feb 2026 07:33:44 +0000 Subject: [PATCH 2/3] Implement 3-2-2 comma rule support for number formatting Co-authored-by: gigincg <13028584+gigincg@users.noreply.github.com> --- frontend/src/metabase-types/api/settings.ts | 1 + .../components/widgets/FormattingWidget.tsx | 24 +++++++++++++++++-- .../src/metabase/lib/formatting/numbers.tsx | 9 +++++-- src/metabase/appearance/settings.clj | 16 +++++++++++-- .../util/formatting/internal/numbers.clj | 11 ++++++++- .../util/formatting/internal/numbers.cljs | 8 +++++-- 6 files changed, 60 insertions(+), 9 deletions(-) diff --git a/frontend/src/metabase-types/api/settings.ts b/frontend/src/metabase-types/api/settings.ts index 144685d3c472..40704c0c1d5c 100644 --- a/frontend/src/metabase-types/api/settings.ts +++ b/frontend/src/metabase-types/api/settings.ts @@ -24,6 +24,7 @@ export interface DateFormattingSettings { export interface NumberFormattingSettings { number_separators?: string; + number_grouping?: string; } export interface CurrencyFormattingSettings { diff --git a/frontend/src/metabase/admin/settings/components/widgets/FormattingWidget.tsx b/frontend/src/metabase/admin/settings/components/widgets/FormattingWidget.tsx index 841b75f21ae5..3d57a045b8a4 100644 --- a/frontend/src/metabase/admin/settings/components/widgets/FormattingWidget.tsx +++ b/frontend/src/metabase/admin/settings/components/widgets/FormattingWidget.tsx @@ -24,6 +24,7 @@ const DEFAULT_FORMATTING_SETTINGS: FormattingSettings = { }, "type/Number": { number_separators: ".,", + number_grouping: "western", }, "type/Currency": { currency: "USD", @@ -54,7 +55,7 @@ export function FormattingWidget() { time_style: timeStyle, } = localValue?.["type/Temporal"] || {}; - const { number_separators: numberSeparators } = + const { number_separators: numberSeparators, number_grouping: numberGrouping } = localValue?.["type/Number"] || {}; const { currency, currency_style: currencyStyle } = @@ -151,6 +152,25 @@ export function FormattingWidget() { /> + + handleChange({ + ...localValue, + "type/Number": { + ...localValue?.["type/Number"], + number_grouping: newValue as string, + }, + }) + } + /> handleChange({ diff --git a/frontend/src/metabase/lib/formatting/numbers.tsx b/frontend/src/metabase/lib/formatting/numbers.tsx index 2cc056bedb18..6f9c82545479 100644 --- a/frontend/src/metabase/lib/formatting/numbers.tsx +++ b/frontend/src/metabase/lib/formatting/numbers.tsx @@ -29,6 +29,7 @@ export type FormatNumberOptions = { minimumSignificantDigits?: number; negativeInParentheses?: boolean; number_separators?: string; + number_grouping?: string; number_style?: string; scale?: number; type?: string; @@ -184,9 +185,13 @@ export function numberFormatterForOptions(options: FormatNumberOptions) { ...getDefaultNumberOptions(options), ...options, }; - // always use "en" locale so we have known number separators we can replace depending on number_separators option + // Use locale based on number_grouping to get correct grouping pattern + // - "en-IN" for Indian numbering (3-2-2: 1,00,00,000) + // - "en" for Western numbering (3-3-3: 10,000,000) - default + // We use "en" as default so we have known number separators we can replace depending on number_separators option // TODO: if we do that how can we get localized currency names? - return new Intl.NumberFormat("en", { + const locale = options.number_grouping === "indian" ? "en-IN" : "en"; + return new Intl.NumberFormat(locale, { style: options.number_style as Intl.NumberFormatOptions["style"], currency: options.currency, currencyDisplay: diff --git a/src/metabase/appearance/settings.clj b/src/metabase/appearance/settings.clj index 4a02c48a8101..b2651d35ec45 100644 --- a/src/metabase/appearance/settings.clj +++ b/src/metabase/appearance/settings.clj @@ -350,7 +350,14 @@ See [fonts](../configuring-metabase/fonts.md).") ".," ",." ", " - ".’"}) + ".'"}) + +(def available-number-groupings + "Number grouping styles that are available for formatting numbers. + - western: 3-3-3 grouping (e.g., 10,000,000) + - indian: 3-2-2 grouping (e.g., 1,00,00,000)" + #{"western" + "indian"}) (defn- validate-custom-formatting! [new-value] @@ -358,7 +365,12 @@ See [fonts](../configuring-metabase/fonts.md).") (when-not (avaialable-number-separators separators) (throw (ex-info (tru "Invalid number separators.") {:separators separators - :available-separators avaialable-number-separators}))))) + :available-separators avaialable-number-separators})))) + (when-some [grouping (some-> new-value :type/Number :number_grouping)] + (when-not (available-number-groupings grouping) + (throw (ex-info (tru "Invalid number grouping.") + {:grouping grouping + :available-groupings available-number-groupings}))))) (defsetting custom-formatting (deferred-tru "Object keyed by type, containing formatting settings") diff --git a/src/metabase/util/formatting/internal/numbers.clj b/src/metabase/util/formatting/internal/numbers.clj index 5f29d7b2beff..64cdd74b1c60 100644 --- a/src/metabase/util/formatting/internal/numbers.clj +++ b/src/metabase/util/formatting/internal/numbers.clj @@ -46,8 +46,17 @@ #{:BTC}) (defn- active-locale [options] - (if (:locale options) + (cond + ;; Use Indian locale for Indian number grouping (3-2-2 pattern) + (= (:number-grouping options) "indian") + (Locale. "en" "IN") + + ;; Use explicit locale if specified + (:locale options) (Locale. (:locale options)) + + ;; Default to system locale + :else (Locale/getDefault))) (defn- number-formatter-for-options-baseline diff --git a/src/metabase/util/formatting/internal/numbers.cljs b/src/metabase/util/formatting/internal/numbers.cljs index 75a24c477ad1..d91a195ab6b9 100644 --- a/src/metabase/util/formatting/internal/numbers.cljs +++ b/src/metabase/util/formatting/internal/numbers.cljs @@ -49,9 +49,13 @@ ;; - "123,00 €" in actual German convention, which is what we would get with a native "de" locale here. (defn- number-formatter-for-options-baseline [options] (let [default-fraction-digits (when (= (:number-style options) "currency") - 2)] + 2) + ;; Use locale based on number-grouping to get correct grouping pattern + ;; - "en-IN" for Indian numbering (3-2-2: 1,00,00,000) + ;; - "en" for Western numbering (3-3-3: 10,000,000) - default + locale (if (= (:number-grouping options) "indian") "en-IN" "en")] (js/Intl.NumberFormat. - "en" + locale (clj->js (u/remove-nils {:style (when-not (= (:number-style options) "scientific") (:number-style options "decimal")) From 67c37249f346654a56fe666e81f6b61e644dafcb Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 14 Feb 2026 07:35:05 +0000 Subject: [PATCH 3/3] Add unit tests for 3-2-2 number grouping Co-authored-by: gigincg <13028584+gigincg@users.noreply.github.com> --- .../lib/formatting/numbers.unit.spec.ts | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/frontend/src/metabase/lib/formatting/numbers.unit.spec.ts b/frontend/src/metabase/lib/formatting/numbers.unit.spec.ts index 2492f8e0ccdd..591e315248bb 100644 --- a/frontend/src/metabase/lib/formatting/numbers.unit.spec.ts +++ b/frontend/src/metabase/lib/formatting/numbers.unit.spec.ts @@ -184,3 +184,120 @@ describe("formatNumber with scale (multiply function)", () => { expect(formatNumber(BigInt(-5), { scale: 2.5 })).toBe("-12.5"); }); }); + +describe("formatNumber with number_grouping", () => { + describe("Western grouping (3-3-3)", () => { + it("should format numbers with Western grouping (default)", () => { + expect(formatNumber(10000000)).toBe("10,000,000"); + expect(formatNumber(1000000)).toBe("1,000,000"); + expect(formatNumber(100000)).toBe("100,000"); + expect(formatNumber(10000)).toBe("10,000"); + expect(formatNumber(1000)).toBe("1,000"); + }); + + it("should format numbers with explicit Western grouping", () => { + expect(formatNumber(10000000, { number_grouping: "western" })).toBe( + "10,000,000", + ); + expect(formatNumber(1000000, { number_grouping: "western" })).toBe( + "1,000,000", + ); + expect(formatNumber(100000, { number_grouping: "western" })).toBe( + "100,000", + ); + }); + }); + + describe("Indian grouping (3-2-2)", () => { + it("should format numbers with Indian grouping", () => { + expect(formatNumber(10000000, { number_grouping: "indian" })).toBe( + "1,00,00,000", + ); + expect(formatNumber(1000000, { number_grouping: "indian" })).toBe( + "10,00,000", + ); + expect(formatNumber(100000, { number_grouping: "indian" })).toBe( + "1,00,000", + ); + expect(formatNumber(10000, { number_grouping: "indian" })).toBe("10,000"); + expect(formatNumber(1000, { number_grouping: "indian" })).toBe("1,000"); + }); + + it("should format large numbers with Indian grouping", () => { + expect(formatNumber(100000000, { number_grouping: "indian" })).toBe( + "10,00,00,000", + ); + expect(formatNumber(1000000000, { number_grouping: "indian" })).toBe( + "1,00,00,00,000", + ); + }); + + it("should format negative numbers with Indian grouping", () => { + expect(formatNumber(-10000000, { number_grouping: "indian" })).toBe( + "-1,00,00,000", + ); + expect(formatNumber(-1000000, { number_grouping: "indian" })).toBe( + "-10,00,000", + ); + }); + + it("should format decimal numbers with Indian grouping", () => { + expect( + formatNumber(10000000.5, { + number_grouping: "indian", + maximumFractionDigits: 2, + }), + ).toBe("1,00,00,000.5"); + expect( + formatNumber(1234567.89, { + number_grouping: "indian", + maximumFractionDigits: 2, + }), + ).toBe("12,34,567.89"); + }); + }); + + describe("Indian grouping with different separators", () => { + it("should combine Indian grouping with European separators", () => { + expect( + formatNumber(10000000, { + number_grouping: "indian", + number_separators: ",.", + }), + ).toBe("1.00.00.000"); + }); + + it("should combine Indian grouping with space separators", () => { + expect( + formatNumber(10000000, { + number_grouping: "indian", + number_separators: ", ", + }), + ).toBe("1 00 00 000"); + }); + }); + + describe("Indian grouping with currency", () => { + it("should format INR currency with Indian grouping", () => { + const result = formatNumber(10000000, { + number_style: "currency", + currency: "INR", + currency_style: "symbol", + number_grouping: "indian", + }); + // INR symbol should be present with Indian grouping + expect(result).toContain("1,00,00,000"); + }); + + it("should format USD currency with Indian grouping", () => { + const result = formatNumber(10000000, { + number_style: "currency", + currency: "USD", + currency_style: "symbol", + number_grouping: "indian", + }); + // USD should work with Indian grouping + expect(result).toContain("1,00,00,000"); + }); + }); +});