This is an automated email from the ASF dual-hosted git repository.

bbovenzi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 71f98d3ac00 UI: Group digits of counters according to the selected 
locale (#73713)
71f98d3ac00 is described below

commit 71f98d3ac008ea4eeb808c82de4b5f4c1e60883d
Author: Shahar Epstein <[email protected]>
AuthorDate: Mon Oct 5 17:59:22 2026 +0300

    UI: Group digits of counters according to the selected locale (#73713)
    
    * UI: Group digits of counters according to the selected locale
    
    Counters such as the number of Dags, runs, task instances, import errors,
    pool slots or pagination pages were rendered as raw integers, so large
    values like 12345 were hard to read and ignored the conventions of the
    selected UI language (12,345 in English, 12.345 in German, 12 345 in 
French).
    
    Formatting goes through one shared hook backed by the cached Intl fallback
    already used for durations, because plugins can contribute UI languages
    whose tags Intl rejects (e.g. "pt_BR"); calling toLocaleString with such a
    tag throws a RangeError and would crash every page that shows a counter.
    
    The Dag warnings count now includes the parse error as originally intended,
    and the Dag header no longer shows "null" when max active runs is unset.
    
    * UI: Format counters with a plain utility instead of a hook
    
    A dedicated hook is more ceremony than digit grouping needs: components
    read the active language from `useTranslation` and pass it to a plain
    function, as the two counters on main already did through
    `toLocaleString`. The language is a required argument rather than a default
    read from the i18next singleton inside the function because the React
    Compiler memoizes the call on its arguments, so a language read inside
    would not invalidate a counter whose memoized output depends only on the
    number.
    
    * UI: Count the page error as one Dag warning instead of NaN
    
    Number(<Error>) is NaN, so after the precedence fix in the first commit the
    warnings button read "Dag Warnings (NaN)" whenever a Dag, run or task page
    failed to load. The old precedence bug had masked it once warnings loaded.
    
    The grouping tests also pin the language through a react-i18next mock:
    without an i18next instance, formatNumber falls through to the host ICU
    locale, which is "de-DE" on a German dev box and breaks the "1,234"
    assertions.
    
    * UI: Cover the warnings count and the unlimited active-runs header with 
tests
    
    Both behaviour fixes shipped without a regression test. The warnings count
    moves into countDagWarnings next to the modal that defines what the count
    lists, so it can be tested without rendering the whole details layout.
    
    The three Dag-list selection counters left out of the first commit now group
    digits like the run and task instance selection counters do.
    
    * UI: Group the remaining Dag detail counters and keep system components 
off the utils barrel
    
    The calendar backfill count and the Dag details limits were the last
    counters rendered outside translations without grouping, so the header read
    "0 of 1,000" while the details tab read "1000".
    
    system-components importing the src/utils barrel created an import cycle
    through errorHandling (which imports the toaster from system-components)
    and pulled DataTable into the lowest UI layer; importing formatNumber from
    its own module breaks the cycle.
---
 .../components/ActionAccordion/ActionAccordion.tsx |  6 ++-
 .../src/components/AssetExpression/AssetNode.tsx   |  7 ++-
 .../ui/src/components/AssetProgressCell.tsx        |  6 ++-
 .../ui/src/components/Assets/AssetEvents.tsx       |  6 ++-
 .../ui/src/components/Assets/ListPopover.tsx       | 46 ++++++++++---------
 .../ui/src/components/Assets/TriggeredRuns.tsx     |  6 ++-
 ...ortErrorCount.tsx => DAGWarningsModal.test.tsx} | 36 ++++++---------
 .../airflow/ui/src/components/DAGWarningsModal.tsx |  4 ++
 .../components/DagActions/RunBackfillForm.test.tsx |  1 +
 .../ui/src/components/DataTable/DataTable.tsx      |  4 +-
 .../ui/src/components/ImportErrorCount.test.tsx    | 45 +++++++++++++++++++
 .../airflow/ui/src/components/ImportErrorCount.tsx |  7 ++-
 .../airflow/ui/src/components/NeedsReviewBadge.tsx |  6 ++-
 .../src/airflow/ui/src/components/PoolBar.test.tsx | 24 +++++++++-
 .../src/airflow/ui/src/components/PoolBar.tsx      |  7 +--
 .../ui/src/components/RollupKeyChecklist.tsx       | 51 ++++++++++++----------
 .../{ImportErrorCount.tsx => StatsCard.test.tsx}   | 44 +++++++++----------
 .../src/airflow/ui/src/components/StatsCard.tsx    |  7 ++-
 .../ui/src/components/TaskInstanceTooltip.test.tsx | 30 ++++++++++++-
 .../ui/src/components/TaskInstanceTooltip.tsx      |  6 +--
 .../src/airflow/ui/src/components/TaskName.tsx     |  9 +++-
 .../airflow/ui/src/components/TrendCountButton.tsx | 12 +++--
 .../ui/src/layouts/Details/DetailsLayout.tsx       |  6 +--
 .../ui/src/pages/AssetsList/DependencyPopover.tsx  |  6 ++-
 .../ui/src/pages/Connections/Connections.tsx       |  6 +--
 .../ui/src/pages/Dag/Calendar/CalendarTooltip.tsx  |  8 ++--
 .../src/airflow/ui/src/pages/Dag/Details.tsx       | 12 ++---
 .../src/airflow/ui/src/pages/Dag/Header.test.tsx   | 12 +++++
 .../src/airflow/ui/src/pages/Dag/Header.tsx        |  7 ++-
 .../airflow/ui/src/pages/DagBundle/BundleFiles.tsx |  7 ++-
 .../src/airflow/ui/src/pages/DagBundle/Header.tsx  |  5 ++-
 .../src/pages/DagRuns/BulkDeleteDagRunsButton.tsx  |  5 ++-
 .../src/pages/DagRuns/BulkMarkDagRunsAsButton.tsx  |  5 ++-
 .../src/airflow/ui/src/pages/DagRuns/DagRuns.tsx   |  6 +--
 .../pages/DagsList/BulkPauseDrainDagsButton.tsx    |  5 ++-
 .../src/pages/DagsList/BulkUnpauseDagsButton.tsx   |  5 ++-
 .../src/pages/DagsList/DagRunStateCounts.test.tsx  | 13 ++++--
 .../ui/src/pages/DagsList/DagRunStateCounts.tsx    |  5 ++-
 .../src/airflow/ui/src/pages/DagsList/DagsList.tsx |  6 +--
 .../Dashboard/HistoricalMetrics/MetricSection.tsx  |  3 +-
 .../Dashboard/PoolSummary/PoolSummary.test.tsx     |  1 +
 .../src/pages/Dashboard/Stats/DagImportErrors.tsx  |  4 +-
 .../pages/Dashboard/Stats/PluginImportErrors.tsx   |  4 +-
 .../ui/src/pages/Dashboard/Stats/Stats.test.tsx    |  5 ++-
 .../ui/src/pages/GroupTaskInstance/Header.tsx      |  6 +--
 .../ui/src/pages/MappedTaskInstance/Details.tsx    |  6 +--
 .../ui/src/pages/MappedTaskInstance/Header.tsx     |  8 ++--
 .../MappedTaskInstance/MappedTaskInstance.tsx      | 10 +++--
 .../src/airflow/ui/src/pages/Pools/PoolBarCard.tsx |  7 +--
 .../BulkDeleteTaskInstancesButton.tsx              |  5 ++-
 .../BulkMarkTaskInstancesAsButton.tsx              |  5 ++-
 .../ui/src/pages/TaskInstances/TaskInstances.tsx   |  6 +--
 .../airflow/ui/src/pages/Variables/Variables.tsx   |  6 +--
 .../ui/src/system-components/Pagination/Item.tsx   |  6 ++-
 .../src/system-components/Pagination/PageText.tsx  | 10 +++--
 .../ui/src/system-components/Select/ValueText.tsx  |  6 ++-
 .../src/airflow/ui/src/utils/datetimeUtils.ts      | 31 +------------
 .../formatNumber.test.ts}                          | 35 ++++++---------
 .../ImportErrorCount.tsx => utils/formatNumber.ts} | 28 ++++--------
 airflow-core/src/airflow/ui/src/utils/index.ts     |  1 +
 airflow-core/src/airflow/ui/src/utils/intlCache.ts | 48 ++++++++++++++++++++
 61 files changed, 471 insertions(+), 259 deletions(-)

diff --git 
a/airflow-core/src/airflow/ui/src/components/ActionAccordion/ActionAccordion.tsx
 
b/airflow-core/src/airflow/ui/src/components/ActionAccordion/ActionAccordion.tsx
index 234b46bc561..768b148325f 100644
--- 
a/airflow-core/src/airflow/ui/src/components/ActionAccordion/ActionAccordion.tsx
+++ 
b/airflow-core/src/airflow/ui/src/components/ActionAccordion/ActionAccordion.tsx
@@ -31,6 +31,8 @@ import { Accordion } from "src/system-components";
 
 import ReactMarkdown from "src/components/ReactMarkdown";
 
+import { formatNumber } from "src/utils";
+
 import { DataTable } from "../DataTable";
 import { getColumns, type RowSelection } from "./columns";
 
