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 231c29c1d42 Migrate dags list plaintext to translate files (#51181)
231c29c1d42 is described below

commit 231c29c1d4235909d99ace1daab67068fdd3fe54
Author: Brent Bovenzi <[email protected]>
AuthorDate: Thu May 29 12:30:42 2025 -0400

    Migrate dags list plaintext to translate files (#51181)
    
    * Migrate dags list plaintext to translate files
    
    * Update SearchBar, DagOwners and fix asset schedule
    
    * Fix test
    
    * Alphabetically sort json
---
 .../src/components/DagActions/DeleteDagButton.tsx  |  11 +-
 .../src/components/DataTable/FilterMenuButton.tsx  |  76 +++---
 .../src/airflow/ui/src/components/DeleteDialog.tsx |  62 ++---
 .../airflow/ui/src/components/SearchBar.test.tsx   |   2 +-
 .../src/airflow/ui/src/components/SearchBar.tsx    |   7 +-
 .../src/components/TriggerDag/TriggerDAGButton.tsx |   6 +-
 .../src/airflow/ui/src/constants/sortParams.ts     |  56 ++--
 airflow-core/src/airflow/ui/src/i18n/config.ts     |   4 +-
 .../src/airflow/ui/src/i18n/locales/en/common.json |  23 +-
 .../src/airflow/ui/src/i18n/locales/en/dags.json   |  55 ++++
 .../src/airflow/ui/src/layouts/DagsLayout.tsx      |  27 +-
 .../ui/src/pages/DagsList/AssetSchedule.tsx        |   5 +-
 .../src/airflow/ui/src/pages/DagsList/DagCard.tsx  |   8 +-
 .../airflow/ui/src/pages/DagsList/DagOwners.tsx    |   6 +-
 .../airflow/ui/src/pages/DagsList/DagsFilters.tsx  | 284 ---------------------
 .../src/pages/DagsList/DagsFilters/DagsFilters.tsx | 181 +++++++++++++
 .../pages/DagsList/DagsFilters/PausedFilter.tsx    |  59 +++++
 .../DagsList/DagsFilters/ResetButton.tsx}          |  34 ++-
 .../pages/DagsList/DagsFilters/StateFilters.tsx    |  70 +++++
 .../src/pages/DagsList/DagsFilters/TagFilter.tsx   | 101 ++++++++
 .../DagsList/DagsFilters/index.ts}                 |  17 +-
 .../src/airflow/ui/src/pages/DagsList/DagsList.tsx |  37 +--
 .../airflow/ui/src/pages/DagsList/RecentRuns.tsx   |  17 +-
 .../src/airflow/ui/src/pages/DagsList/Schedule.tsx |  10 +-
 .../airflow/ui/src/pages/DagsList/SortSelect.tsx   |  48 ++--
 25 files changed, 730 insertions(+), 476 deletions(-)

diff --git 
a/airflow-core/src/airflow/ui/src/components/DagActions/DeleteDagButton.tsx 
b/airflow-core/src/airflow/ui/src/components/DagActions/DeleteDagButton.tsx
index 28645696b15..afddb68436f 100644
--- a/airflow-core/src/airflow/ui/src/components/DagActions/DeleteDagButton.tsx
+++ b/airflow-core/src/airflow/ui/src/components/DagActions/DeleteDagButton.tsx
@@ -17,6 +17,7 @@
  * under the License.
  */
 import { useDisclosure } from "@chakra-ui/react";
+import { useTranslation } from "react-i18next";
 import { FiTrash2 } from "react-icons/fi";
 import { useNavigate } from "react-router-dom";
 