@@ -71,7 +73,7 @@ const TasksTable = ({
 // TODO: Make a front-end only unconnected table component with client side 
ordering and pagination
 const ActionAccordion = ({ affectedTasks, groupByRunId = false, note, 
selection, setNote }: Props) => {
   const showTaskSection = affectedTasks !== undefined;
-  const { t: translate } = useTranslation();
+  const { i18n, t: translate } = useTranslation();
 
   // Group task instances by dag_run_id when requested
   const runGroups = (() => {
@@ -120,7 +122,7 @@ const ActionAccordion = ({ affectedTasks, groupByRunId = 
false, note, selection,
                         <Text fontSize="sm" fontWeight="semibold">
                           {translate("runId")}: {runId}{" "}
                           <Text as="span" color="fg.subtle" 
fontWeight="normal">
-                            ({tis.length})
+                            ({formatNumber(tis.length, i18n.language)})
                           </Text>
                         </Text>
                       </Accordion.ItemTrigger>
diff --git 
a/airflow-core/src/airflow/ui/src/components/AssetExpression/AssetNode.tsx 
b/airflow-core/src/airflow/ui/src/components/AssetExpression/AssetNode.tsx
index 201319f71bb..b5398ef10bc 100644
--- a/airflow-core/src/airflow/ui/src/components/AssetExpression/AssetNode.tsx
+++ b/airflow-core/src/airflow/ui/src/components/AssetExpression/AssetNode.tsx
@@ -17,6 +17,7 @@
  * under the License.
  */
 import { Box, HStack, Text } from "@chakra-ui/react";
+import { useTranslation } from "react-i18next";
 import { FiDatabase } from "react-icons/fi";
 import { PiRectangleDashed } from "react-icons/pi";
 
@@ -30,6 +31,8 @@ import { RouterLink } from "src/system-components";
 
 import { RollupKeyChecklistPopover } from "src/components/RollupKeyChecklist";
 
+import { formatNumber } from "src/utils";
+
 import Time from "../Time";
 
 export const AssetNode = ({
@@ -39,6 +42,7 @@ export const AssetNode = ({
   readonly asset: AssetExpressionAlias | AssetExpressionAsset;
   readonly event?: NextRunAssetEventResponse;
 }) => {
+  const { i18n } = useTranslation();
   const isFullyReceived = Boolean(event?.last_update);
   const isPartial =
     !isFullyReceived &&
@@ -90,7 +94,8 @@ export const AssetNode = ({
         </Text>
       ) : isPartial ? (
         <Text color="warning.fg" fontSize="sm">
-          {event?.received_count} / {event?.required_count}
+          {formatNumber(event?.received_count ?? 0, i18n.language)} /{" "}
+          {formatNumber(event?.required_count ?? 0, i18n.language)}
         </Text>
       ) : undefined}
     </Box>
diff --git a/airflow-core/src/airflow/ui/src/components/AssetProgressCell.tsx 
b/airflow-core/src/airflow/ui/src/components/AssetProgressCell.tsx
index 3ef87d28126..bf62e40b569 100644
--- a/airflow-core/src/airflow/ui/src/components/AssetProgressCell.tsx
+++ b/airflow-core/src/airflow/ui/src/components/AssetProgressCell.tsx
@@ -17,6 +17,7 @@
  * under the License.
  */
 import { Button, HStack, VStack } from "@chakra-ui/react";
+import { useTranslation } from "react-i18next";
 import { FiDatabase } from "react-icons/fi";
 
 import { usePartitionedDagRunServiceGetPendingPartitionedDagRun } from 
"openapi/queries";
@@ -27,6 +28,8 @@ import { Popover, RouterLink } from "src/system-components";
 import { AssetExpression } from "src/components/AssetExpression";
 import { RollupKeyChecklist } from "src/components/RollupKeyChecklist";
 
+import { formatNumber } from "src/utils";
+
 type Props = {
   readonly dagId: string;
   readonly partitionKey: string;
@@ -35,6 +38,7 @@ type Props = {
 };
 
 export const AssetProgressCell = ({ dagId, partitionKey, totalReceived, 
totalRequired }: Props) => {
+  const { i18n } = useTranslation();
   const { data, isLoading } = 
usePartitionedDagRunServiceGetPendingPartitionedDagRun({ dagId, partitionKey });
 
   const assetExpression = data?.asset_expression ?? undefined;
@@ -60,7 +64,7 @@ export const AssetProgressCell = ({ dagId, partitionKey, 
totalReceived, totalReq
       <Popover.Trigger asChild>
         <Button loading={isLoading} paddingInline={0} variant="ghost">
           <FiDatabase style={{ display: "inline" }} />
-          {`${String(totalReceived)} / ${String(totalRequired)}`}
+          {`${formatNumber(totalReceived, i18n.language)} / 
${formatNumber(totalRequired, i18n.language)}`}
         </Button>
       </Popover.Trigger>
       <Popover.Content css={{ "--popover-bg": "colors.bg.emphasized" }} 
width="fit-content">
diff --git a/airflow-core/src/airflow/ui/src/components/Assets/AssetEvents.tsx 
b/airflow-core/src/airflow/ui/src/components/Assets/AssetEvents.tsx
index d695090abe9..201c6974d7f 100644
--- a/airflow-core/src/airflow/ui/src/components/Assets/AssetEvents.tsx
+++ b/airflow-core/src/airflow/ui/src/components/Assets/AssetEvents.tsx
@@ -26,6 +26,8 @@ import { Select } from "src/system-components";
 
 import { StateBadge } from "src/components/StateBadge";
 
+import { formatNumber } from "src/utils";
+
 import { DataTable } from "../DataTable";
 import type { CardDef, TableState } from "../DataTable/types";
 import { AssetEvent } from "./AssetEvent";
@@ -60,7 +62,7 @@ export const AssetEvents = ({
   titleKey,
   ...rest
 }: AssetEventProps & BoxProps) => {
-  const { t: translate } = useTranslation(["dashboard", "common", "dag"]);
+  const { i18n, t: translate } = useTranslation(["dashboard", "common", 
"dag"]);
   const assetSortOptions = createListCollection({
     items: [
       { label: translate("sortBy.newestFirst"), value: "-timestamp" },
@@ -80,7 +82,7 @@ export const AssetEvents = ({
           <HStack>
             <StateBadge colorPalette="brand" fontSize="md" variant="solid">
               <FiDatabase />
-              {data?.total_entries ?? " "}
+              {data?.total_entries === undefined ? " " : 
formatNumber(data.total_entries, i18n.language)}
             </StateBadge>
             <Heading marginEnd="auto" size="md">
               {translate(titleKey ?? "common:assetEvent", { count: 
data?.total_entries ?? 0 })}
diff --git a/airflow-core/src/airflow/ui/src/components/Assets/ListPopover.tsx 
b/airflow-core/src/airflow/ui/src/components/Assets/ListPopover.tsx
index c2df6359873..5bb5157bdbc 100644
--- a/airflow-core/src/airflow/ui/src/components/Assets/ListPopover.tsx
+++ b/airflow-core/src/airflow/ui/src/components/Assets/ListPopover.tsx
@@ -27,31 +27,37 @@ import { Popover } from "src/system-components";
 
 import Time from "src/components/Time";
 
+import { formatNumber } from "src/utils";
+
 type ListPopoverProps = {
   readonly items: Array<{ key: string; label: ReactNode }>;
   readonly noun: string;
 };
 
-const ListPopover = ({ items, noun }: ListPopoverProps) => (
-  // eslint-disable-next-line jsx-a11y/no-autofocus
-  <Popover.Root autoFocus={false} lazyMount unmountOnExit>
-    <Popover.Trigger asChild disabled={items.length === 0}>
-      <Button variant="outline">
-        {items.length} {noun}
-      </Button>
-    </Popover.Trigger>
-    <Popover.Content css={{ "--popover-bg": "colors.bg.emphasized" }} 
width="fit-content">
-      <Popover.Arrow />
-      <Popover.Body>
-        {items.map(({ key, label }) => (
-          <Text key={key} py={2}>
-            {label}
-          </Text>
-        ))}
-      </Popover.Body>
-    </Popover.Content>
-  </Popover.Root>
-);
+const ListPopover = ({ items, noun }: ListPopoverProps) => {
+  const { i18n } = useTranslation();
+
+  return (
+    // eslint-disable-next-line jsx-a11y/no-autofocus
+    <Popover.Root autoFocus={false} lazyMount unmountOnExit>
+      <Popover.Trigger asChild disabled={items.length === 0}>
+        <Button variant="outline">
+          {formatNumber(items.length, i18n.language)} {noun}
+        </Button>
+      </Popover.Trigger>
+      <Popover.Content css={{ "--popover-bg": "colors.bg.emphasized" }} 
width="fit-content">
+        <Popover.Arrow />
+        <Popover.Body>
+          {items.map(({ key, label }) => (
+            <Text key={key} py={2}>
+              {label}
+            </Text>
+          ))}
+        </Popover.Body>
+      </Popover.Content>
+    </Popover.Root>
+  );
+};
 
 export const AliasesPopover = ({ aliases }: { readonly aliases: 
Array<AssetAliasResponse> }) => {
   const { t: translate } = useTranslation("assets");
diff --git 
a/airflow-core/src/airflow/ui/src/components/Assets/TriggeredRuns.tsx 
b/airflow-core/src/airflow/ui/src/components/Assets/TriggeredRuns.tsx
index 439518e11cc..b93327c5756 100644
--- a/airflow-core/src/airflow/ui/src/components/Assets/TriggeredRuns.tsx
+++ b/airflow-core/src/airflow/ui/src/components/Assets/TriggeredRuns.tsx
@@ -23,6 +23,8 @@ import type { DagRunAssetReference, DagRunState } from 
"openapi/requests/types.g
 
 import { Popover, RouterLink } from "src/system-components";
 
+import { formatNumber } from "src/utils";
+
 import { StateBadge } from "../StateBadge";
 
 type Props = {
@@ -36,7 +38,7 @@ const DagRunGroup = ({
   readonly dagRuns: Array<DagRunAssetReference>;
   readonly prefix: string;
 }) => {
-  const { t: translate } = useTranslation();
+  const { i18n, t: translate } = useTranslation();
 
   return dagRuns.length === 1 ? (
     <Flex flexWrap="wrap" gap={1}>
@@ -51,7 +53,7 @@ const DagRunGroup = ({
     <Popover.Root autoFocus={false} lazyMount unmountOnExit>
       <Popover.Trigger asChild>
         <Button variant="outline">
-          {`${dagRuns.length} ${prefix} ${translate("dagRun_other", { count: 
dagRuns.length })}`}
+          {`${formatNumber(dagRuns.length, i18n.language)} ${prefix} 
${translate("dagRun_other", { count: dagRuns.length })}`}
         </Button>
       </Popover.Trigger>
       <Popover.Content css={{ "--popover-bg": "colors.bg.emphasized" }} 
width="fit-content">
diff --git a/airflow-core/src/airflow/ui/src/components/ImportErrorCount.tsx 
b/airflow-core/src/airflow/ui/src/components/DAGWarningsModal.test.tsx
similarity index 57%
copy from airflow-core/src/airflow/ui/src/components/ImportErrorCount.tsx
copy to airflow-core/src/airflow/ui/src/components/DAGWarningsModal.test.tsx
index 8459b9cf4d2..fbd67b53b4a 100644
--- a/airflow-core/src/airflow/ui/src/components/ImportErrorCount.tsx
+++ b/airflow-core/src/airflow/ui/src/components/DAGWarningsModal.test.tsx
@@ -16,28 +16,18 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Badge, Text } from "@chakra-ui/react";
+import { describe, expect, it } from "vitest";
 
-type Props = {
-  readonly count: number | null;
-};
+import { countDagWarnings } from "./DAGWarningsModal";
 
-/**
- * An import-error count, flagged red only when there is something to flag.
- *
- * Null is not zero: it means the caller may not read import errors, so it 
renders as unknown
- * rather than as a clean bill of health.
- */
-export const ImportErrorCount = ({ count }: Props) => {
-  if (count === null) {
-    return <Text color="fg.muted">-</Text>;
-  }
-
-  return count === 0 ? (
-    <Text color="fg.muted">0</Text>
-  ) : (
-    <Badge colorPalette="failed" variant="solid">
-      {count}
-    </Badge>
-  );
-};
+describe("countDagWarnings", () => {
+  it.each([
+    [undefined, null, 0],
+    [3, null, 3],
+    [undefined, new Error("boom"), 1],
+    [0, new Error("boom"), 1],
+    [3, new Error("boom"), 4],
+  ])("counts %s warnings and error %s as %s", (warningsTotal, error, expected) 
=> {
+    expect(countDagWarnings(warningsTotal, error)).toBe(expected);
+  });
+});
diff --git a/airflow-core/src/airflow/ui/src/components/DAGWarningsModal.tsx 
b/airflow-core/src/airflow/ui/src/components/DAGWarningsModal.tsx
index 28f49a89194..15bdc0a986e 100644
--- a/airflow-core/src/airflow/ui/src/components/DAGWarningsModal.tsx
+++ b/airflow-core/src/airflow/ui/src/components/DAGWarningsModal.tsx
@@ -27,6 +27,10 @@ import { Modal } from "src/system-components";
 import { ErrorAlert } from "./ErrorAlert";
 import { WarningAlert } from "./WarningAlert";
 
+/** The modal lists the page error as one more item next to the warnings, so 
the button count does too. */
+export const countDagWarnings = (warningsTotal: number | undefined, error: 
unknown): number =>
+  (warningsTotal ?? 0) + Number(Boolean(error));
+
 type ImportDAGErrorModalProps = {
   readonly error?: unknown;
   readonly onClose: () => void;
diff --git 
a/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.test.tsx
 
b/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.test.tsx
index cdc96f3de2a..8b686f6f6f6 100644
--- 
a/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.test.tsx
+++ 
b/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.test.tsx
@@ -28,6 +28,7 @@ import RunBackfillForm from "./RunBackfillForm";
 
 vi.mock("react-i18next", () => ({
   useTranslation: () => ({
+    i18n: { language: "en" },
     // eslint-disable-next-line id-length
     t: (key: string, opts?: { count?: number; dag_display_name?: string }) => {
       const map: Record<string, string> = {
diff --git a/airflow-core/src/airflow/ui/src/components/DataTable/DataTable.tsx 
b/airflow-core/src/airflow/ui/src/components/DataTable/DataTable.tsx
index d88b53858a7..8f1e1acf680 100644
--- a/airflow-core/src/airflow/ui/src/components/DataTable/DataTable.tsx
+++ b/airflow-core/src/airflow/ui/src/components/DataTable/DataTable.tsx
@@ -42,6 +42,8 @@ import { ToggleTableDisplay } from 
"src/components/DataTable/ToggleTableDisplay"
 import { createSkeletonMock } from "src/components/DataTable/skeleton";
 import type { CardDef, MetaColumn, TableState } from 
"src/components/DataTable/types";
 
+import { formatNumber } from "src/utils";
+
 type DataTableProps<TData> = {
   readonly cardDef?: CardDef<TData>;
   readonly columns: Array<MetaColumn<TData>>;
@@ -231,7 +233,7 @@ export const DataTable = <TData,>({
   const headingNode = Boolean(hideRowCountHeading) ? undefined : (
     <Heading py={1} size="md">
       {hasRowCount
-        ? `${total.toLocaleString(i18n.language)}${isCapped ? "+" : ""} 
${translateModelName(total)}`
+        ? `${formatNumber(total, i18n.language)}${isCapped ? "+" : ""} 
${translateModelName(total)}`
         : pluralModelName}
     </Heading>
   );
diff --git 
a/airflow-core/src/airflow/ui/src/components/ImportErrorCount.test.tsx 
b/airflow-core/src/airflow/ui/src/components/ImportErrorCount.test.tsx
new file mode 100644
index 00000000000..56136e9f51a
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/components/ImportErrorCount.test.tsx
@@ -0,0 +1,45 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import "@testing-library/jest-dom";
+import { act, render, screen } from "@testing-library/react";
+import i18n from "i18next";
+import { initReactI18next } from "react-i18next";
+import { beforeAll, describe, expect, it } from "vitest";
+
+import { Wrapper } from "src/utils/Wrapper";
+
+import { ImportErrorCount } from "./ImportErrorCount";
+
+describe("ImportErrorCount", () => {
+  beforeAll(async () => {
+    await i18n.use(initReactI18next).init({ lng: "en", resources: { de: {}, 
en: {} } });
+  });
+
+  it("groups thousands in the count and regroups on a language switch", async 
() => {
+    render(<ImportErrorCount count={1234} />, { wrapper: Wrapper });
+
+    expect(screen.getByText("1,234")).toBeInTheDocument();
+
+    await act(async () => {
+      await i18n.changeLanguage("de");
+    });
+
+    expect(screen.getByText("1.234")).toBeInTheDocument();
+  });
+});
diff --git a/airflow-core/src/airflow/ui/src/components/ImportErrorCount.tsx 
b/airflow-core/src/airflow/ui/src/components/ImportErrorCount.tsx
index 8459b9cf4d2..f4666691b31 100644
--- a/airflow-core/src/airflow/ui/src/components/ImportErrorCount.tsx
+++ b/airflow-core/src/airflow/ui/src/components/ImportErrorCount.tsx
@@ -17,6 +17,9 @@
  * under the License.
  */
 import { Badge, Text } from "@chakra-ui/react";
+import { useTranslation } from "react-i18next";
+
+import { formatNumber } from "src/utils";
 
 type Props = {
   readonly count: number | null;
@@ -29,6 +32,8 @@ type Props = {
  * rather than as a clean bill of health.
  */
 export const ImportErrorCount = ({ count }: Props) => {
+  const { i18n } = useTranslation();
+
   if (count === null) {
     return <Text color="fg.muted">-</Text>;
   }
@@ -37,7 +42,7 @@ export const ImportErrorCount = ({ count }: Props) => {
     <Text color="fg.muted">0</Text>
   ) : (
     <Badge colorPalette="failed" variant="solid">
-      {count}
+      {formatNumber(count, i18n.language)}
     </Badge>
   );
 };
diff --git a/airflow-core/src/airflow/ui/src/components/NeedsReviewBadge.tsx 
b/airflow-core/src/airflow/ui/src/components/NeedsReviewBadge.tsx
index f472b3cf225..14da7a5c9de 100644
--- a/airflow-core/src/airflow/ui/src/components/NeedsReviewBadge.tsx
+++ b/airflow-core/src/airflow/ui/src/components/NeedsReviewBadge.tsx
@@ -28,12 +28,14 @@ import { Tooltip } from "src/system-components";
 import { HITLReviewModal } from 
"src/components/HITLReview/HITLReviewModal.tsx";
 import { StateBadge } from "src/components/StateBadge";
 
+import { formatNumber } from "src/utils";
+
 type Props = {
   readonly pendingActions: Array<HITLDetail>;
 };
 
 export const NeedsReviewBadge = ({ pendingActions }: Props) => {
-  const { t: translate } = useTranslation("hitl");
+  const { i18n, t: translate } = useTranslation("hitl");
   const { onClose, onOpen, open } = useDisclosure();
 
   if (pendingActions.length === 0) {
@@ -46,7 +48,7 @@ export const NeedsReviewBadge = ({ pendingActions }: Props) 
=> {
         <Button data-testid="needs-review-badge" onClick={onOpen} 
variant="plain">
           <StateBadge colorPalette="awaiting_input" fontSize="md" 
variant="solid">
             <LuUserRoundPen />
-            {pendingActions.length}
+            {formatNumber(pendingActions.length, i18n.language)}
           </StateBadge>
         </Button>
       </Tooltip>
diff --git a/airflow-core/src/airflow/ui/src/components/PoolBar.test.tsx 
b/airflow-core/src/airflow/ui/src/components/PoolBar.test.tsx
index 7e2ee75a316..04b2e5bb47c 100644
--- a/airflow-core/src/airflow/ui/src/components/PoolBar.test.tsx
+++ b/airflow-core/src/airflow/ui/src/components/PoolBar.test.tsx
@@ -18,7 +18,7 @@
  */
 import "@testing-library/jest-dom";
 import { render, screen } from "@testing-library/react";
-import { describe, expect, it } from "vitest";
+import { describe, expect, it, vi } from "vitest";
 
 import type { PoolResponse } from "openapi/requests/types.gen";
 
@@ -26,6 +26,14 @@ import { Wrapper } from "src/utils/Wrapper";
 
 import { PoolBar } from "./PoolBar";
 
+vi.mock("react-i18next", () => ({
+  useTranslation: () => ({
+    i18n: { language: "en" },
+    // eslint-disable-next-line id-length
+    t: (key: string) => key,
+  }),
+}));
+
 const createPool = (pool: Partial<PoolResponse>): PoolResponse => ({
   deferred_slots: 1,
   description: null,
@@ -63,4 +71,18 @@ describe("PoolBar", () => {
     
expect(container.querySelector('a[href*="task_state=deferred"]')).toBeInTheDocument();
     expect(screen.getByText("1")).toBeInTheDocument();
   });
+
+  it("groups thousands in slot counts", () => {
+    render(
+      <PoolBar
+        pool={createPool({ deferred_slots: 2345, open_slots: 8766, 
running_slots: 1234, slots: 10_000 })}
+        totalSlots={10_000}
+      />,
+      { wrapper: Wrapper },
+    );
+
+    expect(screen.getByText("1,234")).toBeInTheDocument();
+    expect(screen.getByText("8,766")).toBeInTheDocument();
+    
expect(screen.getByText(/common:states\.deferred/u)).toHaveTextContent("2,345");
+  });
 });
diff --git a/airflow-core/src/airflow/ui/src/components/PoolBar.tsx 
b/airflow-core/src/airflow/ui/src/components/PoolBar.tsx
index b813a22aa7a..777d55f6e93 100644
--- a/airflow-core/src/airflow/ui/src/components/PoolBar.tsx
+++ b/airflow-core/src/airflow/ui/src/components/PoolBar.tsx
@@ -27,6 +27,7 @@ import { Tooltip } from "src/system-components";
 import { StateIcon } from "src/components/StateIcon";
 
 import { SearchParamsKeys } from "src/constants/searchParams";
+import { formatNumber } from "src/utils";
 import { type Slots, slotConfigs } from "src/utils/slots";
 
 export const UNLIMITED_SLOTS = -1;
@@ -40,7 +41,7 @@ export const PoolBar = ({
   readonly poolsWithSlotType?: Slots;
   readonly totalSlots: number;
 }) => {
-  const { t: translate } = useTranslation();
+  const { i18n, t: translate } = useTranslation();
 
   const isUnlimited = totalSlots === UNLIMITED_SLOTS;
   const isDashboard = Boolean(poolsWithSlotType);
@@ -100,7 +101,7 @@ export const PoolBar = ({
               >
                 {slot.icon}
                 <Text fontSize="xs" fontWeight="bold" truncate>
-                  {slot.slotValue === Infinity ? "∞" : slot.slotValue}
+                  {slot.slotValue === Infinity ? "∞" : 
formatNumber(slot.slotValue, i18n.language)}
                 </Text>
               </Flex>
             </Tooltip>
@@ -129,7 +130,7 @@ export const PoolBar = ({
             <HStack gap={1} key={slot.key}>
               <StateIcon size={12} state={slot.slotType as TaskInstanceState} 
/>
               <Text color="fg.muted" fontSize="xs" fontWeight="medium">
-                {slot.label}: {slot.slotValue}
+                {slot.label}: {formatNumber(slot.slotValue, i18n.language)}
               </Text>
             </HStack>
           ))}
diff --git a/airflow-core/src/airflow/ui/src/components/RollupKeyChecklist.tsx 
b/airflow-core/src/airflow/ui/src/components/RollupKeyChecklist.tsx
index ee7e619890c..9337ca06542 100644
--- a/airflow-core/src/airflow/ui/src/components/RollupKeyChecklist.tsx
+++ b/airflow-core/src/airflow/ui/src/components/RollupKeyChecklist.tsx
@@ -17,10 +17,13 @@
  * under the License.
  */
 import { Button, HStack, Text, VStack } from "@chakra-ui/react";
+import { useTranslation } from "react-i18next";
 import { FiCheck, FiMinus } from "react-icons/fi";
 
 import { Popover } from "src/system-components";
 
+import { formatNumber } from "src/utils";
+
 type ChecklistProps = {
   readonly receivedKeys: ReadonlyArray<string>;
   readonly requiredKeys: ReadonlyArray<string>;
@@ -72,25 +75,29 @@ export const RollupKeyChecklistPopover = ({
   receivedKeys,
   requiredCount,
   requiredKeys,
-}: PopoverProps) => (
-  // eslint-disable-next-line jsx-a11y/no-autofocus
-  <Popover.Root autoFocus={false} lazyMount positioning={{ placement: 
"bottom-end" }} unmountOnExit>
-    <Popover.Trigger asChild>
-      <Button
-        color={receivedCount < requiredCount ? "warning.fg" : "fg.muted"}
-        loading={isLoading}
-        paddingInline={0}
-        size="sm"
-        variant="ghost"
-      >
-        {receivedCount} / {requiredCount}
-      </Button>
-    </Popover.Trigger>
-    <Popover.Content css={{ "--popover-bg": "colors.bg.emphasized" }} 
width="fit-content">
-      <Popover.Arrow />
-      <Popover.Body>
-        <RollupKeyChecklist receivedKeys={receivedKeys} 
requiredKeys={requiredKeys} />
-      </Popover.Body>
-    </Popover.Content>
-  </Popover.Root>
-);
+}: PopoverProps) => {
+  const { i18n } = useTranslation();
+
+  return (
+    // eslint-disable-next-line jsx-a11y/no-autofocus
+    <Popover.Root autoFocus={false} lazyMount positioning={{ placement: 
"bottom-end" }} unmountOnExit>
+      <Popover.Trigger asChild>
+        <Button
+          color={receivedCount < requiredCount ? "warning.fg" : "fg.muted"}
+          loading={isLoading}
+          paddingInline={0}
+          size="sm"
+          variant="ghost"
+        >
+          {formatNumber(receivedCount, i18n.language)} / 
{formatNumber(requiredCount, i18n.language)}
+        </Button>
+      </Popover.Trigger>
+      <Popover.Content css={{ "--popover-bg": "colors.bg.emphasized" }} 
width="fit-content">
+        <Popover.Arrow />
+        <Popover.Body>
+          <RollupKeyChecklist receivedKeys={receivedKeys} 
requiredKeys={requiredKeys} />
+        </Popover.Body>
+      </Popover.Content>
+    </Popover.Root>
+  );
+};
diff --git a/airflow-core/src/airflow/ui/src/components/ImportErrorCount.tsx 
b/airflow-core/src/airflow/ui/src/components/StatsCard.test.tsx
similarity index 53%
copy from airflow-core/src/airflow/ui/src/components/ImportErrorCount.tsx
copy to airflow-core/src/airflow/ui/src/components/StatsCard.test.tsx
index 8459b9cf4d2..44b62e1515f 100644
--- a/airflow-core/src/airflow/ui/src/components/ImportErrorCount.tsx
+++ b/airflow-core/src/airflow/ui/src/components/StatsCard.test.tsx
@@ -16,28 +16,28 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Badge, Text } from "@chakra-ui/react";
+import "@testing-library/jest-dom";
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
 
-type Props = {
-  readonly count: number | null;
-};
+import { Wrapper } from "src/utils/Wrapper";
 
-/**
- * An import-error count, flagged red only when there is something to flag.
- *
- * Null is not zero: it means the caller may not read import errors, so it 
renders as unknown
- * rather than as a clean bill of health.
- */
-export const ImportErrorCount = ({ count }: Props) => {
-  if (count === null) {
-    return <Text color="fg.muted">-</Text>;
-  }
+import { StatsCard } from "./StatsCard";
+
+vi.mock("react-i18next", () => ({
+  useTranslation: () => ({
+    i18n: { language: "en" },
+    // eslint-disable-next-line id-length
+    t: (key: string) => key,
+  }),
+}));
+
+describe("StatsCard", () => {
+  it("groups thousands in the count", () => {
+    render(<StatsCard colorScheme="failed" count={12_345} isRTL={false} 
label="Failed Dags" />, {
+      wrapper: Wrapper,
+    });
 
-  return count === 0 ? (
-    <Text color="fg.muted">0</Text>
-  ) : (
-    <Badge colorPalette="failed" variant="solid">
-      {count}
-    </Badge>
-  );
-};
+    expect(screen.getByTestId("stats-card")).toHaveTextContent("12,345");
+  });
+});
diff --git a/airflow-core/src/airflow/ui/src/components/StatsCard.tsx 
b/airflow-core/src/airflow/ui/src/components/StatsCard.tsx
index 76955ea3448..bf2a2ab03f8 100644
--- a/airflow-core/src/airflow/ui/src/components/StatsCard.tsx
+++ b/airflow-core/src/airflow/ui/src/components/StatsCard.tsx
@@ -19,6 +19,7 @@
 import type { ReactNode } from "react";
 
 import { Box, HStack, Skeleton, Text } from "@chakra-ui/react";
+import { useTranslation } from "react-i18next";
 import { FiChevronRight, FiChevronLeft } from "react-icons/fi";
 import { Link as RouterLink } from "react-router-dom";
 
@@ -26,6 +27,8 @@ import type { TaskInstanceState } from 
"openapi/requests/types.gen";
 
 import { StateBadge } from "src/components/StateBadge";
 
+import { formatNumber } from "src/utils";
+
 export const StatsCard = ({
   colorScheme,
   count,
@@ -47,6 +50,8 @@ export const StatsCard = ({
   readonly onClick?: () => void;
   readonly state?: TaskInstanceState | null;
 }) => {
+  const { i18n } = useTranslation();
+
   if (isLoading) {
     return <Skeleton borderRadius="lg" height="42px" width="175px" />;
   }
@@ -63,7 +68,7 @@ export const StatsCard = ({
     >
       <StateBadge colorPalette={colorScheme} mr={2} state={state}>
         {icon}
-        {count}
+        {formatNumber(count, i18n.language)}
       </StateBadge>
 
       <Text color="fg" fontSize="sm" fontWeight="bold">
diff --git 
a/airflow-core/src/airflow/ui/src/components/TaskInstanceTooltip.test.tsx 
b/airflow-core/src/airflow/ui/src/components/TaskInstanceTooltip.test.tsx
index 423af4809f8..3a5a5039d3e 100644
--- a/airflow-core/src/airflow/ui/src/components/TaskInstanceTooltip.test.tsx
+++ b/airflow-core/src/airflow/ui/src/components/TaskInstanceTooltip.test.tsx
@@ -18,7 +18,7 @@
  */
 import "@testing-library/jest-dom";
 import { render, screen } from "@testing-library/react";
-import { describe, expect, it } from "vitest";
+import { describe, expect, it, vi } from "vitest";
 
 import type { LightGridTaskInstanceSummary, TaskInstanceResponse } from 
"openapi/requests/types.gen";
 
@@ -26,6 +26,14 @@ import { Wrapper } from "src/utils/Wrapper";
 
 import TaskInstanceTooltip from "./TaskInstanceTooltip";
 
+vi.mock("react-i18next", () => ({
+  useTranslation: () => ({
+    i18n: { language: "en" },
+    // eslint-disable-next-line id-length
+    t: (key: string) => key,
+  }),
+}));
+
 describe("TaskInstanceTooltip", () => {
   it("renders children directly when both taskInstance and tooltip are 
undefined", () => {
     render(
@@ -217,6 +225,26 @@ describe("TaskInstanceTooltip", () => {
     
expect(screen.getByText(/1\s+common:states\.success/iu)).toBeInTheDocument();
   });
 
+  it("groups thousands in the child state breakdown", () => {
+    const taskInstance: LightGridTaskInstanceSummary = {
+      child_states: { success: 1234 },
+      max_end_date: null,
+      min_start_date: null,
+      state: "success",
+      task_display_name: "Mapped Task",
+      task_id: "mapped_task",
+    };
+
+    render(
+      <TaskInstanceTooltip open taskInstance={taskInstance}>
+        <span>trigger</span>
+      </TaskInstanceTooltip>,
+      { wrapper: Wrapper },
+    );
+
+    
expect(screen.getByText(/1,234\s+common:states\.success/iu)).toBeInTheDocument();
+  });
+
   it("shows run ID when provided explicitly for grid summaries", () => {
     const taskInstance: LightGridTaskInstanceSummary = {
       child_states: null,
diff --git a/airflow-core/src/airflow/ui/src/components/TaskInstanceTooltip.tsx 
b/airflow-core/src/airflow/ui/src/components/TaskInstanceTooltip.tsx
index 059f168ae39..31d8f6b23c7 100644
--- a/airflow-core/src/airflow/ui/src/components/TaskInstanceTooltip.tsx
+++ b/airflow-core/src/airflow/ui/src/components/TaskInstanceTooltip.tsx
@@ -29,7 +29,7 @@ import { Tooltip, type TooltipProps } from 
"src/system-components";
 
 import Time from "src/components/Time";
 
-import { sortStateEntries, useDurationFormat } from "src/utils";
+import { formatNumber, sortStateEntries, useDurationFormat } from "src/utils";
 
 /** Grid summary plus optional schedule/queue hints (e.g. Gantt segment 
tooltips). */
 type LightGridTaskInstanceSummaryWithWhen = {
@@ -45,7 +45,7 @@ type Props = {
 } & Omit<TooltipProps, "content">;
 
 const TaskInstanceTooltip = ({ children, positioning, runId, taskInstance, 
tooltip, ...rest }: Props) => {
-  const { t: translate } = useTranslation();
+  const { i18n, t: translate } = useTranslation();
   const { formatElapsed, renderDuration } = useDurationFormat();
 
   const hasTooltip = tooltip !== undefined && tooltip !== null;
@@ -148,7 +148,7 @@ const TaskInstanceTooltip = ({ children, positioning, 
runId, taskInstance, toolt
                         width="10px"
                       />
                       <Text fontSize="xs">
-                        {count} {translate(`common:states.${state}`)}
+                        {formatNumber(count, i18n.language)} 
{translate(`common:states.${state}`)}
                       </Text>
                     </HStack>
                   ))}
diff --git a/airflow-core/src/airflow/ui/src/components/TaskName.tsx 
b/airflow-core/src/airflow/ui/src/components/TaskName.tsx
index e2375e05244..ed83175944a 100644
--- a/airflow-core/src/airflow/ui/src/components/TaskName.tsx
+++ b/airflow-core/src/airflow/ui/src/components/TaskName.tsx
@@ -19,10 +19,13 @@
 import type { CSSProperties } from "react";
 
 import { Box, type TextProps } from "@chakra-ui/react";
+import { useTranslation } from "react-i18next";
 import { FiArrowDownRight, FiArrowUpRight } from "react-icons/fi";
 
 import type { NodeResponse } from "openapi/requests/types.gen";
 
+import { formatNumber } from "src/utils";
+
 export type TaskNameProps = {
   readonly childCount?: number;
   readonly isGroup?: boolean;
@@ -49,6 +52,8 @@ export const TaskName = ({
   setupTeardownType,
   ...rest
 }: TaskNameProps) => {
+  const { i18n } = useTranslation();
+
   if (isGroup) {
     return (
       <Box
@@ -75,7 +80,9 @@ export const TaskName = ({
       {...rest}
     >
       {label}
-      {isMapped ? ` [${childCount ?? " "}]` : undefined}
+      {isMapped
+        ? ` [${childCount === undefined ? " " : formatNumber(childCount, 
i18n.language)}]`
+        : undefined}
       {setupTeardownType === "setup" && <FiArrowUpRight size={isZoomedOut ? 24 
: 15} style={iconStyle} />}
       {setupTeardownType === "teardown" && (
         <FiArrowDownRight size={isZoomedOut ? 24 : 15} style={iconStyle} />
diff --git a/airflow-core/src/airflow/ui/src/components/TrendCountButton.tsx 
b/airflow-core/src/airflow/ui/src/components/TrendCountButton.tsx
index ab7f8de6a80..38b38196ff8 100644
--- a/airflow-core/src/airflow/ui/src/components/TrendCountButton.tsx
+++ b/airflow-core/src/airflow/ui/src/components/TrendCountButton.tsx
@@ -17,8 +17,11 @@
  * under the License.
  */
 import { HStack, Badge, Text, Skeleton } from "@chakra-ui/react";
+import { useTranslation } from "react-i18next";
 import { Link, type To } from "react-router-dom";
 
+import { formatNumber } from "src/utils";
+
 import { TrendCountChart, type ChartEvent } from "./TrendCountChart";
 
 type Props = {
@@ -41,8 +44,10 @@ export const TrendCountButton = ({
   label,
   route,
   startDate,
-}: Props) =>
-  isLoading ? (
+}: Props) => {
+  const { i18n } = useTranslation();
+
+  return isLoading ? (
     <Skeleton borderRadius={4} height="45px" width="350px" />
   ) : (
     <Link to={route}>
@@ -57,7 +62,7 @@ export const TrendCountButton = ({
         width="350px"
       >
         <Badge borderRadius="md" colorPalette={colorPalette} variant="solid">
-          {count}
+          {formatNumber(count, i18n.language)}
         </Badge>
         <Text fontSize="sm" fontWeight="bold">
           {label}
@@ -66,3 +71,4 @@ export const TrendCountButton = ({
       </HStack>
     </Link>
   );
+};
diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx
index 72b8dd476a0..1c5c3f3cc46 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Details/DetailsLayout.tsx
@@ -40,7 +40,7 @@ import { IconButton, ProgressBar, Toaster } from 
"src/system-components";
 
 import BackfillBanner from "src/components/Banner/BackfillBanner";
 import DrainingBanner from "src/components/Banner/DrainingBanner";
-import { DAGWarningsModal } from "src/components/DAGWarningsModal";
+import { countDagWarnings, DAGWarningsModal } from 
"src/components/DAGWarningsModal";
 import { TogglePause } from "src/components/TogglePause";
 import { TriggerDAGButton } from "src/components/TriggerDag/TriggerDAGButton";
 
@@ -50,7 +50,7 @@ import { SearchParamsKeys } from "src/constants/searchParams";
 import { VersionIndicatorOptions } from 
"src/constants/showVersionIndicatorOptions";
 import { GroupsProvider } from "src/context/groups";
 import { useGridRuns } from "src/queries/useGridRuns.ts";
-import { useAutoRefresh } from "src/utils";
+import { formatNumber, useAutoRefresh } from "src/utils";
 
 import { DagBreadcrumb } from "./DagBreadcrumb";
 import { Gantt } from "./Gantt/Gantt";
@@ -441,7 +441,7 @@ export const DetailsLayout = ({ children, error, isLoading, 
outletContext, tabs
                       <>
                         <IconButton
                           colorPalette={Boolean(error) ? "red" : "orange"}
-                          label={`${translate("common:dagWarnings")} 
(${warningData?.total_entries ?? 0 + Number(error)})`}
+                          label={`${translate("common:dagWarnings")} 
(${formatNumber(countDagWarnings(warningData?.total_entries, error), 
i18n.language)})`}
                           margin="2"
                           marginBottom="-1"
                           onClick={onOpen}
diff --git 
a/airflow-core/src/airflow/ui/src/pages/AssetsList/DependencyPopover.tsx 
b/airflow-core/src/airflow/ui/src/pages/AssetsList/DependencyPopover.tsx
index 473b6e8d603..4b340c28229 100644
--- a/airflow-core/src/airflow/ui/src/pages/AssetsList/DependencyPopover.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/AssetsList/DependencyPopover.tsx
@@ -27,6 +27,7 @@ import { Popover, RouterLink, Tooltip } from 
"src/system-components";
 import { TeamName } from "src/components/TeamName";
 
 import { useShowTeam } from "src/hooks/useShowTeam";
+import { formatNumber } from "src/utils";
 
 type Props = {
   readonly dependencies: Array<DagScheduleAssetReference | 
TaskOutletAssetReference>;
@@ -60,7 +61,7 @@ const IconTeamName = ({ teamName }: { readonly teamName?: 
string | null }) => {
 };
 
 export const DependencyPopover = ({ dependencies, type }: Props) => {
-  const { t: translate } = useTranslation();
+  const { i18n, t: translate } = useTranslation();
   const dependencyKey = type.toLowerCase() as "dag" | "task";
 
   return (
@@ -68,7 +69,8 @@ export const DependencyPopover = ({ dependencies, type }: 
Props) => {
     <Popover.Root autoFocus={false} lazyMount unmountOnExit>
       <Popover.Trigger asChild disabled={dependencies.length === 0}>
         <Button variant="outline">
-          {dependencies.length} {translate(dependencyKey, { count: 
dependencies.length })}
+          {formatNumber(dependencies.length, i18n.language)}{" "}
+          {translate(dependencyKey, { count: dependencies.length })}
         </Button>
       </Popover.Trigger>
       <Popover.Content css={{ "--popover-bg": "colors.bg.emphasized" }} 
width="fit-content">
diff --git a/airflow-core/src/airflow/ui/src/pages/Connections/Connections.tsx 
b/airflow-core/src/airflow/ui/src/pages/Connections/Connections.tsx
index a7e45722e11..49031e0bea9 100644
--- a/airflow-core/src/airflow/ui/src/pages/Connections/Connections.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Connections/Connections.tsx
@@ -46,7 +46,7 @@ import { SearchParamsKeys, type SearchParamsKeysType } from 
"src/constants/searc
 import { useAdvancedSearch } from "src/hooks/useAdvancedSearch";
 import { useConfig } from "src/queries/useConfig.tsx";
 import { useConnectionTypeMeta } from "src/queries/useConnectionTypeMeta";
-import { useDocumentTitle } from "src/utils";
+import { formatNumber, useDocumentTitle } from "src/utils";
 
 import AddConnectionButton from "./AddConnectionButton";
 import DeleteConnectionButton from "./DeleteConnectionButton";
@@ -139,7 +139,7 @@ const getColumns = ({
 };
 
 export const Connections = () => {
-  const { t: translate } = useTranslation(["admin", "common"]);
+  const { i18n, t: translate } = useTranslation(["admin", "common"]);
 
   useDocumentTitle(translate("common:admin.Connections"));
 
@@ -221,7 +221,7 @@ export const Connections = () => {
       <ActionBar.Root closeOnInteractOutside={false} 
open={Boolean(selectedRows.size)}>
         <ActionBar.Content>
           <ActionBar.SelectionTrigger>
-            {selectedRows.size} {translate("deleteActions.selected")}
+            {formatNumber(selectedRows.size, i18n.language)} 
{translate("deleteActions.selected")}
           </ActionBar.SelectionTrigger>
           <ActionBar.Separator />
           <Tooltip content={translate("deleteActions.tooltip")}>
diff --git 
a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarTooltip.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarTooltip.tsx
index 8ca36b11109..6075e2e9f97 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarTooltip.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarTooltip.tsx
@@ -22,6 +22,8 @@ import { FiAlertTriangle, FiClock } from "react-icons/fi";
 
 import { RunTypeIcon } from "src/components/RunTypeIcon";
 
+import { formatNumber } from "src/utils";
+
 import type { CalendarCellData, CalendarColorMode } from "./types";
 
 const SQUARE_SIZE = "12px";
@@ -41,7 +43,7 @@ const stateColorMap = {
 };
 
 export const CalendarTooltip = ({ cellData, viewMode = "total" }: Props) => {
-  const { t: translate } = useTranslation(["dag", "common"]);
+  const { i18n, t: translate } = useTranslation(["dag", "common"]);
 
   if (!cellData) {
     return undefined;
@@ -95,7 +97,7 @@ export const CalendarTooltip = ({ cellData, viewMode = 
"total" }: Props) => {
                 width={SQUARE_SIZE}
               />
               <Text fontSize="xs">
-                {count} {state}
+                {formatNumber(count, i18n.language)} {state}
               </Text>
             </HStack>
           ))}
@@ -105,7 +107,7 @@ export const CalendarTooltip = ({ cellData, viewMode = 
"total" }: Props) => {
                 <RunTypeIcon runType="backfill" />
               </Box>
               <Text fontSize="xs">
-                {counts.backfill} {translate("dag:calendar.backfill")}
+                {formatNumber(counts.backfill, i18n.language)} 
{translate("dag:calendar.backfill")}
               </Text>
             </HStack>
           )}
diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Details.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dag/Details.tsx
index 3c7ac58c0bb..b358004bbf7 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Details.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Details.tsx
@@ -30,10 +30,10 @@ import { TeamName } from "src/components/TeamName";
 import Time from "src/components/Time";
 
 import { useShowTeam } from "src/hooks/useShowTeam";
-import { useDurationFormat } from "src/utils";
+import { formatNumber, useDurationFormat } from "src/utils";
 
 export const Details = () => {
-  const { t: translate } = useTranslation(["common", "dag"]);
+  const { i18n, t: translate } = useTranslation(["common", "dag"]);
   const { renderDuration } = useDurationFormat();
   const { dagId = "" } = useParams();
 
@@ -127,15 +127,17 @@ export const Details = () => {
             </Table.Row>
             <Table.Row data-testid="max-active-runs-row">
               <Table.Cell>{translate("dagDetails.maxActiveRuns")}</Table.Cell>
-              <Table.Cell>{dag.max_active_runs}</Table.Cell>
+              <Table.Cell>
+                {dag.max_active_runs === null ? undefined : 
formatNumber(dag.max_active_runs, i18n.language)}
+              </Table.Cell>
             </Table.Row>
             <Table.Row data-testid="max-active-tasks-row">
               <Table.Cell>{translate("dagDetails.maxActiveTasks")}</Table.Cell>
-              <Table.Cell>{dag.max_active_tasks}</Table.Cell>
+              <Table.Cell>{formatNumber(dag.max_active_tasks, 
i18n.language)}</Table.Cell>
             </Table.Row>
             <Table.Row data-testid="max-consecutive-failed-dag-runs-row">
               
<Table.Cell>{translate("dagDetails.maxConsecutiveFailedDagRuns")}</Table.Cell>
-              <Table.Cell>{dag.max_consecutive_failed_dag_runs}</Table.Cell>
+              <Table.Cell>{formatNumber(dag.max_consecutive_failed_dag_runs, 
i18n.language)}</Table.Cell>
             </Table.Row>
             <Table.Row data-testid="catchup-row">
               <Table.Cell>{translate("dagDetails.catchup")}</Table.Cell>
diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Header.test.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dag/Header.test.tsx
index 194879737fe..a88cf4edff6 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Header.test.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Header.test.tsx
@@ -93,6 +93,18 @@ describe("Header", () => {
     expect(screen.getByText("2 of 2")).toBeInTheDocument();
   });
 
+  it("shows only the active runs when the Dag has no maximum", () => {
+    render(
+      <Wrapper>
+        <Header dag={{ ...mockDag, active_runs_count: 2, max_active_runs: null 
}} />
+      </Wrapper>,
+    );
+
+    
expect(screen.getByText(i18n.t("common:dagDetails.activeRuns"))).toBeInTheDocument();
+    expect(screen.getByText("2")).toBeInTheDocument();
+    expect(screen.queryByText(/null/u)).not.toBeInTheDocument();
+  });
+
   it("renders the draining badge instead of the next run timestamp for a 
draining Dag", () => {
     render(
       <Wrapper>
diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx
index 7687488fcfc..fb4e37751f7 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Header.tsx
@@ -37,6 +37,7 @@ import { TeamName } from "src/components/TeamName";
 
 import { DagIcon } from "src/assets/DagIcon";
 import { useShowTeam } from "src/hooks/useShowTeam";
+import { formatNumber } from "src/utils";
 
 import { DagOwners } from "../DagsList/DagOwners";
 import { DagTags } from "../DagsList/DagTags";
@@ -60,7 +61,7 @@ export const Header = ({
   readonly dag?: DAGDetailsResponse;
   readonly latestRunInfo?: LatestRunInfo;
 }) => {
-  const { t: translate } = useTranslation(["common", "dag"]);
+  const { i18n, t: translate } = useTranslation(["common", "dag"]);
   // We would still like to show the dagId even if the dag object hasn't 
loaded yet
   const { dagId } = useParams();
   const showTeam = useShowTeam(dag?.team_name);
@@ -117,7 +118,9 @@ export const Header = ({
       value:
         dag?.max_active_runs === undefined
           ? undefined
-          : `${dag.active_runs_count ?? 0} of ${dag.max_active_runs}`,
+          : dag.max_active_runs === null
+            ? formatNumber(dag.active_runs_count ?? 0, i18n.language)
+            : `${formatNumber(dag.active_runs_count ?? 0, i18n.language)} of 
${formatNumber(dag.max_active_runs, i18n.language)}`,
     },
     {
       label: translate("dagDetails.owner"),
diff --git a/airflow-core/src/airflow/ui/src/pages/DagBundle/BundleFiles.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagBundle/BundleFiles.tsx
index f8308d0571f..b0ed83451ae 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagBundle/BundleFiles.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagBundle/BundleFiles.tsx
@@ -35,6 +35,7 @@ import { ImportErrorCount } from 
"src/components/ImportErrorCount";
 import Time from "src/components/Time";
 
 import { useDagBundleRefetchInterval } from 
"src/queries/useDagBundleRefetchInterval";
+import { formatNumber } from "src/utils";
 
 import { FileImportError } from "./FileImportError";
 
@@ -44,6 +45,7 @@ type FileRow = { row: { original: DagBundleFileResponse } };
 // parameter and the table offers none.
 const createColumns = (
   translate: TFunction,
+  locale: string,
   onShowImportError: (relativeFileloc: string) => void,
 ): Array<ColumnDef<DagBundleFileResponse>> => [
   {
@@ -54,6 +56,7 @@ const createColumns = (
   },
   {
     accessorKey: "dag_count",
+    cell: ({ row: { original } }: FileRow) => formatNumber(original.dag_count, 
locale),
     enableSorting: false,
     header: translate("common:dag_other"),
   },
@@ -102,7 +105,7 @@ const createColumns = (
 ];
 
 export const BundleFiles = ({ bundleName }: { readonly bundleName: string }) 
=> {
-  const { t: translate } = useTranslation(["browse", "common"]);
+  const { i18n, t: translate } = useTranslation(["browse", "common"]);
   const refetchInterval = useDagBundleRefetchInterval();
 
   const { setTableURLState, tableURLState } = useTableURLState();
@@ -129,7 +132,7 @@ export const BundleFiles = ({ bundleName }: { readonly 
bundleName: string }) =>
         relativeFileloc={errorFileloc}
       />
       <DataTable
-        columns={createColumns(translate, setErrorFileloc)}
+        columns={createColumns(translate, i18n.language, setErrorFileloc)}
         data={data?.dag_bundle_files ?? []}
         errorMessage={<ErrorAlert error={error} />}
         initialState={tableURLState}
diff --git a/airflow-core/src/airflow/ui/src/pages/DagBundle/Header.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagBundle/Header.tsx
index f16c588e841..a20251a9d8a 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagBundle/Header.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagBundle/Header.tsx
@@ -29,9 +29,10 @@ import { TeamName } from "src/components/TeamName";
 import Time from "src/components/Time";
 
 import { useShowTeam } from "src/hooks/useShowTeam";
+import { formatNumber } from "src/utils";
 
 export const Header = ({ bundle }: { readonly bundle: DagBundleDetailResponse 
}) => {
-  const { t: translate } = useTranslation(["browse", "common"]);
+  const { i18n, t: translate } = useTranslation(["browse", "common"]);
   // Teams exist only in multi-team deployments, and a bundle need not belong 
to one; an
   // empty "Team" stat reads as a broken page.
   const showTeam = useShowTeam(bundle.team_name);
@@ -65,7 +66,7 @@ export const Header = ({ bundle }: { readonly bundle: 
DagBundleDetailResponse })
           <Time datetime={bundle.last_refreshed} />
         ),
     },
-    { label: translate("common:dag_other"), value: bundle.dag_count },
+    { label: translate("common:dag_other"), value: 
formatNumber(bundle.dag_count, i18n.language) },
     {
       label: translate("browse:dagBundles.columns.importErrors"),
       value: <ImportErrorCount count={bundle.import_error_count} />,
diff --git 
a/airflow-core/src/airflow/ui/src/pages/DagRuns/BulkDeleteDagRunsButton.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagRuns/BulkDeleteDagRunsButton.tsx
index eb5625e8345..bda3a8d0e35 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagRuns/BulkDeleteDagRunsButton.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagRuns/BulkDeleteDagRunsButton.tsx
@@ -32,6 +32,7 @@ import { StateBadge } from "src/components/StateBadge";
 import Time from "src/components/Time";
 
 import { useBulkDeleteDagRuns } from "src/queries/useBulkDeleteDagRuns";
+import { formatNumber } from "src/utils";
 
 type Props = {
   readonly deselectKeys: (keys: Array<string>) => void;
@@ -62,7 +63,7 @@ const getColumns = (translate: TFunction): 
Array<ColumnDef<DAGRunResponse>> => [
 ];
 
 const BulkDeleteDagRunsButton = ({ deselectKeys, selectedDagRuns }: Props) => {
-  const { t: translate } = useTranslation(["common", "dags"]);
+  const { i18n, t: translate } = useTranslation(["common", "dags"]);
   const { onClose, onOpen, open } = useDisclosure();
   const { bulkAction, data, error, isPending } = useBulkDeleteDagRuns({
     deselectKeys,
@@ -135,7 +136,7 @@ const BulkDeleteDagRunsButton = ({ deselectKeys, 
selectedDagRuns }: Props) => {
                     <Text fontSize="sm" fontWeight="semibold">
                       {translate("dagId")}: {dagId}{" "}
                       <Text as="span" color="fg.subtle" fontWeight="normal">
-                        ({dagRuns.length})
+                        ({formatNumber(dagRuns.length, i18n.language)})
                       </Text>
                     </Text>
                   </Accordion.ItemTrigger>
diff --git 
a/airflow-core/src/airflow/ui/src/pages/DagRuns/BulkMarkDagRunsAsButton.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagRuns/BulkMarkDagRunsAsButton.tsx
index 1022efea119..602fcad29b1 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagRuns/BulkMarkDagRunsAsButton.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagRuns/BulkMarkDagRunsAsButton.tsx
@@ -33,6 +33,7 @@ import { allowedStates } from "src/components/MarkAs/utils";
 import { StateBadge } from "src/components/StateBadge";
 
 import { useBulkPatchDagRun } from "src/queries/useBulkPatchDagRun";
+import { formatNumber } from "src/utils";
 
 type Props = {
   readonly deselectKeys: (keys: Array<string>) => void;
@@ -40,7 +41,7 @@ type Props = {
 };
 
 const BulkMarkDagRunsAsButton = ({ deselectKeys, selectedDagRuns }: Props) => {
-  const { t: translate } = useTranslation(["common", "dags"]);
+  const { i18n, t: translate } = useTranslation(["common", "dags"]);
   const { onClose, onOpen, open } = useDisclosure();
   const [state, setState] = useState<DagRunMutableStates>("success");
   const [note, setNote] = useState<string | null>(null);
@@ -75,7 +76,7 @@ const BulkMarkDagRunsAsButton = ({ deselectKeys, 
selectedDagRuns }: Props) => {
               <HStack justify="space-between" width="full">
                 <StateBadge 
state={menuState}>{translate(`common:states.${menuState}`)}</StateBadge>
                 <Badge colorPalette="gray" variant="subtle">
-                  {selectedDagRuns.length}
+                  {formatNumber(selectedDagRuns.length, i18n.language)}
                 </Badge>
               </HStack>
             </Menu.Item>
diff --git a/airflow-core/src/airflow/ui/src/pages/DagRuns/DagRuns.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagRuns/DagRuns.tsx
index 456cbf2af8d..c84542dbbef 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagRuns/DagRuns.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagRuns/DagRuns.tsx
@@ -53,7 +53,7 @@ import { TruncatedText } from "src/components/TruncatedText";
 import { SearchParamsKeys, type SearchParamsKeysType } from 
"src/constants/searchParams";
 import { useAdvancedSearchArg } from "src/hooks/useAdvancedSearch";
 import { useConfig } from "src/queries/useConfig";
-import { useAutoRefresh, hasDagRunConfig, isStatePending, useDocumentTitle } 
from "src/utils";
+import { formatNumber, useAutoRefresh, hasDagRunConfig, isStatePending, 
useDocumentTitle } from "src/utils";
 
 import BulkClearDagRunsButton from "./BulkClearDagRunsButton";
 import BulkDeleteDagRunsButton from "./BulkDeleteDagRunsButton";
@@ -239,7 +239,7 @@ const runColumns = ({ dagId, multiTeam, open, translate }: 
ColumnProps): Array<C
 ];
 
 export const DagRuns = () => {
-  const { t: translate } = useTranslation();
+  const { i18n, t: translate } = useTranslation();
   const { dagId } = useParams();
 
   // Only the standalone list page owns the tab title; the Dag-scoped tab 
inherits the Dag page's title.
@@ -405,7 +405,7 @@ export const DagRuns = () => {
       <ActionBar.Root closeOnInteractOutside={false} 
open={Boolean(selectedRows.size)}>
         <ActionBar.Content>
           <ActionBar.SelectionTrigger>
-            {selectedRows.size} {translate("selected")}
+            {formatNumber(selectedRows.size, i18n.language)} 
{translate("selected")}
           </ActionBar.SelectionTrigger>
           <ActionBar.Separator />
           <BulkClearDagRunsButton deselectKeys={deselectKeys} 
selectedDagRuns={selectedDagRuns} />
diff --git 
a/airflow-core/src/airflow/ui/src/pages/DagsList/BulkPauseDrainDagsButton.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/BulkPauseDrainDagsButton.tsx
index cabb36c2777..03cfe5c0941 100644
--- 
a/airflow-core/src/airflow/ui/src/pages/DagsList/BulkPauseDrainDagsButton.tsx
+++ 
b/airflow-core/src/airflow/ui/src/pages/DagsList/BulkPauseDrainDagsButton.tsx
@@ -26,6 +26,7 @@ import { ConfirmationModal } from 
"src/components/ConfirmationModal";
 import { PauseOrDrainChoiceModal } from 
"src/components/PauseOrDrainChoiceModal";
 
 import { useBulkSetDagSchedulingState } from 
"src/queries/useBulkSetDagSchedulingState";
+import { formatNumber } from "src/utils";
 
 type Props = {
   readonly deselectKeys: (keys: Array<string>) => void;
@@ -33,7 +34,7 @@ type Props = {
 };
 
 const BulkPauseDrainDagsButton = ({ deselectKeys, selectedDags }: Props) => {
-  const { t: translate } = useTranslation(["common", "dags"]);
+  const { i18n, t: translate } = useTranslation(["common", "dags"]);
   const { onClose, onOpen, open } = useDisclosure();
   const { bulkAction, data, error, isPending, reset } = 
useBulkSetDagSchedulingState({
     deselectKeys,
@@ -44,7 +45,7 @@ const BulkPauseDrainDagsButton = ({ deselectKeys, 
selectedDags }: Props) => {
   // for the whole batch — skip the drain-vs-pause choice, same as the 
single-Dag toggle does.
   // Already-paused Dags are never drained (see runBulkAction), so their runs 
don't count.
   const allIdle = selectedDags.every((dag) => dag.is_paused || 
!dag.has_unfinished_runs);
-  const displayName = `${selectedDags.length} ${translate("dag", { count: 
selectedDags.length })}`;
+  const displayName = `${formatNumber(selectedDags.length, i18n.language)} 
${translate("dag", { count: selectedDags.length })}`;
 
   const runBulkAction = (schedulingState: DagSchedulingState) => {
     bulkAction({
diff --git 
a/airflow-core/src/airflow/ui/src/pages/DagsList/BulkUnpauseDagsButton.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/BulkUnpauseDagsButton.tsx
index 1b673ac6faf..6768e823300 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/BulkUnpauseDagsButton.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/BulkUnpauseDagsButton.tsx
@@ -25,6 +25,7 @@ import { ActionErrors } from "src/components/ActionErrors";
 import { ConfirmationModal } from "src/components/ConfirmationModal";
 
 import { useBulkSetDagSchedulingState } from 
"src/queries/useBulkSetDagSchedulingState";
+import { formatNumber } from "src/utils";
 
 type Props = {
   readonly deselectKeys: (keys: Array<string>) => void;
@@ -32,14 +33,14 @@ type Props = {
 };
 
 const BulkUnpauseDagsButton = ({ deselectKeys, selectedDags }: Props) => {
-  const { t: translate } = useTranslation(["common", "dags"]);
+  const { i18n, t: translate } = useTranslation(["common", "dags"]);
   const { onClose, onOpen, open } = useDisclosure();
   const { bulkAction, data, error, isPending, reset } = 
useBulkSetDagSchedulingState({
     deselectKeys,
     onSuccessConfirm: onClose,
   });
 
-  const displayName = `${selectedDags.length} ${translate("dag", { count: 
selectedDags.length })}`;
+  const displayName = `${formatNumber(selectedDags.length, i18n.language)} 
${translate("dag", { count: selectedDags.length })}`;
 
   const handleOpen = () => {
     reset();
diff --git 
a/airflow-core/src/airflow/ui/src/pages/DagsList/DagRunStateCounts.test.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagRunStateCounts.test.tsx
index 906b0b80937..7dd58a69bf0 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagRunStateCounts.test.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagRunStateCounts.test.tsx
@@ -19,13 +19,20 @@
 import "@testing-library/jest-dom/vitest";
 import { render, screen } from "@testing-library/react";
 import { MemoryRouter } from "react-router-dom";
-import { describe, expect, it } from "vitest";
+import { describe, expect, it, vi } from "vitest";
 
 import { BaseWrapper } from "src/utils/Wrapper";
 
-import "../../i18n/config";
 import { DagRunStateCounts } from "./DagRunStateCounts";
 
+vi.mock("react-i18next", () => ({
+  useTranslation: () => ({
+    i18n: { language: "en" },
+    // eslint-disable-next-line id-length
+    t: (key: string) => key,
+  }),
+}));
+
 const renderCounts = (
   counts: Record<string, number> | undefined,
   options: { isLoading?: boolean; stateCountLimit?: number } = {},
@@ -93,7 +100,7 @@ describe("DagRunStateCounts", () => {
 
   it("suffixes counts that reached the cap with '+'", () => {
     renderCounts({ failed: 5, queued: 0, running: 1, success: 1000 }, { 
stateCountLimit: 1000 });
-    
expect(screen.getByTestId("run-state-count-success-my_dag")).toHaveTextContent("1000+");
+    
expect(screen.getByTestId("run-state-count-success-my_dag")).toHaveTextContent("1,000+");
     
expect(screen.getByTestId("run-state-count-failed-my_dag")).toHaveTextContent("5");
     
expect(screen.getByTestId("run-state-count-running-my_dag")).toHaveTextContent("1");
   });
diff --git 
a/airflow-core/src/airflow/ui/src/pages/DagsList/DagRunStateCounts.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagRunStateCounts.tsx
index 4661c9d68d2..30da8d7ff21 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagRunStateCounts.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagRunStateCounts.tsx
@@ -26,6 +26,7 @@ import { RouterLink } from "src/system-components";
 import { StateBadge } from "src/components/StateBadge";
 
 import { SearchParamsKeys } from "src/constants/searchParams";
+import { formatNumber } from "src/utils";
 
 const DISPLAYED_STATES: ReadonlyArray<DagRunState> = ["success", "failed", 
"running", "queued"];
 
@@ -38,7 +39,7 @@ type Props = {
 };
 
 export const DagRunStateCounts = ({ compact = false, counts, dagId, isLoading, 
stateCountLimit }: Props) => {
-  const { t: translate } = useTranslation(["dags", "common"]);
+  const { i18n, t: translate } = useTranslation(["dags", "common"]);
   const gap = compact ? 0.5 : 1;
   const fontSize = compact ? "xs" : "sm";
 
@@ -62,7 +63,7 @@ export const DagRunStateCounts = ({ compact = false, counts, 
dagId, isLoading, s
     const count = counts[state] ?? 0;
     // A count that reached the API cap is only a lower bound; suffix it with 
"+".
     const isCapped = stateCountLimit !== undefined && count >= stateCountLimit;
-    const formattedCount = `${count}${isCapped ? "+" : ""}`;
+    const formattedCount = `${formatNumber(count, i18n.language)}${isCapped ? 
"+" : ""}`;
 
     return {
       formattedCount,
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
index f3223a4e0d2..b1da1380424 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
@@ -60,7 +60,7 @@ import { useConfig } from "src/queries/useConfig";
 import { useDagFolders } from "src/queries/useDagFolders";
 import { useDagRunStateCounts } from "src/queries/useDagRunStateCounts";
 import { useDags } from "src/queries/useDags";
-import { useDocumentTitle } from "src/utils";
+import { formatNumber, useDocumentTitle } from "src/utils";
 
 import { DagImportErrors } from "../Dashboard/Stats/DagImportErrors";
 import BulkPauseDrainDagsButton from "./BulkPauseDrainDagsButton";
@@ -279,7 +279,7 @@ const createCardDef = (runStateContext: 
RunStateCountsContext): CardDef<DAGWithL
 });
 
 export const DagsList = () => {
-  const { t: translate } = useTranslation();
+  const { i18n, t: translate } = useTranslation();
 
   useDocumentTitle(translate("common:nav.dags"));
 
@@ -530,7 +530,7 @@ export const DagsList = () => {
             >
               <ActionBar.Content>
                 <ActionBar.SelectionTrigger>
-                  {selectedRows.size} {translate("selected")}
+                  {formatNumber(selectedRows.size, i18n.language)} 
{translate("selected")}
                 </ActionBar.SelectionTrigger>
                 <ActionBar.Separator />
                 <BulkPauseDrainDagsButton deselectKeys={deselectKeys} 
selectedDags={selectedDags} />
diff --git 
a/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/MetricSection.tsx
 
b/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/MetricSection.tsx
index a4fb2a04811..89b617f8855 100644
--- 
a/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/MetricSection.tsx
+++ 
b/airflow-core/src/airflow/ui/src/pages/Dashboard/HistoricalMetrics/MetricSection.tsx
@@ -25,6 +25,7 @@ import type { TaskInstanceStateCount } from 
"openapi/requests/types.gen";
 import { StateBadge } from "src/components/StateBadge";
 
 import { SearchParamsKeys } from "src/constants/searchParams";
+import { formatNumber } from "src/utils";
 
 const BAR_WIDTH = 100;
 const BAR_HEIGHT = 5;
@@ -73,7 +74,7 @@ export const MetricSection = ({
           <RouterLink to={`/${kind}?${searchParams.toString()}`}>
             <StateBadge fontSize="md" state={state === "no_status" ? null : 
state}>
               {}
-              {`${runs.toLocaleString(i18n.language)}${capped ? "+" : ""}`}
+              {`${formatNumber(runs, i18n.language)}${capped ? "+" : ""}`}
             </StateBadge>
           </RouterLink>
           <Text>{translate(`states.${state}`)}</Text>
diff --git 
a/airflow-core/src/airflow/ui/src/pages/Dashboard/PoolSummary/PoolSummary.test.tsx
 
b/airflow-core/src/airflow/ui/src/pages/Dashboard/PoolSummary/PoolSummary.test.tsx
index 9a3b472dab3..af777e4d766 100644
--- 
a/airflow-core/src/airflow/ui/src/pages/Dashboard/PoolSummary/PoolSummary.test.tsx
+++ 
b/airflow-core/src/airflow/ui/src/pages/Dashboard/PoolSummary/PoolSummary.test.tsx
@@ -43,6 +43,7 @@ vi.mock("openapi/queries/queries", () => ({
 }));
 
 vi.mock("src/utils", () => ({
+  formatNumber: String,
   useAutoRefresh: () => false,
 }));
 
diff --git 
a/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/DagImportErrors.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/DagImportErrors.tsx
index 096b347269f..b650e07afe5 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/DagImportErrors.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/DagImportErrors.tsx
@@ -26,6 +26,8 @@ import { ErrorAlert } from "src/components/ErrorAlert";
 import { StateBadge } from "src/components/StateBadge";
 import { StatsCard } from "src/components/StatsCard";
 
+import { formatNumber } from "src/utils";
+
 import { DagImportErrorsModal } from "./DagImportErrorsModal";
 
 export const DagImportErrors = ({ iconOnly = false }: { readonly iconOnly?: 
boolean }) => {
@@ -58,7 +60,7 @@ export const DagImportErrors = ({ iconOnly = false }: { 
readonly iconOnly?: bool
           title={translate("importErrors.dagImportError", { count: 
importErrorsCount })}
         >
           <LuFileWarning size={8} />
-          {importErrorsCount}
+          {formatNumber(importErrorsCount, i18n.language)}
         </StateBadge>
       ) : (
         <StatsCard
diff --git 
a/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/PluginImportErrors.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/PluginImportErrors.tsx
index a98858ef1e7..a72cf86766c 100644
--- 
a/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/PluginImportErrors.tsx
+++ 
b/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/PluginImportErrors.tsx
@@ -26,6 +26,8 @@ import { ErrorAlert, type ExpandedApiError } from 
"src/components/ErrorAlert";
 import { StateBadge } from "src/components/StateBadge";
 import { StatsCard } from "src/components/StatsCard";
 
+import { formatNumber } from "src/utils";
+
 import { PluginImportErrorsModal } from "./PluginImportErrorsModal";
 
 export const PluginImportErrors = ({ iconOnly = false }: { readonly iconOnly?: 
boolean }) => {
@@ -62,7 +64,7 @@ export const PluginImportErrors = ({ iconOnly = false }: { 
readonly iconOnly?: b
           title={translate("plugins.importError", { count: importErrorsCount 
})}
         >
           <LuPlug size={8} />
-          {importErrorsCount}
+          {formatNumber(importErrorsCount, i18n.language)}
         </StateBadge>
       ) : (
         <StatsCard
diff --git 
a/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/Stats.test.tsx 
b/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/Stats.test.tsx
index 930974bd617..dc2c69d84a2 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/Stats.test.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dashboard/Stats/Stats.test.tsx
@@ -48,7 +48,10 @@ vi.mock("react-i18next", () => ({
 }));
 
 vi.mock("src/components/NeedsReviewButton", () => ({ 
NeedsReviewButtonWithModal: () => null }));
-vi.mock("src/utils", () => ({ useAutoRefresh: () => false }));
+vi.mock("src/utils", () => ({
+  formatNumber: String,
+  useAutoRefresh: () => false,
+}));
 vi.mock("./DagImportErrors", () => ({ DagImportErrors: () => null }));
 vi.mock("./PluginImportErrors", () => ({ PluginImportErrors: () => null }));
 
diff --git a/airflow-core/src/airflow/ui/src/pages/GroupTaskInstance/Header.tsx 
b/airflow-core/src/airflow/ui/src/pages/GroupTaskInstance/Header.tsx
index 8f2c7d8fac2..264d1ccc6c1 100644
--- a/airflow-core/src/airflow/ui/src/pages/GroupTaskInstance/Header.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/GroupTaskInstance/Header.tsx
@@ -28,17 +28,17 @@ import { HeaderCard } from "src/components/HeaderCard";
 import { MarkTaskGroupAsButton } from "src/components/MarkAs";
 import Time from "src/components/Time";
 
-import { useDurationFormat } from "src/utils";
+import { formatNumber, useDurationFormat } from "src/utils";
 
 export const Header = ({ taskInstance }: { readonly taskInstance: 
LightGridTaskInstanceSummary }) => {
-  const { t: translate } = useTranslation();
+  const { i18n, t: translate } = useTranslation();
   const { formatElapsed } = useDurationFormat();
   const entries: Array<{ label: string; value: number | ReactNode | string }> 
= [];
 
   Object.entries(taskInstance.child_states ?? {}).forEach(([state, count]) => {
     entries.push({
       label: translate("total", { state: 
translate(`states.${state.toLowerCase()}`) }),
-      value: count,
+      value: formatNumber(count, i18n.language),
     });
   });
   const stats = [
diff --git 
a/airflow-core/src/airflow/ui/src/pages/MappedTaskInstance/Details.tsx 
b/airflow-core/src/airflow/ui/src/pages/MappedTaskInstance/Details.tsx
index 783179291f5..f4b70941465 100644
--- a/airflow-core/src/airflow/ui/src/pages/MappedTaskInstance/Details.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/MappedTaskInstance/Details.tsx
@@ -26,11 +26,11 @@ import type { LightGridTaskInstanceSummary } from 
"openapi/requests/types.gen";
 import { StateBadge } from "src/components/StateBadge";
 import Time from "src/components/Time";
 
-import { useDurationFormat } from "src/utils";
+import { formatNumber, useDurationFormat } from "src/utils";
 
 export const Details = () => {
   const { dagId = "", taskId = "" } = useParams();
-  const { t: translate } = useTranslation();
+  const { i18n, t: translate } = useTranslation();
   const { formatElapsed } = useDurationFormat();
 
   // The aggregate summary (per-state counts, dates) is streamed once by the 
parent page and
@@ -67,7 +67,7 @@ export const Details = () => {
                     height="10px"
                     width="10px"
                   />
-                  {count}
+                  {formatNumber(count, i18n.language)}
                 </Flex>
               </Table.Cell>
             </Table.Row>
diff --git 
a/airflow-core/src/airflow/ui/src/pages/MappedTaskInstance/Header.tsx 
b/airflow-core/src/airflow/ui/src/pages/MappedTaskInstance/Header.tsx
index 7de01299c8a..f07d9685ebb 100644
--- a/airflow-core/src/airflow/ui/src/pages/MappedTaskInstance/Header.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/MappedTaskInstance/Header.tsx
@@ -29,11 +29,11 @@ import { ClearTaskInstanceButton } from 
"src/components/Clear";
 import { HeaderCard } from "src/components/HeaderCard";
 import Time from "src/components/Time";
 
-import { useDurationFormat } from "src/utils";
+import { formatNumber, useDurationFormat } from "src/utils";
 
 export const Header = ({ taskInstance }: { readonly taskInstance: 
LightGridTaskInstanceSummary }) => {
   const { dagId = "", runId = "" } = useParams();
-  const { t: translate } = useTranslation();
+  const { i18n, t: translate } = useTranslation();
   const { formatElapsed } = useDurationFormat();
   const entries: Array<{ key?: string; label: string; value: number | 
ReactNode | string }> = [];
   let taskCount: number = 0;
@@ -52,7 +52,7 @@ export const Header = ({ taskInstance }: { readonly 
taskInstance: LightGridTaskI
             height="10px"
             width="10px"
           />
-          {count}
+          {formatNumber(count, i18n.language)}
         </HStack>
       ),
     });
@@ -89,7 +89,7 @@ export const Header = ({ taskInstance }: { readonly 
taskInstance: LightGridTaskI
       state={taskInstance.state}
       stats={stats}
       subTitle={<Time datetime={taskInstance.min_start_date} />}
-      title={`${taskInstance.task_display_name} [${taskCount}]`}
+      title={`${taskInstance.task_display_name} [${formatNumber(taskCount, 
i18n.language)}]`}
       type="taskInstance"
     />
   );
diff --git 
a/airflow-core/src/airflow/ui/src/pages/MappedTaskInstance/MappedTaskInstance.tsx
 
b/airflow-core/src/airflow/ui/src/pages/MappedTaskInstance/MappedTaskInstance.tsx
index fd7e6f4ee40..7f8da88e6b7 100644
--- 
a/airflow-core/src/airflow/ui/src/pages/MappedTaskInstance/MappedTaskInstance.tsx
+++ 
b/airflow-core/src/airflow/ui/src/pages/MappedTaskInstance/MappedTaskInstance.tsx
@@ -26,13 +26,13 @@ import { useDagRunServiceGetDagRun } from "openapi/queries";
 import { DetailsLayout } from "src/layouts/Details/DetailsLayout";
 
 import { useGridTiSummariesStream } from "src/queries/useGridTISummaries.ts";
-import { useDocumentTitle } from "src/utils";
+import { formatNumber, useDocumentTitle } from "src/utils";
 
 import { Header } from "./Header";
 
 export const MappedTaskInstance = () => {
   const { dagId = "", runId = "", taskId = "" } = useParams();
-  const { t: translate } = useTranslation("dag");
+  const { i18n, t: translate } = useTranslation("dag");
 
   useDocumentTitle(taskId);
 
@@ -56,7 +56,11 @@ export const MappedTaskInstance = () => {
   });
 
   const tabs = [
-    { icon: <MdOutlineTask />, label: `${translate("tabs.taskInstances")} 
[${taskCount}]`, value: "" },
+    {
+      icon: <MdOutlineTask />,
+      label: `${translate("tabs.taskInstances")} [${formatNumber(taskCount, 
i18n.language)}]`,
+      value: "",
+    },
     { icon: <MdDetails />, label: translate("tabs.details"), value: "details" 
},
   ];
 
diff --git a/airflow-core/src/airflow/ui/src/pages/Pools/PoolBarCard.tsx 
b/airflow-core/src/airflow/ui/src/pages/Pools/PoolBarCard.tsx
index bcd4586ab1a..07a5116b939 100644
--- a/airflow-core/src/airflow/ui/src/pages/Pools/PoolBarCard.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Pools/PoolBarCard.tsx
@@ -28,6 +28,7 @@ import { StateIcon } from "src/components/StateIcon";
 import { TeamName } from "src/components/TeamName";
 
 import { useShowTeam } from "src/hooks/useShowTeam";
+import { formatNumber } from "src/utils";
 
 import DeletePoolButton from "./DeletePoolButton";
 import EditPoolButton from "./EditPoolButton";
@@ -37,7 +38,7 @@ type PoolBarCardProps = {
 };
 
 const PoolBarCard = ({ pool }: PoolBarCardProps) => {
-  const { t: translate } = useTranslation("admin");
+  const { i18n, t: translate } = useTranslation("admin");
   const showTeam = useShowTeam(pool.team_name);
 
   return (
@@ -46,8 +47,8 @@ const PoolBarCard = ({ pool }: PoolBarCardProps) => {
         <VStack align="start" flex="1">
           <HStack justifyContent="space-between" width="100%">
             <Text fontSize="lg" fontWeight="bold" whiteSpace="normal" 
wordBreak="break-word">
-              {pool.name} ({pool.slots === UNLIMITED_SLOTS ? "∞" : pool.slots} 
{translate("pools.form.slots")}
-              )
+              {pool.name} ({pool.slots === UNLIMITED_SLOTS ? "∞" : 
formatNumber(pool.slots, i18n.language)}{" "}
+              {translate("pools.form.slots")})
               {showTeam ? (
                 <>
                   {" ("}
diff --git 
a/airflow-core/src/airflow/ui/src/pages/TaskInstances/BulkDeleteTaskInstancesButton.tsx
 
b/airflow-core/src/airflow/ui/src/pages/TaskInstances/BulkDeleteTaskInstancesButton.tsx
index 9b07723ddeb..229ebd4651c 100644
--- 
a/airflow-core/src/airflow/ui/src/pages/TaskInstances/BulkDeleteTaskInstancesButton.tsx
+++ 
b/airflow-core/src/airflow/ui/src/pages/TaskInstances/BulkDeleteTaskInstancesButton.tsx
@@ -29,6 +29,7 @@ import { ActionErrors } from "src/components/ActionErrors";
 import { DataTable } from "src/components/DataTable";
 
 import { useBulkTaskInstances } from "src/queries/useBulkTaskInstances";
+import { formatNumber } from "src/utils";
 
 type Props = {
   readonly deselectKeys: (keys: Array<string>) => void;
@@ -36,7 +37,7 @@ type Props = {
 };
 
 const BulkDeleteTaskInstancesButton = ({ deselectKeys, selectedTaskInstances 
}: Props) => {
-  const { t: translate } = useTranslation();
+  const { i18n, t: translate } = useTranslation();
   const { onClose, onOpen, open } = useDisclosure();
   const { bulkAction, data, error, isPending } = useBulkTaskInstances({
     deselectKeys,
@@ -111,7 +112,7 @@ const BulkDeleteTaskInstancesButton = ({ deselectKeys, 
selectedTaskInstances }:
                     <Text fontSize="sm" fontWeight="semibold">
                       {translate("runId")}: {runId}{" "}
                       <Text as="span" color="fg.subtle" fontWeight="normal">
-                        ({tis.length})
+                        ({formatNumber(tis.length, i18n.language)})
                       </Text>
                     </Text>
                   </Accordion.ItemTrigger>
diff --git 
a/airflow-core/src/airflow/ui/src/pages/TaskInstances/BulkMarkTaskInstancesAsButton.tsx
 
b/airflow-core/src/airflow/ui/src/pages/TaskInstances/BulkMarkTaskInstancesAsButton.tsx
index d3ad9ff7e7b..8a16e7d269a 100644
--- 
a/airflow-core/src/airflow/ui/src/pages/TaskInstances/BulkMarkTaskInstancesAsButton.tsx
+++ 
b/airflow-core/src/airflow/ui/src/pages/TaskInstances/BulkMarkTaskInstancesAsButton.tsx
@@ -34,6 +34,7 @@ import { StateBadge } from "src/components/StateBadge";
 
 import { useBulkMarkAsDryRun } from "src/queries/useBulkMarkAsDryRun";
 import { useBulkTaskInstances } from "src/queries/useBulkTaskInstances";
+import { formatNumber } from "src/utils";
 
 type Props = {
   readonly deselectKeys: (keys: Array<string>) => void;
@@ -41,7 +42,7 @@ type Props = {
 };
 
 const BulkMarkTaskInstancesAsButton = ({ deselectKeys, selectedTaskInstances 
}: Props) => {
-  const { t: translate } = useTranslation();
+  const { i18n, t: translate } = useTranslation();
   const { onClose, onOpen, open } = useDisclosure();
   const [state, setState] = useState<TaskInstanceState>("success");
   const [selectedOptions, setSelectedOptions] = useState<Array<string>>([]);
@@ -113,7 +114,7 @@ const BulkMarkTaskInstancesAsButton = ({ deselectKeys, 
selectedTaskInstances }:
                 <HStack justify="space-between" width="full">
                   <StateBadge 
state={menuState}>{translate(`common:states.${menuState}`)}</StateBadge>
                   <Badge colorPalette="gray" variant="subtle">
-                    {count}
+                    {formatNumber(count, i18n.language)}
                   </Badge>
                 </HStack>
               </Menu.Item>
diff --git 
a/airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstances.tsx 
b/airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstances.tsx
index 626d7a7611a..2e978b13fbb 100644
--- a/airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstances.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstances.tsx
@@ -49,7 +49,7 @@ import { TruncatedText } from "src/components/TruncatedText";
 import { SearchParamsKeys, type SearchParamsKeysType } from 
"src/constants/searchParams";
 import { useAdvancedSearchArg } from "src/hooks/useAdvancedSearch";
 import { useConfig } from "src/queries/useConfig";
-import { useAutoRefresh, isStatePending, useDocumentTitle } from "src/utils";
+import { formatNumber, useAutoRefresh, isStatePending, useDocumentTitle } from 
"src/utils";
 import { getTaskInstanceLink } from "src/utils/links";
 
 import BulkClearTaskInstancesButton from "./BulkClearTaskInstancesButton";
@@ -268,7 +268,7 @@ const taskInstanceColumns = ({
 ];
 
 export const TaskInstances = () => {
-  const { t: translate } = useTranslation();
+  const { i18n, t: translate } = useTranslation();
   const { dagId, groupId, runId, taskId } = useParams();
 
   // Only the standalone list page owns the tab title; nested tabs inherit 
their parent page's title.
@@ -436,7 +436,7 @@ export const TaskInstances = () => {
       <ActionBar.Root closeOnInteractOutside={false} 
open={Boolean(selectedRows.size)}>
         <ActionBar.Content>
           <ActionBar.SelectionTrigger>
-            {selectedRows.size} {translate("selected")}
+            {formatNumber(selectedRows.size, i18n.language)} 
{translate("selected")}
           </ActionBar.SelectionTrigger>
           <ActionBar.Separator />
           <BulkClearTaskInstancesButton
diff --git a/airflow-core/src/airflow/ui/src/pages/Variables/Variables.tsx 
b/airflow-core/src/airflow/ui/src/pages/Variables/Variables.tsx
index f2a8f194806..2d61a85c1cc 100644
--- a/airflow-core/src/airflow/ui/src/pages/Variables/Variables.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Variables/Variables.tsx
@@ -46,7 +46,7 @@ import { TeamName } from "src/components/TeamName";
 import { SearchParamsKeys, type SearchParamsKeysType } from 
"src/constants/searchParams";
 import { useAdvancedSearch } from "src/hooks/useAdvancedSearch";
 import { useConfig } from "src/queries/useConfig.tsx";
-import { useDocumentTitle } from "src/utils";
+import { formatNumber, useDocumentTitle } from "src/utils";
 import { TrimText } from "src/utils/TrimText";
 
 import DeleteVariablesButton from "./DeleteVariablesButton";
@@ -143,7 +143,7 @@ const getColumns = ({
 };
 
 export const Variables = () => {
-  const { t: translate } = useTranslation("admin");
+  const { i18n, t: translate } = useTranslation("admin");
 
   useDocumentTitle(translate("common:admin.Variables"));
 
@@ -246,7 +246,7 @@ export const Variables = () => {
       <ActionBar.Root closeOnInteractOutside={false} 
open={Boolean(selectedRows.size)}>
         <ActionBar.Content>
           <ActionBar.SelectionTrigger>
-            {selectedRows.size} {translate("deleteActions.selected")}
+            {formatNumber(selectedRows.size, i18n.language)} 
{translate("deleteActions.selected")}
           </ActionBar.SelectionTrigger>
           <ActionBar.Separator />
           <Tooltip content={translate("variables.delete.tooltip")}>
diff --git 
a/airflow-core/src/airflow/ui/src/system-components/Pagination/Item.tsx 
b/airflow-core/src/airflow/ui/src/system-components/Pagination/Item.tsx
index 8f7dcc35669..780a4f9b201 100644
--- a/airflow-core/src/airflow/ui/src/system-components/Pagination/Item.tsx
+++ b/airflow-core/src/airflow/ui/src/system-components/Pagination/Item.tsx
@@ -20,6 +20,9 @@ import { forwardRef } from "react";
 
 import type { ButtonProps } from "@chakra-ui/react";
 import { Button, Pagination as ChakraPagination, usePaginationContext } from 
"@chakra-ui/react";
+import { useTranslation } from "react-i18next";
+
+import { formatNumber } from "src/utils/formatNumber";
 
 import { paginationContext } from "./context";
 
@@ -33,6 +36,7 @@ export type PaginationRootProps = {
 } & Omit<ChakraPagination.RootProps, "type">;
 
 export const Item = forwardRef<HTMLButtonElement, 
ChakraPagination.ItemProps>((props, ref) => {
+  const { i18n } = useTranslation();
   const { page } = usePaginationContext();
   const { size, variantMap } = useRootProps();
 
@@ -42,7 +46,7 @@ export const Item = forwardRef<HTMLButtonElement, 
ChakraPagination.ItemProps>((p
   return (
     <ChakraPagination.Item ref={ref} {...props} asChild>
       <Button size={size} variant={variant}>
-        {props.value}
+        {formatNumber(props.value, i18n.language)}
       </Button>
     </ChakraPagination.Item>
   );
diff --git 
a/airflow-core/src/airflow/ui/src/system-components/Pagination/PageText.tsx 
b/airflow-core/src/airflow/ui/src/system-components/Pagination/PageText.tsx
index 30adc2a7581..455119332dc 100644
--- a/airflow-core/src/airflow/ui/src/system-components/Pagination/PageText.tsx
+++ b/airflow-core/src/airflow/ui/src/system-components/Pagination/PageText.tsx
@@ -20,6 +20,9 @@ import { forwardRef } from "react";
 
 import type { TextProps } from "@chakra-ui/react";
 import { Text, usePaginationContext } from "@chakra-ui/react";
+import { useTranslation } from "react-i18next";
+
+import { formatNumber } from "src/utils/formatNumber";
 
 type PageTextProps = {
   readonly format?: "compact" | "long" | "short";
@@ -27,12 +30,13 @@ type PageTextProps = {
 
 export const PageText = forwardRef<HTMLParagraphElement, 
PageTextProps>((props, ref) => {
   const { format = "compact", ...rest } = props;
+  const { i18n } = useTranslation();
   const { count, page, pageRange, pages } = usePaginationContext();
 
   const content = {
-    compact: `${page} of ${pages.length}`,
-    long: `${pageRange.start + 1} - ${pageRange.end} of ${count}`,
-    short: `${page} / ${pages.length}`,
+    compact: `${formatNumber(page, i18n.language)} of 
${formatNumber(pages.length, i18n.language)}`,
+    long: `${formatNumber(pageRange.start + 1, i18n.language)} - 
${formatNumber(pageRange.end, i18n.language)} of ${formatNumber(count, 
i18n.language)}`,
+    short: `${formatNumber(page, i18n.language)} / 
${formatNumber(pages.length, i18n.language)}`,
   };
 
   return (
diff --git 
a/airflow-core/src/airflow/ui/src/system-components/Select/ValueText.tsx 
b/airflow-core/src/airflow/ui/src/system-components/Select/ValueText.tsx
index 3466120a180..284d109ddd5 100644
--- a/airflow-core/src/airflow/ui/src/system-components/Select/ValueText.tsx
+++ b/airflow-core/src/airflow/ui/src/system-components/Select/ValueText.tsx
@@ -20,6 +20,9 @@ import { forwardRef, type ReactNode } from "react";
 
 import type { CollectionItem } from "@chakra-ui/react";
 import { Select as ChakraSelect } from "@chakra-ui/react";
+import { useTranslation } from "react-i18next";
+
+import { formatNumber } from "src/utils/formatNumber";
 
 type ValueTextProps = {
   readonly children?: (items: Array<CollectionItem>) => ReactNode;
@@ -27,6 +30,7 @@ type ValueTextProps = {
 
 export const ValueText = forwardRef<HTMLSpanElement, ValueTextProps>((props, 
ref) => {
   const { children, ...rest } = props;
+  const { i18n } = useTranslation();
 
   return (
     <ChakraSelect.ValueText {...rest} ref={ref}>
@@ -44,7 +48,7 @@ export const ValueText = forwardRef<HTMLSpanElement, 
ValueTextProps>((props, ref
             return select.collection.stringifyItem(items[0]);
           }
 
-          return `${items.length} selected`;
+          return `${formatNumber(items.length, i18n.language)} selected`;
         }}
       </ChakraSelect.Context>
     </ChakraSelect.ValueText>
diff --git a/airflow-core/src/airflow/ui/src/utils/datetimeUtils.ts 
b/airflow-core/src/airflow/ui/src/utils/datetimeUtils.ts
index 1ed1f8f3b6f..cd513705a14 100644
--- a/airflow-core/src/airflow/ui/src/utils/datetimeUtils.ts
+++ b/airflow-core/src/airflow/ui/src/utils/datetimeUtils.ts
@@ -21,6 +21,8 @@ import dayjsDuration from "dayjs/plugin/duration";
 import tz from "dayjs/plugin/timezone";
 import i18n from "i18next";
 
+import { createIntlCache, DEFAULT_LOCALE } from "./intlCache";
+
 dayjs.extend(dayjsDuration);
 dayjs.extend(tz);
 
@@ -28,7 +30,6 @@ export const DATE_FORMAT = "YYYY-MM-DD";
 export const DEFAULT_DATETIME_FORMAT = `${DATE_FORMAT} HH:mm:ss`;
 export const DEFAULT_DATETIME_FORMAT_WITH_TZ = `${DEFAULT_DATETIME_FORMAT} z`;
 
-const DEFAULT_LOCALE = "en";
 const SECONDS_PER_MINUTE = 60;
 const SECONDS_PER_HOUR = 3600;
 const SECONDS_PER_DAY = 86_400;
@@ -42,34 +43,6 @@ type DurationPart = { fractionDigits?: number; unit: 
DurationUnit; value: number
 /** `narrow` ("1h 2m") suits dense tables and charts; `long` ("1 hour, 2 
minutes") suits prose. */
 type DurationStyle = "long" | "narrow";
 
-// Intl constructors are costly and durations render in every table row and 
chart tick callback, so
-// instances are reused. A stored language Intl rejects must not blank out 
every duration in the UI,
-// hence the fallback to DEFAULT_LOCALE rather than letting the RangeError 
escape.
-const createIntlCache = <T>() => {
-  const cache = new Map<string, T>();
-
-  return (variant: string, locale: string, construct: (forLocale: string) => 
T): T => {
-    const key = `${locale}|${variant}`;
-    const cached = cache.get(key);
-
-    if (cached !== undefined) {
-      return cached;
-    }
-
-    let formatter: T;
-
-    try {
-      formatter = construct(locale);
-    } catch {
-      formatter = construct(DEFAULT_LOCALE);
-    }
-
-    cache.set(key, formatter);
-
-    return formatter;
-  };
-};
-
 const unitFormatter = createIntlCache<Intl.NumberFormat>();
 const listFormatter = createIntlCache<Intl.ListFormat>();
 const relativeTimeFormatter = createIntlCache<Intl.RelativeTimeFormat>();
diff --git a/airflow-core/src/airflow/ui/src/components/ImportErrorCount.tsx 
b/airflow-core/src/airflow/ui/src/utils/formatNumber.test.ts
similarity index 57%
copy from airflow-core/src/airflow/ui/src/components/ImportErrorCount.tsx
copy to airflow-core/src/airflow/ui/src/utils/formatNumber.test.ts
index 8459b9cf4d2..671cdadf994 100644
--- a/airflow-core/src/airflow/ui/src/components/ImportErrorCount.tsx
+++ b/airflow-core/src/airflow/ui/src/utils/formatNumber.test.ts
@@ -16,28 +16,19 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Badge, Text } from "@chakra-ui/react";
+import { describe, expect, it } from "vitest";
 
-type Props = {
-  readonly count: number | null;
-};
+import { formatNumber } from "./formatNumber";
 
-/**
- * An import-error count, flagged red only when there is something to flag.
- *
- * Null is not zero: it means the caller may not read import errors, so it 
renders as unknown
- * rather than as a clean bill of health.
- */
-export const ImportErrorCount = ({ count }: Props) => {
-  if (count === null) {
-    return <Text color="fg.muted">-</Text>;
-  }
+describe("formatNumber", () => {
+  it.each([
+    ["en", "1,234"],
+    ["de", "1.234"],
+  ])("groups digits for %s", (locale, expected) => {
+    expect(formatNumber(1234, locale)).toBe(expected);
+  });
 
-  return count === 0 ? (
-    <Text color="fg.muted">0</Text>
-  ) : (
-    <Badge colorPalette="failed" variant="solid">
-      {count}
-    </Badge>
-  );
-};
+  it("falls back to the default locale instead of throwing on a malformed 
tag", () => {
+    expect(formatNumber(1234, "pt_BR")).toBe("1,234");
+  });
+});
diff --git a/airflow-core/src/airflow/ui/src/components/ImportErrorCount.tsx 
b/airflow-core/src/airflow/ui/src/utils/formatNumber.ts
similarity index 54%
copy from airflow-core/src/airflow/ui/src/components/ImportErrorCount.tsx
copy to airflow-core/src/airflow/ui/src/utils/formatNumber.ts
index 8459b9cf4d2..34932a7cb1e 100644
--- a/airflow-core/src/airflow/ui/src/components/ImportErrorCount.tsx
+++ b/airflow-core/src/airflow/ui/src/utils/formatNumber.ts
@@ -16,28 +16,16 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Badge, Text } from "@chakra-ui/react";
+import { createIntlCache } from "./intlCache";
 
-type Props = {
-  readonly count: number | null;
-};
+const numberFormatter = createIntlCache<Intl.NumberFormat>();
 
 /**
- * An import-error count, flagged red only when there is something to flag.
+ * Locale digit grouping for counters rendered outside translations; use 
instead of `toLocaleString`.
  *
- * Null is not zero: it means the caller may not read import errors, so it 
renders as unknown
- * rather than as a clean bill of health.
+ * Components pass `i18n.language` from `useTranslation`, which the React 
Compiler tracks (react-i18next
+ * hands out a new `i18n` wrapper on every language switch). A language read 
from the i18next singleton
+ * in here is invisible to the compiler, so a memoized counter would keep its 
old grouping.
  */
-export const ImportErrorCount = ({ count }: Props) => {
-  if (count === null) {
-    return <Text color="fg.muted">-</Text>;
-  }
-
-  return count === 0 ? (
-    <Text color="fg.muted">0</Text>
-  ) : (
-    <Badge colorPalette="failed" variant="solid">
-      {count}
-    </Badge>
-  );
-};
+export const formatNumber = (value: number, locale: string): string =>
+  numberFormatter("number", locale, (forLocale) => new 
Intl.NumberFormat(forLocale)).format(value);
diff --git a/airflow-core/src/airflow/ui/src/utils/index.ts 
b/airflow-core/src/airflow/ui/src/utils/index.ts
index fc958e3ea37..84d5097eea8 100644
--- a/airflow-core/src/airflow/ui/src/utils/index.ts
+++ b/airflow-core/src/airflow/ui/src/utils/index.ts
@@ -19,6 +19,7 @@
 
 export { capitalize } from "./capitalize";
 export { createErrorToaster, getErrorStatus } from "./errorHandling";
+export { formatNumber } from "./formatNumber";
 export { getMetaKey } from "./getMetaKey";
 export { hasDagRunConfig } from "./hasDagRunConfig";
 export { toNullablePartitionKey } from "./partitionKey";
diff --git a/airflow-core/src/airflow/ui/src/utils/intlCache.ts 
b/airflow-core/src/airflow/ui/src/utils/intlCache.ts
new file mode 100644
index 00000000000..3090d959166
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/utils/intlCache.ts
@@ -0,0 +1,48 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+export const DEFAULT_LOCALE = "en";
+
+// Intl constructors are costly and durations and counters render in every 
table row and chart tick
+// callback, so instances are reused. A stored language Intl rejects (e.g. a 
plugin-contributed "pt_BR")
+// must not blank out or crash every duration and counter in the UI,
+// hence the fallback to DEFAULT_LOCALE rather than letting the RangeError 
escape.
+export const createIntlCache = <T>() => {
+  const cache = new Map<string, T>();
+
+  return (variant: string, locale: string, construct: (forLocale: string) => 
T): T => {
+    const key = `${locale}|${variant}`;
+    const cached = cache.get(key);
+
+    if (cached !== undefined) {
+      return cached;
+    }
+
+    let formatter: T;
+
+    try {
+      formatter = construct(locale);
+    } catch {
+      formatter = construct(DEFAULT_LOCALE);
+    }
+
+    cache.set(key, formatter);
+
+    return formatter;
+  };
+};

Reply via email to