@@ -33,7 +34,7 @@ type DeleteDagButtonProps = {
 const DeleteDagButton = ({ dagDisplayName, dagId, withText = true }: 
DeleteDagButtonProps) => {
   const { onClose, onOpen, open } = useDisclosure();
   const navigate = useNavigate();
-
+  const { t: translate } = useTranslation("dags");
   const { isPending, mutate: deleteDag } = useDeleteDag({
     dagId,
     onSuccessConfirm: () => {
@@ -45,11 +46,11 @@ const DeleteDagButton = ({ dagDisplayName, dagId, withText 
= true }: DeleteDagBu
   return (
     <>
       <ActionButton
-        actionName="Delete DAG"
+        actionName={translate("actions.delete")}
         colorPalette="red"
         icon={<FiTrash2 />}
         onClick={onOpen}
-        text="Delete DAG"
+        text={translate("actions.delete")}
         variant="solid"
         withText={withText}
       />
@@ -60,8 +61,8 @@ const DeleteDagButton = ({ dagDisplayName, dagId, withText = 
true }: DeleteDagBu
         onDelete={() => deleteDag({ dagId })}
         open={open}
         resourceName={dagDisplayName}
-        title="Delete DAG"
-        warningText="This will remove all metadata related to the DAG, 
including DAG Runs and Tasks."
+        title={translate("actions.delete")}
+        warningText={translate("actions.deleteDagWarning")}
       />
     </>
   );
diff --git 
a/airflow-core/src/airflow/ui/src/components/DataTable/FilterMenuButton.tsx 
b/airflow-core/src/airflow/ui/src/components/DataTable/FilterMenuButton.tsx
index 6b98ab0df63..189e177200f 100644
--- a/airflow-core/src/airflow/ui/src/components/DataTable/FilterMenuButton.tsx
+++ b/airflow-core/src/airflow/ui/src/components/DataTable/FilterMenuButton.tsx
@@ -18,6 +18,7 @@
  */
 import { IconButton } from "@chakra-ui/react";
 import { flexRender, type Header, type Table } from "@tanstack/react-table";
+import { useTranslation } from "react-i18next";
 import { MdFilterList } from "react-icons/md";
 
 import { Menu } from "src/components/ui";
@@ -27,44 +28,43 @@ type Props<TData> = {
   readonly table: Table<TData>;
 };
 
-const FilterMenuButton = <TData,>({ table }: Props<TData>) => (
-  <Menu.Root closeOnSelect={false} positioning={{ placement: "bottom" }}>
-    <Menu.Trigger asChild>
-      <IconButton
-        aria-label="Filter table columns"
-        margin={1}
-        padding={0}
-        title="Filter table columns"
-        variant="ghost"
-      >
-        <MdFilterList size="1" />
-      </IconButton>
-    </Menu.Trigger>
-    <Menu.Content>
-      {table.getAllLeafColumns().map((column) => {
-        const text = flexRender(column.columnDef.header, {
-          column,
-          header: { column } as Header<TData, unknown>,
-          table,
-        });
+const FilterMenuButton = <TData,>({ table }: Props<TData>) => {
+  const { t: translate } = useTranslation("common");
+  const filterLabel = translate("table.filterColumns");
 
-        return text?.toString ? (
-          <Menu.Item asChild key={column.id} value={column.id}>
-            <Checkbox
-              checked={column.getIsVisible()}
-              // At least one item needs to be visible
-              disabled={table.getVisibleFlatColumns().length < 2 && 
column.getIsVisible()}
-              onChange={() => {
-                column.toggleVisibility();
-              }}
-            >
-              {text}
-            </Checkbox>
-          </Menu.Item>
-        ) : undefined;
-      })}
-    </Menu.Content>
-  </Menu.Root>
-);
+  return (
+    <Menu.Root closeOnSelect={false} positioning={{ placement: "bottom" }}>
+      <Menu.Trigger asChild>
+        <IconButton aria-label={filterLabel} margin={1} padding={0} 
title={filterLabel} variant="ghost">
+          <MdFilterList size="1" />
+        </IconButton>
+      </Menu.Trigger>
+      <Menu.Content>
+        {table.getAllLeafColumns().map((column) => {
+          const text = flexRender(column.columnDef.header, {
+            column,
+            header: { column } as Header<TData, unknown>,
+            table,
+          });
+
+          return text?.toString ? (
+            <Menu.Item asChild key={column.id} value={column.id}>
+              <Checkbox
+                checked={column.getIsVisible()}
+                // At least one item needs to be visible
+                disabled={table.getVisibleFlatColumns().length < 2 && 
column.getIsVisible()}
+                onChange={() => {
+                  column.toggleVisibility();
+                }}
+              >
+                {text}
+              </Checkbox>
+            </Menu.Item>
+          ) : undefined;
+        })}
+      </Menu.Content>
+    </Menu.Root>
+  );
+};
 
 export default FilterMenuButton;
diff --git a/airflow-core/src/airflow/ui/src/components/DeleteDialog.tsx 
b/airflow-core/src/airflow/ui/src/components/DeleteDialog.tsx
index 6d8ac6b56fc..b77262838bf 100644
--- a/airflow-core/src/airflow/ui/src/components/DeleteDialog.tsx
+++ b/airflow-core/src/airflow/ui/src/components/DeleteDialog.tsx
@@ -18,6 +18,7 @@
  */
 import { Text, Heading, HStack } from "@chakra-ui/react";
 import React from "react";
+import { useTranslation } from "react-i18next";
 import { FiTrash2 } from "react-icons/fi";
 
 import { Button, Dialog } from "src/components/ui";
@@ -34,7 +35,7 @@ type DeleteDialogProps = {
 };
 
 const DeleteDialog: React.FC<DeleteDialogProps> = ({
-  deleteButtonText = "Delete",
+  deleteButtonText,
   isDeleting,
   onClose,
   onDelete,
@@ -42,33 +43,36 @@ const DeleteDialog: React.FC<DeleteDialogProps> = ({
   resourceName,
   title,
   warningText,
-}) => (
-  <Dialog.Root lazyMount onOpenChange={onClose} open={open} size="md" 
unmountOnExit>
-    <Dialog.Content backdrop>
-      <Dialog.Header>
-        <Heading size="lg">{title}</Heading>
-      </Dialog.Header>
-      <Dialog.CloseTrigger />
-      <Dialog.Body>
-        <Text>
-          Are you sure you want to delete <strong>{resourceName}</strong>? 
This action cannot be undone.
-        </Text>
-        <Text color="red.500" fontWeight="bold" mt={4}>
-          {warningText}
-        </Text>
-      </Dialog.Body>
-      <Dialog.Footer>
-        <HStack justifyContent="flex-end" width="100%">
-          <Button onClick={onClose} variant="outline">
-            Cancel
-          </Button>
-          <Button colorPalette="red" loading={isDeleting} onClick={onDelete}>
-            <FiTrash2 style={{ marginRight: "8px" }} /> {deleteButtonText}
-          </Button>
-        </HStack>
-      </Dialog.Footer>
-    </Dialog.Content>
-  </Dialog.Root>
-);
+}) => {
+  const { t: translate } = useTranslation("common");
+
+  return (
+    <Dialog.Root lazyMount onOpenChange={onClose} open={open} size="md" 
unmountOnExit>
+      <Dialog.Content backdrop>
+        <Dialog.Header>
+          <Heading size="lg">{title}</Heading>
+        </Dialog.Header>
+        <Dialog.CloseTrigger />
+        <Dialog.Body>
+          <Text>{translate("modal.delete.confirmation", { resourceName 
})}</Text>
+          <Text color="red.500" fontWeight="bold" mt={4}>
+            {warningText}
+          </Text>
+        </Dialog.Body>
+        <Dialog.Footer>
+          <HStack justifyContent="flex-end" width="100%">
+            <Button onClick={onClose} variant="outline">
+              {translate("modal.cancel")}
+            </Button>
+            <Button colorPalette="red" loading={isDeleting} onClick={onDelete}>
+              <FiTrash2 style={{ marginRight: "8px" }} />{" "}
+              {deleteButtonText ?? translate("modal.delete.button")}
+            </Button>
+          </HStack>
+        </Dialog.Footer>
+      </Dialog.Content>
+    </Dialog.Root>
+  );
+};
 
 export default DeleteDialog;
diff --git a/airflow-core/src/airflow/ui/src/components/SearchBar.test.tsx 
b/airflow-core/src/airflow/ui/src/components/SearchBar.test.tsx
index 264b82c7705..0b0faaf6b4b 100644
--- a/airflow-core/src/airflow/ui/src/components/SearchBar.test.tsx
+++ b/airflow-core/src/airflow/ui/src/components/SearchBar.test.tsx
@@ -31,7 +31,7 @@ describe("Test SearchBar", () => {
 
     const input = screen.getByTestId("search-dags");
 
-    expect(screen.getByText("Advanced Search")).toBeDefined();
+    expect(screen.getByText("advancedSearch")).toBeDefined();
     expect(screen.queryByTestId("clear-search")).toBeNull();
 
     fireEvent.change(input, { target: { value: "search" } });
diff --git a/airflow-core/src/airflow/ui/src/components/SearchBar.tsx 
b/airflow-core/src/airflow/ui/src/components/SearchBar.tsx
index 5578aae5415..b70a5655b71 100644
--- a/airflow-core/src/airflow/ui/src/components/SearchBar.tsx
+++ b/airflow-core/src/airflow/ui/src/components/SearchBar.tsx
@@ -19,6 +19,7 @@
 import { Button, Input, Kbd, type ButtonProps } from "@chakra-ui/react";
 import { useState, useRef, type ChangeEvent } from "react";
 import { useHotkeys } from "react-hotkeys-hook";
+import { useTranslation } from "react-i18next";
 import { FiSearch } from "react-icons/fi";
 import { useDebouncedCallback } from "use-debounce";
 
@@ -51,7 +52,7 @@ export const SearchBar = ({
   const searchRef = useRef<HTMLInputElement>(null);
   const [value, setValue] = useState(defaultValue);
   const metaKey = getMetaKey();
-
+  const { t: translate } = useTranslation(["dags"]);
   const onSearchChange = (event: ChangeEvent<HTMLInputElement>) => {
     setValue(event.target.value);
     handleSearchChange(event.target.value);
@@ -73,7 +74,7 @@ export const SearchBar = ({
         <>
           {Boolean(value) ? (
             <CloseButton
-              aria-label="Clear search"
+              aria-label={translate("clearSearch")}
               colorPalette="gray"
               data-testid="clear-search"
               onClick={() => {
@@ -85,7 +86,7 @@ export const SearchBar = ({
           ) : undefined}
           {Boolean(hideAdvanced) ? undefined : (
             <Button fontWeight="normal" height="1.75rem" variant="ghost" 
width={140} {...buttonProps}>
-              Advanced Search
+              {translate("advancedSearch")}
             </Button>
           )}
           {!hotkeyDisabled && <Kbd size="sm">{metaKey}+K</Kbd>}
diff --git 
a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGButton.tsx 
b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGButton.tsx
index ce79b04dd2a..07f87d11cb7 100644
--- a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGButton.tsx
+++ b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGButton.tsx
@@ -18,6 +18,7 @@
  */
 import { Box } from "@chakra-ui/react";
 import { useDisclosure } from "@chakra-ui/react";
+import { useTranslation } from "react-i18next";
 import { FiPlay } from "react-icons/fi";
 
 import type { DAGResponse, DAGWithLatestDagRunsResponse } from 
"openapi/requests/types.gen";
@@ -32,15 +33,16 @@ type Props = {
 
 const TriggerDAGButton: React.FC<Props> = ({ dag, withText = true }) => {
   const { onClose, onOpen, open } = useDisclosure();
+  const { t: translate } = useTranslation("dags");
 
   return (
     <Box>
       <ActionButton
-        actionName="Trigger Dag"
+        actionName={translate("actions.triggerDag")}
         colorPalette="blue"
         icon={<FiPlay />}
         onClick={onOpen}
-        text="Trigger"
+        text={translate("actions.trigger")}
         variant="solid"
         withText={withText}
       />
diff --git a/airflow-core/src/airflow/ui/src/constants/sortParams.ts 
b/airflow-core/src/airflow/ui/src/constants/sortParams.ts
index 3ffc36844e8..39864ba7f95 100644
--- a/airflow-core/src/airflow/ui/src/constants/sortParams.ts
+++ b/airflow-core/src/airflow/ui/src/constants/sortParams.ts
@@ -17,22 +17,42 @@
  * under the License.
  */
 import { createListCollection } from "@chakra-ui/react/collection";
+import type { TFunction } from "i18next";
 
-export const dagSortOptions = createListCollection({
-  items: [
-    { label: "Sort by Display Name (A-Z)", value: "dag_display_name" },
-    { label: "Sort by Display Name (Z-A)", value: "-dag_display_name" },
-    { label: "Sort by Next DAG Run (Earliest-Latest)", value: "next_dagrun" },
-    { label: "Sort by Next DAG Run (Latest-Earliest)", value: "-next_dagrun" },
-    { label: "Sort by Latest Run State (A-Z)", value: "last_run_state" },
-    { label: "Sort by Latest Run State (Z-A)", value: "-last_run_state" },
-    {
-      label: "Sort by Latest Run Start Date (Earliest-Latest)",
-      value: "last_run_start_date",
-    },
-    {
-      label: "Sort by Latest Run Start Date (Latest-Earliest)",
-      value: "-last_run_start_date",
-    },
-  ],
-});
+export const createDagSortOptions = (translate: TFunction) =>
+  createListCollection({
+    items: [
+      {
+        label: translate("sort.displayName.asc"),
+        value: "dag_display_name",
+      },
+      {
+        label: translate("sort.displayName.desc"),
+        value: "-dag_display_name",
+      },
+      {
+        label: translate("sort.nextDagRun.asc"),
+        value: "next_dagrun",
+      },
+      {
+        label: translate("sort.nextDagRun.desc"),
+        value: "-next_dagrun",
+      },
+      {
+        label: translate("sort.lastRunState.asc"),
+        value: "last_run_state",
+      },
+      {
+        label: translate("sort.lastRunState.desc"),
+        value: "-last_run_state",
+      },
+      {
+        label: translate("sort.lastRunStartDate.asc"),
+        value: "last_run_start_date",
+      },
+      {
+        label: translate("sort.lastRunStartDate.desc"),
+        value: "-last_run_start_date",
+      },
+    ],
+  });
diff --git a/airflow-core/src/airflow/ui/src/i18n/config.ts 
b/airflow-core/src/airflow/ui/src/i18n/config.ts
index 944ca987bd5..045216407a9 100644
--- a/airflow-core/src/airflow/ui/src/i18n/config.ts
+++ b/airflow-core/src/airflow/ui/src/i18n/config.ts
@@ -23,6 +23,7 @@ import { initReactI18next } from "react-i18next";
 import deCommon from "./locales/de/common.json";
 import deDashboard from "./locales/de/dashboard.json";
 import enCommon from "./locales/en/common.json";
+import enDags from "./locales/en/dags.json";
 import enDashboard from "./locales/en/dashboard.json";
 import koCommon from "./locales/ko/common.json";
 import koDashboard from "./locales/ko/dashboard.json";
@@ -43,7 +44,7 @@ export const supportedLanguages = [
 ] as const;
 
 export const defaultLanguage = "en";
-export const namespaces = ["common", "dashboard"] as const;
+export const namespaces = ["common", "dashboard", "dags"] as const;
 
 const resources = {
   de: {
@@ -52,6 +53,7 @@ const resources = {
   },
   en: {
     common: enCommon,
+    dags: enDags,
     dashboard: enDashboard,
   },
   ko: {
diff --git a/airflow-core/src/airflow/ui/src/i18n/locales/en/common.json 
b/airflow-core/src/airflow/ui/src/i18n/locales/en/common.json
index b1a3a0d0c42..59e26eae483 100644
--- a/airflow-core/src/airflow/ui/src/i18n/locales/en/common.json
+++ b/airflow-core/src/airflow/ui/src/i18n/locales/en/common.json
@@ -13,6 +13,8 @@
     "auditLog": "Audit Log",
     "xcoms": "XComs"
   },
+  "dag_one": "Dag",
+  "dag_other": "Dags",
   "dagRun_one": "Dag Run",
   "dagRun_other": "Dag Runs",
   "defaultToGraphView": "Default to graph view",
@@ -26,7 +28,11 @@
   "logoutConfirmation": "You are about to logout from the application.",
   "modal": {
     "cancel": "Cancel",
-    "confirm": "Confirm"
+    "confirm": "Confirm",
+    "delete": {
+      "button": "Delete",
+      "confirmation": "Are you sure you want to delete {{resourceName}}? This 
action cannot be undone."
+    }
   },
   "nav": {
     "admin": "Admin",
@@ -72,6 +78,21 @@
   },
   "switchToDarkMode": "Switch to Dark Mode",
   "switchToLightMode": "Switch to Light Mode",
+  "table": {
+    "filterByTag": "Filter Dags by tag",
+    "filterColumns": "Filter table columns",
+    "filters": {
+      "filter_one": "filter",
+      "filter_other": "filters",
+      "reset": "Reset"
+    },
+    "noTagsFound": "No tags found",
+    "tagMode": {
+      "all": "All",
+      "any": "Any"
+    },
+    "tagPlaceholder": "Filter by tag"
+  },
   "taskInstance_one": "Task Instance",
   "taskInstance_other": "Task Instances",
   "timeRange": {
diff --git a/airflow-core/src/airflow/ui/src/i18n/locales/en/dags.json 
b/airflow-core/src/airflow/ui/src/i18n/locales/en/dags.json
new file mode 100644
index 00000000000..cb6db5cc3ce
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/i18n/locales/en/dags.json
@@ -0,0 +1,55 @@
+{
+  "actions": {
+    "delete": "Delete Dag",
+    "deleteDagWarning": "This will remove all metadata related to the DAG, 
including DAG Runs and Tasks.",
+    "trigger": "Trigger",
+    "triggerDag": "Trigger Dag"
+  },
+  "assetSchedule": "{{count}} of {{total}} assets updated",
+  "filters": {
+    "paused": {
+      "active": "Active",
+      "all": "All",
+      "paused": "Paused"
+    }
+  },
+  "list": {
+    "advancedSearch": "Advanced Search",
+    "clearSearch": "Clear search",
+    "columns": {
+      "dagId": "Dag ID",
+      "lastDagRun": "Last Dag Run",
+      "nextDagRun": "Next Dag Run",
+      "schedule": "Schedule",
+      "tags": "Tags"
+    },
+    "ownerLink": "Owner link for {{owner}}",
+    "runs": {
+      "duration": "Duration",
+      "endDate": "End Date",
+      "runAfter": "Run After",
+      "startDate": "Start Date",
+      "state": "State"
+    },
+    "searchPlaceholder": "Search Dags"
+  },
+  "sort": {
+    "displayName": {
+      "asc": "Sort by Display Name (A-Z)",
+      "desc": "Sort by Display Name (Z-A)"
+    },
+    "lastRunStartDate": {
+      "asc": "Sort by Latest Run Start Date (Earliest-Latest)",
+      "desc": "Sort by Latest Run Start Date (Latest-Earliest)"
+    },
+    "lastRunState": {
+      "asc": "Sort by Latest Run State (A-Z)",
+      "desc": "Sort by Latest Run State (Z-A)"
+    },
+    "nextDagRun": {
+      "asc": "Sort by Next Dag Run (Earliest-Latest)",
+      "desc": "Sort by Next Dag Run (Latest-Earliest)"
+    },
+    "placeholder": "Sort by"
+  }
+}
diff --git a/airflow-core/src/airflow/ui/src/layouts/DagsLayout.tsx 
b/airflow-core/src/airflow/ui/src/layouts/DagsLayout.tsx
index 331fd77e68f..3838179e0d4 100644
--- a/airflow-core/src/airflow/ui/src/layouts/DagsLayout.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/DagsLayout.tsx
@@ -18,18 +18,23 @@
  */
 import { Box } from "@chakra-ui/react";
 import type { PropsWithChildren } from "react";
+import { useTranslation } from "react-i18next";
 
 import { NavTabs } from "./Details/NavTabs";
 
-const tabs = [
-  { label: "Dags", value: "/dags" },
-  { label: "Runs", value: "/dag_runs" },
-  { label: "Task Instances", value: "/task_instances" },
-];
+export const DagsLayout = ({ children }: PropsWithChildren) => {
+  const { t: translate } = useTranslation("common");
 
-export const DagsLayout = ({ children }: PropsWithChildren) => (
-  <Box>
-    <NavTabs tabs={tabs} />
-    {children}
-  </Box>
-);
+  const tabs = [
+    { label: translate("nav.dags"), value: "/dags" },
+    { label: translate("dagRun_other"), value: "/dag_runs" },
+    { label: translate("taskInstance_other"), value: "/task_instances" },
+  ];
+
+  return (
+    <Box>
+      <NavTabs tabs={tabs} />
+      {children}
+    </Box>
+  );
+};
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/AssetSchedule.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/AssetSchedule.tsx
index c566a5532ac..39a51c4be0c 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/AssetSchedule.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/AssetSchedule.tsx
@@ -18,6 +18,7 @@
  */
 import { HStack, Text, Link } from "@chakra-ui/react";
 import dayjs from "dayjs";
+import { useTranslation } from "react-i18next";
 import { FiDatabase } from "react-icons/fi";
 import { Link as RouterLink } from "react-router-dom";
 
@@ -32,6 +33,7 @@ type Props = {
 };
 
 export const AssetSchedule = ({ dag }: Props) => {
+  const { t: translate } = useTranslation("dags");
   const { data: nextRun, isLoading } = useAssetServiceNextRunAssets({ dagId: 
dag.dag_id });
 
   const nextRunEvents = (nextRun?.events ?? []) as Array<NextRunEvent>;
@@ -72,8 +74,7 @@ export const AssetSchedule = ({ dag }: Props) => {
       <Popover.Trigger asChild>
         <Button loading={isLoading} paddingInline={0} size="sm" 
variant="ghost">
           <FiDatabase style={{ display: "inline" }} />
-          {pendingEvents.length}
-          {nextRunEvents.length > 1 ? ` of ${nextRunEvents.length} ` : " 
"}assets updated
+          {translate("assetSchedule", { count: pendingEvents.length, total: 
nextRunEvents.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/DagsList/DagCard.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx
index 26b2655c26a..46a9b362a72 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx
@@ -17,6 +17,7 @@
  * under the License.
  */
 import { Box, Flex, HStack, SimpleGrid, Link, Spinner } from 
"@chakra-ui/react";
+import { useTranslation } from "react-i18next";
 import { Link as RouterLink } from "react-router-dom";
 
 import type { DAGWithLatestDagRunsResponse } from "openapi/requests/types.gen";
@@ -37,6 +38,7 @@ type Props = {
 };
 
 export const DagCard = ({ dag }: Props) => {
+  const { t: translate } = useTranslation(["dags", "common"]);
   const [latestRun] = dag.latest_dag_runs;
 
   const refetchInterval = useAutoRefresh({ isPaused: dag.is_paused });
@@ -64,10 +66,10 @@ export const DagCard = ({ dag }: Props) => {
         </HStack>
       </Flex>
       <SimpleGrid columns={4} gap={1} height={20} px={3} py={1}>
-        <Stat label="Schedule">
+        <Stat label={translate("list.columns.schedule")}>
           <Schedule dag={dag} />
         </Stat>
-        <Stat label="Latest Run">
+        <Stat label={translate("list.columns.lastDagRun")}>
           {latestRun ? (
             <Link asChild color="fg.info">
               <RouterLink 
to={`/dags/${latestRun.dag_id}/runs/${latestRun.dag_run_id}`}>
@@ -83,7 +85,7 @@ export const DagCard = ({ dag }: Props) => {
             </Link>
           ) : undefined}
         </Stat>
-        <Stat label="Next Run">
+        <Stat label={translate("list.columns.nextDagRun")}>
           {Boolean(dag.next_dagrun_run_after) ? (
             <DagRunInfo
               logicalDate={dag.next_dagrun_logical_date}
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagOwners.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagOwners.tsx
index 1ba38be2510..70fb6195c0a 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagOwners.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagOwners.tsx
@@ -17,6 +17,7 @@
  * under the License.
  */
 import { Link, Text } from "@chakra-ui/react";
+import { useTranslation } from "react-i18next";
 
 import { LimitedItemsList } from "src/components/LimitedItemsList";
 
@@ -30,13 +31,16 @@ export const DagOwners = ({
   readonly ownerLinks?: Record<string, string> | null;
   readonly owners?: Array<string>;
 }) => {
+  const { t: translate } = useTranslation("dags");
   const items = owners.map((owner) => {
     const link = ownerLinks?.[owner];
     const hasOwnerLink = link !== undefined;
 
     return hasOwnerLink ? (
       <Link
-        aria-label={`Owner link for ${owner}`}
+        aria-label={translate("list.ownerLink", { owner })}
+        asChild
+        color="fg.info"
         href={link}
         key={owner}
         rel="noopener noreferrer"
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters.tsx
deleted file mode 100644
index c44a87410d8..00000000000
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters.tsx
+++ /dev/null
@@ -1,284 +0,0 @@
-/*!
- * 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 {
-  Box,
-  Button,
-  createListCollection,
-  Field,
-  HStack,
-  Text,
-  type SelectValueChangeDetails,
-} from "@chakra-ui/react";
-import { Select as ReactSelect, type MultiValue } from "chakra-react-select";
-import { useCallback } from "react";
-import { LuX } from "react-icons/lu";
-import { useSearchParams } from "react-router-dom";
-
-import { useDagServiceGetDagTags } from "openapi/queries";
-import { useTableURLState } from "src/components/DataTable/useTableUrlState";
-import { QuickFilterButton } from "src/components/QuickFilterButton";
-import { StateBadge } from "src/components/StateBadge";
-import { Select, Switch } from "src/components/ui";
-import { SearchParamsKeys, type SearchParamsKeysType } from 
"src/constants/searchParams";
-import { useConfig } from "src/queries/useConfig";
-import { pluralize } from "src/utils";
-
-const {
-  LAST_DAG_RUN_STATE: LAST_DAG_RUN_STATE_PARAM,
-  PAUSED: PAUSED_PARAM,
-  TAGS: TAGS_PARAM,
-  TAGS_MATCH_MODE: TAGS_MATCH_MODE_PARAM,
-}: SearchParamsKeysType = SearchParamsKeys;
-
-const enabledOptions = createListCollection({
-  items: [
-    { label: "All", value: "all" },
-    { label: "Enabled", value: "false" },
-    { label: "Disabled", value: "true" },
-  ],
-});
-
-export const DagsFilters = () => {
-  const [searchParams, setSearchParams] = useSearchParams();
-
-  const showPaused = searchParams.get(PAUSED_PARAM);
-  const state = searchParams.get(LAST_DAG_RUN_STATE_PARAM);
-  const selectedTags = searchParams.getAll(TAGS_PARAM);
-  const tagFilterMode = searchParams.get(TAGS_MATCH_MODE_PARAM) ?? "any";
-  const isAll = state === null;
-  const isRunning = state === "running";
-  const isFailed = state === "failed";
-  const isSuccess = state === "success";
-
-  const { data } = useDagServiceGetDagTags({
-    orderBy: "name",
-  });
-
-  const hidePausedDagsByDefault = 
Boolean(useConfig("hide_paused_dags_by_default"));
-  const defaultShowPaused = hidePausedDagsByDefault ? "false" : "all";
-
-  const { setTableURLState, tableURLState } = useTableURLState();
-  const { pagination, sorting } = tableURLState;
-
-  const handlePausedChange = useCallback(
-    ({ value }: SelectValueChangeDetails<string>) => {
-      const [val] = value;
-
-      if (val === undefined || val === "all") {
-        searchParams.delete(PAUSED_PARAM);
-      } else {
-        searchParams.set(PAUSED_PARAM, val);
-      }
-      setTableURLState({
-        pagination: { ...pagination, pageIndex: 0 },
-        sorting,
-      });
-      setSearchParams(searchParams);
-    },
-    [pagination, searchParams, setSearchParams, setTableURLState, sorting],
-  );
-
-  const handleStateChange: React.MouseEventHandler<HTMLButtonElement> = 
useCallback(
-    ({ currentTarget: { value } }) => {
-      if (value === "all") {
-        searchParams.delete(LAST_DAG_RUN_STATE_PARAM);
-      } else {
-        searchParams.set(LAST_DAG_RUN_STATE_PARAM, value);
-      }
-      setTableURLState({
-        pagination: { ...pagination, pageIndex: 0 },
-        sorting,
-      });
-      setSearchParams(searchParams);
-    },
-    [pagination, searchParams, setSearchParams, setTableURLState, sorting],
-  );
-  const handleSelectTagsChange = useCallback(
-    (
-      tags: MultiValue<{
-        label: string;
-        value: string;
-      }>,
-    ) => {
-      searchParams.delete(TAGS_PARAM);
-      tags.forEach(({ value }) => {
-        searchParams.append(TAGS_PARAM, value);
-      });
-      if (tags.length < 2) {
-        searchParams.delete(TAGS_MATCH_MODE_PARAM);
-      }
-      setSearchParams(searchParams);
-    },
-    [searchParams, setSearchParams],
-  );
-
-  const onClearFilters = () => {
-    searchParams.delete(PAUSED_PARAM);
-    searchParams.delete(LAST_DAG_RUN_STATE_PARAM);
-    searchParams.delete(TAGS_PARAM);
-    searchParams.delete(TAGS_MATCH_MODE_PARAM);
-
-    setSearchParams(searchParams);
-  };
-
-  let filterCount = 0;
-
-  if (state !== null) {
-    filterCount += 1;
-  }
-  if (showPaused !== null) {
-    filterCount += 1;
-  }
-  if (selectedTags.length > 0) {
-    filterCount += 1;
-  }
-
-  const handleTagModeChange = useCallback(
-    ({ checked }: { checked: boolean }) => {
-      const mode = checked ? "all" : "any";
-
-      searchParams.set(TAGS_MATCH_MODE_PARAM, mode);
-      setSearchParams(searchParams);
-    },
-    [searchParams, setSearchParams],
-  );
-
-  return (
-    <HStack justifyContent="space-between">
-      <HStack gap={4}>
-        <HStack>
-          <QuickFilterButton isActive={isAll} onClick={handleStateChange} 
value="all">
-            All
-          </QuickFilterButton>
-          <QuickFilterButton
-            data-testid="dags-failed-filter"
-            isActive={isFailed}
-            onClick={handleStateChange}
-            value="failed"
-          >
-            <StateBadge state="failed" />
-            Failed
-          </QuickFilterButton>
-          <QuickFilterButton
-            data-testid="dags-running-filter"
-            isActive={isRunning}
-            onClick={handleStateChange}
-            value="running"
-          >
-            <StateBadge state="running" />
-            Running
-          </QuickFilterButton>
-          <QuickFilterButton
-            data-testid="dags-success-filter"
-            isActive={isSuccess}
-            onClick={handleStateChange}
-            value="success"
-          >
-            <StateBadge state="success" />
-            Success
-          </QuickFilterButton>
-        </HStack>
-        <Select.Root
-          collection={enabledOptions}
-          onValueChange={handlePausedChange}
-          value={[showPaused ?? defaultShowPaused]}
-        >
-          <Select.Trigger colorPalette="blue" isActive={Boolean(showPaused)}>
-            <Select.ValueText width={20} />
-          </Select.Trigger>
-          <Select.Content>
-            {enabledOptions.items.map((option) => (
-              <Select.Item item={option} key={option.label}>
-                {option.label}
-              </Select.Item>
-            ))}
-          </Select.Content>
-        </Select.Root>
-        <Field.Root>
-          <ReactSelect
-            aria-label="Filter Dags by tag"
-            chakraStyles={{
-              clearIndicator: (provided) => ({
-                ...provided,
-                color: "gray.fg",
-              }),
-              container: (provided) => ({
-                ...provided,
-                minWidth: 64,
-              }),
-              control: (provided) => ({
-                ...provided,
-                colorPalette: "blue",
-              }),
-              menu: (provided) => ({
-                ...provided,
-                zIndex: 2,
-              }),
-            }}
-            isClearable
-            isMulti
-            noOptionsMessage={() => "No tags found"}
-            onChange={handleSelectTagsChange}
-            options={
-              data?.tags.map((tag) => ({
-                label: tag,
-                value: tag,
-              })) ?? []
-            }
-            placeholder="Filter by tag"
-            value={selectedTags.map((tag) => ({
-              label: tag,
-              value: tag,
-            }))}
-          />
-        </Field.Root>
-        {selectedTags.length >= 2 && (
-          <HStack align="center" gap={1}>
-            <Text
-              color={tagFilterMode === "any" ? "fg.info" : "fg.muted"}
-              fontSize="sm"
-              fontWeight={tagFilterMode === "any" ? "bold" : "normal"}
-            >
-              Any
-            </Text>
-            <Switch
-              checked={tagFilterMode === "all"}
-              onCheckedChange={handleTagModeChange}
-              variant="raised"
-            />
-            <Text
-              color={tagFilterMode === "all" ? "fg.info" : "fg.muted"}
-              fontSize="sm"
-              fontWeight={tagFilterMode === "all" ? "bold" : "normal"}
-            >
-              All
-            </Text>
-          </HStack>
-        )}
-      </HStack>
-      <Box>
-        {filterCount > 0 && (
-          <Button onClick={onClearFilters} size="sm" variant="outline">
-            <LuX /> Reset {pluralize("filter", filterCount)}
-          </Button>
-        )}
-      </Box>
-    </HStack>
-  );
-};
diff --git 
a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx
new file mode 100644
index 00000000000..d36e2b2f8ef
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx
@@ -0,0 +1,181 @@
+/*!
+ * 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 { Box, HStack } from "@chakra-ui/react";
+import type { MultiValue } from "chakra-react-select";
+import { useCallback } from "react";
+import { useSearchParams } from "react-router-dom";
+
+import { useDagServiceGetDagTags } from "openapi/queries";
+import { useTableURLState } from "src/components/DataTable/useTableUrlState";
+import { SearchParamsKeys, type SearchParamsKeysType } from 
"src/constants/searchParams";
+import { useConfig } from "src/queries/useConfig";
+
+import { PausedFilter } from "./PausedFilter";
+import { ResetButton } from "./ResetButton";
+import { StateFilters } from "./StateFilters";
+import { TagFilter } from "./TagFilter";
+
+const {
+  LAST_DAG_RUN_STATE: LAST_DAG_RUN_STATE_PARAM,
+  PAUSED: PAUSED_PARAM,
+  TAGS: TAGS_PARAM,
+  TAGS_MATCH_MODE: TAGS_MATCH_MODE_PARAM,
+}: SearchParamsKeysType = SearchParamsKeys;
+
+const getFilterCount = (state: string | null, showPaused: string | null, 
selectedTags: Array<string>) => {
+  let count = 0;
+
+  if (state !== null) {
+    count += 1;
+  }
+  if (showPaused !== null) {
+    count += 1;
+  }
+  if (selectedTags.length > 0) {
+    count += 1;
+  }
+
+  return count;
+};
+
+export const DagsFilters = () => {
+  const [searchParams, setSearchParams] = useSearchParams();
+
+  const showPaused = searchParams.get(PAUSED_PARAM);
+  const state = searchParams.get(LAST_DAG_RUN_STATE_PARAM);
+  const selectedTags = searchParams.getAll(TAGS_PARAM);
+  const tagFilterMode = searchParams.get(TAGS_MATCH_MODE_PARAM) ?? "any";
+  const isAll = state === null;
+  const isRunning = state === "running";
+  const isFailed = state === "failed";
+  const isSuccess = state === "success";
+
+  const { data } = useDagServiceGetDagTags({
+    orderBy: "name",
+  });
+
+  const hidePausedDagsByDefault = 
Boolean(useConfig("hide_paused_dags_by_default"));
+  const defaultShowPaused = hidePausedDagsByDefault ? "false" : "all";
+
+  const { setTableURLState, tableURLState } = useTableURLState();
+  const { pagination, sorting } = tableURLState;
+
+  const handlePausedChange = useCallback(
+    ({ value }: { value: Array<string> }) => {
+      const [val] = value;
+
+      if (val === undefined || val === "all") {
+        searchParams.delete(PAUSED_PARAM);
+      } else {
+        searchParams.set(PAUSED_PARAM, val);
+      }
+      setTableURLState({
+        pagination: { ...pagination, pageIndex: 0 },
+        sorting,
+      });
+      setSearchParams(searchParams);
+    },
+    [pagination, searchParams, setSearchParams, setTableURLState, sorting],
+  );
+
+  const handleStateChange: React.MouseEventHandler<HTMLButtonElement> = 
useCallback(
+    ({ currentTarget: { value } }) => {
+      if (value === "all") {
+        searchParams.delete(LAST_DAG_RUN_STATE_PARAM);
+      } else {
+        searchParams.set(LAST_DAG_RUN_STATE_PARAM, value);
+      }
+      setTableURLState({
+        pagination: { ...pagination, pageIndex: 0 },
+        sorting,
+      });
+      setSearchParams(searchParams);
+    },
+    [pagination, searchParams, setSearchParams, setTableURLState, sorting],
+  );
+
+  const handleSelectTagsChange = useCallback(
+    (
+      tags: MultiValue<{
+        label: string;
+        value: string;
+      }>,
+    ) => {
+      searchParams.delete(TAGS_PARAM);
+      tags.forEach(({ value }) => {
+        searchParams.append(TAGS_PARAM, value);
+      });
+      if (tags.length < 2) {
+        searchParams.delete(TAGS_MATCH_MODE_PARAM);
+      }
+      setSearchParams(searchParams);
+    },
+    [searchParams, setSearchParams],
+  );
+
+  const onClearFilters = () => {
+    searchParams.delete(PAUSED_PARAM);
+    searchParams.delete(LAST_DAG_RUN_STATE_PARAM);
+    searchParams.delete(TAGS_PARAM);
+    searchParams.delete(TAGS_MATCH_MODE_PARAM);
+
+    setSearchParams(searchParams);
+  };
+
+  const handleTagModeChange = useCallback(
+    ({ checked }: { checked: boolean }) => {
+      const mode = checked ? "all" : "any";
+
+      searchParams.set(TAGS_MATCH_MODE_PARAM, mode);
+      setSearchParams(searchParams);
+    },
+    [searchParams, setSearchParams],
+  );
+
+  const filterCount = getFilterCount(state, showPaused, selectedTags);
+
+  return (
+    <HStack justifyContent="space-between">
+      <HStack gap={4}>
+        <StateFilters
+          isAll={isAll}
+          isFailed={isFailed}
+          isRunning={isRunning}
+          isSuccess={isSuccess}
+          onStateChange={handleStateChange}
+        />
+        <PausedFilter
+          defaultShowPaused={defaultShowPaused}
+          onPausedChange={handlePausedChange}
+          showPaused={showPaused}
+        />
+        <TagFilter
+          onSelectTagsChange={handleSelectTagsChange}
+          onTagModeChange={handleTagModeChange}
+          selectedTags={selectedTags}
+          tagFilterMode={tagFilterMode}
+          tags={data?.tags ?? []}
+        />
+      </HStack>
+      <Box>
+        <ResetButton filterCount={filterCount} onClearFilters={onClearFilters} 
/>
+      </Box>
+    </HStack>
+  );
+};
diff --git 
a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/PausedFilter.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/PausedFilter.tsx
new file mode 100644
index 00000000000..61f467630b4
--- /dev/null
+++ 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/PausedFilter.tsx
@@ -0,0 +1,59 @@
+/*!
+ * 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 { createListCollection, type SelectValueChangeDetails } from 
"@chakra-ui/react";
+import { useTranslation } from "react-i18next";
+
+import { Select } from "src/components/ui";
+
+type Props = {
+  readonly defaultShowPaused: string;
+  readonly onPausedChange: (details: SelectValueChangeDetails<string>) => void;
+  readonly showPaused: string | null;
+};
+
+export const PausedFilter = ({ defaultShowPaused, onPausedChange, showPaused 
}: Props) => {
+  const { t: translate } = useTranslation("dags");
+
+  const enabledOptions = createListCollection({
+    items: [
+      { label: translate("filters.paused.all"), value: "all" },
+      { label: translate("filters.paused.active"), value: "false" },
+      { label: translate("filters.paused.paused"), value: "true" },
+    ],
+  });
+
+  return (
+    <Select.Root
+      collection={enabledOptions}
+      onValueChange={onPausedChange}
+      value={[showPaused ?? defaultShowPaused]}
+    >
+      <Select.Trigger colorPalette="blue" isActive={Boolean(showPaused)}>
+        <Select.ValueText width={20} />
+      </Select.Trigger>
+      <Select.Content>
+        {enabledOptions.items.map((option) => (
+          <Select.Item item={option} key={option.label}>
+            {option.label}
+          </Select.Item>
+        ))}
+      </Select.Content>
+    </Select.Root>
+  );
+};
diff --git a/airflow-core/src/airflow/ui/src/layouts/DagsLayout.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/ResetButton.tsx
similarity index 55%
copy from airflow-core/src/airflow/ui/src/layouts/DagsLayout.tsx
copy to 
airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/ResetButton.tsx
index 331fd77e68f..32905fc9fcf 100644
--- a/airflow-core/src/airflow/ui/src/layouts/DagsLayout.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/ResetButton.tsx
@@ -16,20 +16,26 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Box } from "@chakra-ui/react";
-import type { PropsWithChildren } from "react";
+import { Button } from "@chakra-ui/react";
+import { useTranslation } from "react-i18next";
+import { LuX } from "react-icons/lu";
 
-import { NavTabs } from "./Details/NavTabs";
+type Props = {
+  readonly filterCount: number;
+  readonly onClearFilters: () => void;
+};
 
-const tabs = [
-  { label: "Dags", value: "/dags" },
-  { label: "Runs", value: "/dag_runs" },
-  { label: "Task Instances", value: "/task_instances" },
-];
+export const ResetButton = ({ filterCount, onClearFilters }: Props) => {
+  const { t: translate } = useTranslation("common");
 
-export const DagsLayout = ({ children }: PropsWithChildren) => (
-  <Box>
-    <NavTabs tabs={tabs} />
-    {children}
-  </Box>
-);
+  if (filterCount === 0) {
+    return undefined;
+  }
+
+  return (
+    <Button onClick={onClearFilters} size="sm" variant="outline">
+      <LuX /> {translate("table.filters.reset")}{" "}
+      {filterCount === 1 ? translate("table.filters.filter_one") : 
translate("table.filters.filter_other")}
+    </Button>
+  );
+};
diff --git 
a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/StateFilters.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/StateFilters.tsx
new file mode 100644
index 00000000000..0ec17d809b4
--- /dev/null
+++ 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/StateFilters.tsx
@@ -0,0 +1,70 @@
+/*!
+ * 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 { HStack } from "@chakra-ui/react";
+import { useTranslation } from "react-i18next";
+
+import { QuickFilterButton } from "src/components/QuickFilterButton";
+import { StateBadge } from "src/components/StateBadge";
+
+type Props = {
+  readonly isAll: boolean;
+  readonly isFailed: boolean;
+  readonly isRunning: boolean;
+  readonly isSuccess: boolean;
+  readonly onStateChange: React.MouseEventHandler<HTMLButtonElement>;
+};
+
+export const StateFilters = ({ isAll, isFailed, isRunning, isSuccess, 
onStateChange }: Props) => {
+  const { t: translate } = useTranslation(["dags", "common"]);
+
+  return (
+    <HStack>
+      <QuickFilterButton isActive={isAll} onClick={onStateChange} value="all">
+        {translate("filters.paused.all")}
+      </QuickFilterButton>
+      <QuickFilterButton
+        data-testid="dags-failed-filter"
+        isActive={isFailed}
+        onClick={onStateChange}
+        value="failed"
+      >
+        <StateBadge state="failed" />
+        {translate("common:states.failed")}
+      </QuickFilterButton>
+      <QuickFilterButton
+        data-testid="dags-running-filter"
+        isActive={isRunning}
+        onClick={onStateChange}
+        value="running"
+      >
+        <StateBadge state="running" />
+        {translate("common:states.running")}
+      </QuickFilterButton>
+      <QuickFilterButton
+        data-testid="dags-success-filter"
+        isActive={isSuccess}
+        onClick={onStateChange}
+        value="success"
+      >
+        <StateBadge state="success" />
+        {translate("common:states.success")}
+      </QuickFilterButton>
+    </HStack>
+  );
+};
diff --git 
a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TagFilter.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TagFilter.tsx
new file mode 100644
index 00000000000..57fa2b1089d
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TagFilter.tsx
@@ -0,0 +1,101 @@
+/*!
+ * 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 { Field, HStack, Text } from "@chakra-ui/react";
+import { Select as ReactSelect, type MultiValue } from "chakra-react-select";
+import { useTranslation } from "react-i18next";
+
+import { Switch } from "src/components/ui";
+
+type Props = {
+  readonly onSelectTagsChange: (tags: MultiValue<{ label: string; value: 
string }>) => void;
+  readonly onTagModeChange: ({ checked }: { checked: boolean }) => void;
+  readonly selectedTags: Array<string>;
+  readonly tagFilterMode: string;
+  readonly tags: Array<string>;
+};
+
+export const TagFilter = ({
+  onSelectTagsChange,
+  onTagModeChange,
+  selectedTags,
+  tagFilterMode,
+  tags,
+}: Props) => {
+  const { t: translate } = useTranslation("common");
+
+  return (
+    <>
+      <Field.Root>
+        <ReactSelect
+          aria-label={translate("table.filterByTag")}
+          chakraStyles={{
+            clearIndicator: (provided) => ({
+              ...provided,
+              color: "gray.fg",
+            }),
+            container: (provided) => ({
+              ...provided,
+              minWidth: 64,
+            }),
+            control: (provided) => ({
+              ...provided,
+              colorPalette: "blue",
+            }),
+            menu: (provided) => ({
+              ...provided,
+              zIndex: 2,
+            }),
+          }}
+          isClearable
+          isMulti
+          noOptionsMessage={() => translate("table.noTagsFound")}
+          onChange={onSelectTagsChange}
+          options={tags.map((tag) => ({
+            label: tag,
+            value: tag,
+          }))}
+          placeholder={translate("table.tagPlaceholder")}
+          value={selectedTags.map((tag) => ({
+            label: tag,
+            value: tag,
+          }))}
+        />
+      </Field.Root>
+      {selectedTags.length >= 2 && (
+        <HStack align="center" gap={1}>
+          <Text
+            color={tagFilterMode === "any" ? "fg.info" : "fg.muted"}
+            fontSize="sm"
+            fontWeight={tagFilterMode === "any" ? "bold" : "normal"}
+          >
+            {translate("table.tagMode.any")}
+          </Text>
+          <Switch checked={tagFilterMode === "all"} 
onCheckedChange={onTagModeChange} variant="raised" />
+          <Text
+            color={tagFilterMode === "all" ? "fg.info" : "fg.muted"}
+            fontSize="sm"
+            fontWeight={tagFilterMode === "all" ? "bold" : "normal"}
+          >
+            {translate("table.tagMode.all")}
+          </Text>
+        </HStack>
+      )}
+    </>
+  );
+};
diff --git a/airflow-core/src/airflow/ui/src/layouts/DagsLayout.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/index.ts
similarity index 66%
copy from airflow-core/src/airflow/ui/src/layouts/DagsLayout.tsx
copy to airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/index.ts
index 331fd77e68f..6c88a1f6a1d 100644
--- a/airflow-core/src/airflow/ui/src/layouts/DagsLayout.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/index.ts
@@ -16,20 +16,5 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Box } from "@chakra-ui/react";
-import type { PropsWithChildren } from "react";
 
-import { NavTabs } from "./Details/NavTabs";
-
-const tabs = [
-  { label: "Dags", value: "/dags" },
-  { label: "Runs", value: "/dag_runs" },
-  { label: "Task Instances", value: "/task_instances" },
-];
-
-export const DagsLayout = ({ children }: PropsWithChildren) => (
-  <Box>
-    <NavTabs tabs={tabs} />
-    {children}
-  </Box>
-);
+export { DagsFilters } from "./DagsFilters";
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 60b45fbaca9..e06098d4dce 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
@@ -26,7 +26,8 @@ import {
   Box,
 } from "@chakra-ui/react";
 import type { ColumnDef } from "@tanstack/react-table";
-import { useCallback, useState } from "react";
+import { useCallback, useState, useMemo } from "react";
+import { useTranslation } from "react-i18next";
 import { Link as RouterLink, useSearchParams } from "react-router-dom";
 import { useLocalStorage } from "usehooks-ts";
 
@@ -45,7 +46,6 @@ import { SearchParamsKeys, type SearchParamsKeysType } from 
"src/constants/searc
 import { DagsLayout } from "src/layouts/DagsLayout";
 import { useConfig } from "src/queries/useConfig";
 import { useDags } from "src/queries/useDags";
-import { pluralize } from "src/utils";
 
 import { DAGImportErrors } from "../Dashboard/Stats/DAGImportErrors";
 import { DagCard } from "./DagCard";
@@ -54,7 +54,9 @@ import { DagsFilters } from "./DagsFilters";
 import { Schedule } from "./Schedule";
 import { SortSelect } from "./SortSelect";
 
-const columns: Array<ColumnDef<DAGWithLatestDagRunsResponse>> = [
+const createColumns = (
+  translate: (key: string, options?: Record<string, unknown>) => string,
+): Array<ColumnDef<DAGWithLatestDagRunsResponse>> => [
   {
     accessorKey: "is_paused",
     cell: ({ row: { original } }) => (
@@ -77,13 +79,13 @@ const columns: 
Array<ColumnDef<DAGWithLatestDagRunsResponse>> = [
         <RouterLink 
to={`/dags/${original.dag_id}`}>{original.dag_display_name}</RouterLink>
       </Link>
     ),
-    header: "Dag",
+    header: () => translate("list.columns.dagId"),
   },
   {
     accessorKey: "timetable_description",
     cell: ({ row: { original } }) => <Schedule dag={original} />,
     enableSorting: false,
-    header: () => "Schedule",
+    header: () => translate("list.columns.schedule"),
   },
   {
     accessorKey: "next_dagrun",
@@ -94,7 +96,7 @@ const columns: Array<ColumnDef<DAGWithLatestDagRunsResponse>> 
= [
           runAfter={original.next_dagrun_run_after as string}
         />
       ) : undefined,
-    header: "Next Dag Run",
+    header: () => translate("list.columns.nextDagRun"),
   },
   {
     accessorKey: "last_run_start_date",
@@ -112,7 +114,7 @@ const columns: 
Array<ColumnDef<DAGWithLatestDagRunsResponse>> = [
           </RouterLink>
         </Link>
       ) : undefined,
-    header: "Last Dag Run",
+    header: () => translate("list.columns.lastDagRun"),
   },
   {
     accessorKey: "tags",
@@ -122,22 +124,18 @@ const columns: 
Array<ColumnDef<DAGWithLatestDagRunsResponse>> = [
       },
     }) => <DagTags hideIcon tags={tags} />,
     enableSorting: false,
-    header: () => "Tags",
+    header: () => translate("list.columns.tags"),
   },
   {
     accessorKey: "trigger",
-    cell: ({ row }) => <TriggerDAGButton dag={row.original} withText={false} 
/>,
+    cell: ({ row: { original } }) => <TriggerDAGButton dag={original} 
withText={false} />,
     enableSorting: false,
     header: "",
   },
   {
     accessorKey: "delete",
-    cell: ({ row }) => (
-      <DeleteDagButton
-        dagDisplayName={row.original.dag_display_name}
-        dagId={row.original.dag_id}
-        withText={false}
-      />
+    cell: ({ row: { original } }) => (
+      <DeleteDagButton dagDisplayName={original.dag_display_name} 
dagId={original.dag_id} withText={false} />
     ),
     enableSorting: false,
     header: "",
@@ -162,6 +160,7 @@ const cardDef: CardDef<DAGWithLatestDagRunsResponse> = {
 const DAGS_LIST_DISPLAY = "dags_list_display";
 
 export const DagsList = () => {
+  const { t: translate } = useTranslation(["dags", "common"]);
   const [searchParams, setSearchParams] = useSearchParams();
   const [display, setDisplay] = useLocalStorage<"card" | 
"table">(DAGS_LIST_DISPLAY, "card");
   const dagRunsLimit = display === "card" ? 14 : 1;
@@ -185,6 +184,8 @@ export const DagsList = () => {
   const [sort] = sorting;
   const orderBy = sort ? `${sort.desc ? "-" : ""}${sort.id}` : 
"dag_display_name";
 
+  const columns = useMemo(() => createColumns(translate), [translate]);
+
   const handleSearchChange = (value: string) => {
     if (value) {
       searchParams.set(NAME_PATTERN_PARAM, value);
@@ -241,13 +242,13 @@ export const DagsList = () => {
           buttonProps={{ disabled: true }}
           defaultValue={dagDisplayNamePattern ?? ""}
           onChange={handleSearchChange}
-          placeHolder="Search Dags"
+          placeHolder={translate("list.searchPlaceholder")}
         />
         <DagsFilters />
         <HStack justifyContent="space-between">
           <HStack>
             <Heading py={3} size="md">
-              {pluralize("Dag", data.total_entries)}
+              {`${data.total_entries} ${data.total_entries === 1 ? 
translate("common:dag_one") : translate("common:dag_other")}`}
             </Heading>
             <DAGImportErrors iconOnly />
           </HStack>
@@ -266,7 +267,7 @@ export const DagsList = () => {
           errorMessage={<ErrorAlert error={error} />}
           initialState={tableURLState}
           isLoading={isLoading}
-          modelName="Dag"
+          modelName={translate("common:dag_one")}
           onStateChange={setTableURLState}
           skeletonCount={display === "card" ? 5 : undefined}
           total={data.total_entries}
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/RecentRuns.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/RecentRuns.tsx
index 5e7def01650..af6c5c1a146 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/RecentRuns.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/RecentRuns.tsx
@@ -19,6 +19,7 @@
 import { Flex, Box, Text } from "@chakra-ui/react";
 import dayjs from "dayjs";
 import duration from "dayjs/plugin/duration";
+import { useTranslation } from "react-i18next";
 import { Link } from "react-router-dom";
 
 import type { DAGWithLatestDagRunsResponse } from "openapi/requests/types.gen";
@@ -35,6 +36,8 @@ export const RecentRuns = ({
 }: {
   readonly latestRuns: DAGWithLatestDagRunsResponse["latest_dag_runs"];
 }) => {
+  const { t: translate } = useTranslation(["dags", "common"]);
+
   if (!latestRuns.length) {
     return undefined;
   }
@@ -55,21 +58,25 @@ export const RecentRuns = ({
         <Tooltip
           content={
             <Box>
-              <Text>State: {run.state}</Text>
               <Text>
-                Run After: <Time datetime={run.run_after} />
+                {translate("list.runs.state")}: 
{translate(`common:states.${run.state}`)}
+              </Text>
+              <Text>
+                {translate("list.runs.runAfter")}: <Time 
datetime={run.run_after} />
               </Text>
               {run.start_date === null ? undefined : (
                 <Text>
-                  Start Date: <Time datetime={run.start_date} />
+                  {translate("list.runs.startDate")}: <Time 
datetime={run.start_date} />
                 </Text>
               )}
               {run.end_date === null ? undefined : (
                 <Text>
-                  End Date: <Time datetime={run.end_date} />
+                  {translate("list.runs.endDate")}: <Time 
datetime={run.end_date} />
                 </Text>
               )}
-              <Text>Duration: {getDuration(run.start_date, 
run.end_date)}</Text>
+              <Text>
+                {translate("list.runs.duration")}: 
{getDuration(run.start_date, run.end_date)}
+              </Text>
             </Box>
           }
           key={run.dag_run_id}
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/Schedule.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/Schedule.tsx
index 9f071cfb437..0810fa743fb 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/Schedule.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/Schedule.tsx
@@ -17,6 +17,7 @@
  * under the License.
  */
 import { Text } from "@chakra-ui/react";
+import { useTranslation } from "react-i18next";
 import { FiCalendar } from "react-icons/fi";
 
 import type { DAGWithLatestDagRunsResponse } from "openapi/requests/types.gen";
@@ -28,9 +29,11 @@ type Props = {
   readonly dag: DAGWithLatestDagRunsResponse;
 };
 
-export const Schedule = ({ dag }: Props) =>
-  Boolean(dag.timetable_summary) ? (
-    Boolean(dag.asset_expression) || dag.timetable_summary === "Asset" ? (
+export const Schedule = ({ dag }: Props) => {
+  const { t: translate } = useTranslation("dags");
+
+  return Boolean(dag.timetable_summary) ? (
+    Boolean(dag.asset_expression) || dag.timetable_summary === 
translate("schedule.asset") ? (
       <AssetSchedule dag={dag} />
     ) : (
       <Tooltip content={dag.timetable_description}>
@@ -40,3 +43,4 @@ export const Schedule = ({ dag }: Props) =>
       </Tooltip>
     )
   ) : undefined;
+};
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/SortSelect.tsx 
b/airflow-core/src/airflow/ui/src/pages/DagsList/SortSelect.tsx
index 256bc862de3..e0e66b9033f 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/SortSelect.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/SortSelect.tsx
@@ -17,32 +17,38 @@
  * under the License.
  */
 import type { SelectValueChangeDetails } from "@chakra-ui/react";
+import { useTranslation } from "react-i18next";
 
 import { Select } from "src/components/ui";
-import { dagSortOptions } from "src/constants/sortParams";
+import { createDagSortOptions } from "src/constants/sortParams";
 
 type Props = {
   readonly handleSortChange: ({ value }: 
SelectValueChangeDetails<Array<string>>) => void;
   readonly orderBy?: string;
 };
 
-export const SortSelect = ({ handleSortChange, orderBy }: Props) => (
-  <Select.Root
-    collection={dagSortOptions}
-    data-testid="sort-by-select"
-    onValueChange={handleSortChange}
-    value={orderBy === undefined ? undefined : [orderBy]}
-    width="310px"
-  >
-    <Select.Trigger>
-      <Select.ValueText placeholder="Sort by" />
-    </Select.Trigger>
-    <Select.Content>
-      {dagSortOptions.items.map((option) => (
-        <Select.Item item={option} key={option.value}>
-          {option.label}
-        </Select.Item>
-      ))}
-    </Select.Content>
-  </Select.Root>
-);
+export const SortSelect = ({ handleSortChange, orderBy }: Props) => {
+  const { t: translate } = useTranslation("dags");
+  const dagSortOptions = createDagSortOptions(translate);
+
+  return (
+    <Select.Root
+      collection={dagSortOptions}
+      data-testid="sort-by-select"
+      onValueChange={handleSortChange}
+      value={orderBy === undefined ? undefined : [orderBy]}
+      width="310px"
+    >
+      <Select.Trigger>
+        <Select.ValueText placeholder={translate("sort.placeholder")} />
+      </Select.Trigger>
+      <Select.Content>
+        {dagSortOptions.items.map((option) => (
+          <Select.Item item={option} key={option.value}>
+            {option.label}
+          </Select.Item>
+        ))}
+      </Select.Content>
+    </Select.Root>
+  );
+};

Reply via email to