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

weilee 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 cfb81fbe882 Add LanguageModal for lang selection (#51369)
cfb81fbe882 is described below

commit cfb81fbe882d31dc99ea345d3204d3c6927419dc
Author: Guan Ming(Wesley) Chiu <[email protected]>
AuthorDate: Fri Jun 6 09:13:20 2025 +0800

    Add LanguageModal for lang selection (#51369)
    
    * feat: add LanguageModal
    
    * feat: add 'direction' translation to en and zh-TW
---
 .../src/airflow/ui/src/i18n/locales/en/common.json |  1 +
 .../airflow/ui/src/i18n/locales/zh-TW/common.json  |  1 +
 .../airflow/ui/src/layouts/Nav/LanguageModal.tsx   | 47 +++++++++++++++
 .../ui/src/layouts/Nav/LanguageSelector.tsx        | 66 ++++++++++++++++++++++
 .../ui/src/layouts/Nav/UserSettingsButton.tsx      | 32 +++--------
 5 files changed, 124 insertions(+), 23 deletions(-)

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 06cc0db232f..80272396994 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
@@ -19,6 +19,7 @@
   "dagRun_other": "Dag Runs",
   "defaultToGraphView": "Default to graph view",
   "defaultToGridView": "Default to grid view",
+  "direction": "Direction",
   "docs": {
     "documentation": "Documentation",
     "githubRepo": "GitHub Repo",
diff --git a/airflow-core/src/airflow/ui/src/i18n/locales/zh-TW/common.json 
b/airflow-core/src/airflow/ui/src/i18n/locales/zh-TW/common.json
index ca0a6c5a548..9ab309d94fe 100644
--- a/airflow-core/src/airflow/ui/src/i18n/locales/zh-TW/common.json
+++ b/airflow-core/src/airflow/ui/src/i18n/locales/zh-TW/common.json
@@ -19,6 +19,7 @@
   "dagRun_other": "Dag 執行",
   "defaultToGraphView": "預設使用圖形視圖",
   "defaultToGridView": "預設使用網格視圖",
+  "direction": "書寫方向",
   "docs": {
     "documentation": "文件",
     "githubRepo": "GitHub 倉庫",
diff --git a/airflow-core/src/airflow/ui/src/layouts/Nav/LanguageModal.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Nav/LanguageModal.tsx
new file mode 100644
index 00000000000..7853734aba4
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/layouts/Nav/LanguageModal.tsx
@@ -0,0 +1,47 @@
+/*!
+ * 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 React from "react";
+import { useTranslation } from "react-i18next";
+
+import { Dialog } from "src/components/ui";
+
+import LanguageSelector from "./LanguageSelector";
+
+type LanguageModalProps = {
+  readonly isOpen: boolean;
+  readonly onClose: () => void;
+};
+
+const LanguageModal: React.FC<LanguageModalProps> = ({ isOpen, onClose }) => {
+  const { t: translate } = useTranslation();
+
+  return (
+    <Dialog.Root lazyMount onOpenChange={onClose} open={isOpen} size="xl">
+      <Dialog.Content backdrop>
+        <Dialog.Header>{translate("selectLanguage")}</Dialog.Header>
+        <Dialog.CloseTrigger />
+        <Dialog.Body>
+          <LanguageSelector />
+        </Dialog.Body>
+      </Dialog.Content>
+    </Dialog.Root>
+  );
+};
+
+export default LanguageModal;
diff --git a/airflow-core/src/airflow/ui/src/layouts/Nav/LanguageSelector.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Nav/LanguageSelector.tsx
new file mode 100644
index 00000000000..c309aeba0c0
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/layouts/Nav/LanguageSelector.tsx
@@ -0,0 +1,66 @@
+/*!
+ * 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, VStack, Box, Text } from "@chakra-ui/react";
+import { Select, type SingleValue } from "chakra-react-select";
+import React, { useMemo } from "react";
+import { useTranslation } from "react-i18next";
+
+import { supportedLanguages } from "src/i18n/config";
+
+const LanguageSelector: React.FC = () => {
+  const { i18n, t: translate } = useTranslation();
+  const options = useMemo(
+    () =>
+      supportedLanguages.map((lang) => ({
+        label: lang.name,
+        value: lang.code,
+      })),
+    [],
+  );
+
+  const handleLanguageChange = (selectedOption: SingleValue<{ label: string; 
value: string }>) => {
+    if (selectedOption) {
+      void i18n.changeLanguage(selectedOption.value);
+    }
+  };
+
+  const currentLang = options.find((option) => option.value === i18n.language);
+  const langDir = i18n.dir(i18n.language);
+
+  return (
+    <VStack align="stretch" gap={6}>
+      <Field.Root>
+        <Select<{ label: string; value: string }>
+          onChange={handleLanguageChange}
+          options={options}
+          placeholder={translate("selectLanguage")}
+          value={currentLang}
+        />
+      </Field.Root>
+      <Box borderRadius="md" boxShadow="sm" display="flex" 
flexDirection="column" gap={2} p={6}>
+        <Text fontSize="lg" fontWeight="bold">
+          {currentLang?.label} ({i18n.language})
+        </Text>
+        <Text>{`${translate("direction")}: ${langDir.toUpperCase()}`}</Text>
+      </Box>
+    </VStack>
+  );
+};
+
+export default LanguageSelector;
diff --git a/airflow-core/src/airflow/ui/src/layouts/Nav/UserSettingsButton.tsx 
b/airflow-core/src/airflow/ui/src/layouts/Nav/UserSettingsButton.tsx
index 476c315e917..4eca5ea77d8 100644
--- a/airflow-core/src/airflow/ui/src/layouts/Nav/UserSettingsButton.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/Nav/UserSettingsButton.tsx
@@ -22,15 +22,15 @@ import timezone from "dayjs/plugin/timezone";
 import utc from "dayjs/plugin/utc";
 import { useState } from "react";
 import { useTranslation } from "react-i18next";
-import { FiClock, FiGrid, FiLogOut, FiMoon, FiSun, FiUser, FiGlobe, 
FiChevronRight } from "react-icons/fi";
+import { FiClock, FiGrid, FiLogOut, FiMoon, FiSun, FiUser, FiGlobe } from 
"react-icons/fi";
 import { MdOutlineAccountTree } from "react-icons/md";
 import { useLocalStorage } from "usehooks-ts";
 
 import { Menu } from "src/components/ui";
 import { useColorMode } from "src/context/colorMode/useColorMode";
 import { useTimezone } from "src/context/timezone";
-import { supportedLanguages } from "src/i18n/config";
 
+import LanguageModal from "./LanguageModal";
 import LogoutModal from "./LogoutModal";
 import { NavButton } from "./NavButton";
 import TimezoneModal from "./TimezoneModal";
@@ -39,10 +39,11 @@ dayjs.extend(utc);
 dayjs.extend(timezone);
 
 export const UserSettingsButton = () => {
-  const { i18n, t: translate } = useTranslation();
+  const { t: translate } = useTranslation();
   const { colorMode, toggleColorMode } = useColorMode();
   const { onClose: onCloseTimezone, onOpen: onOpenTimezone, open: 
isOpenTimezone } = useDisclosure();
   const { onClose: onCloseLogout, onOpen: onOpenLogout, open: isOpenLogout } = 
useDisclosure();
+  const { onClose: onCloseLanguage, onOpen: onOpenLanguage, open: 
isOpenLanguage } = useDisclosure();
   const { selectedTimezone } = useTimezone();
   const [dagView, setDagView] = useLocalStorage<"graph" | 
"grid">("default_dag_view", "grid");
 
@@ -54,26 +55,10 @@ export const UserSettingsButton = () => {
         <NavButton icon={<FiUser size="1.75rem" />} title={translate("user")} 
/>
       </Menu.Trigger>
       <Menu.Content>
-        <Menu.Root>
-          <Menu.TriggerItem>
-            <FiGlobe size="1.25rem" style={{ marginRight: "8px" }} />
-            {translate("selectLanguage")}
-            <FiChevronRight size="1.25rem" style={{ marginLeft: "auto" }} />
-          </Menu.TriggerItem>
-          <Menu.Content>
-            <Menu.RadioItemGroup
-              onValueChange={(element) => void 
i18n.changeLanguage(element.value)}
-              value={i18n.language}
-            >
-              {supportedLanguages.map((lang) => (
-                <Menu.RadioItem key={lang.code} value={lang.code}>
-                  {lang.name}
-                  <Menu.ItemIndicator />
-                </Menu.RadioItem>
-              ))}
-            </Menu.RadioItemGroup>
-          </Menu.Content>
-        </Menu.Root>
+        <Menu.Item onClick={onOpenLanguage} value="language">
+          <FiGlobe size="1.25rem" style={{ marginRight: "8px" }} />
+          {translate("selectLanguage")}
+        </Menu.Item>
         <Menu.Item onClick={toggleColorMode} value="color-mode">
           {colorMode === "light" ? (
             <>
@@ -112,6 +97,7 @@ export const UserSettingsButton = () => {
           {translate("logout")}
         </Menu.Item>
       </Menu.Content>
+      <LanguageModal isOpen={isOpenLanguage} onClose={onCloseLanguage} />
       <TimezoneModal isOpen={isOpenTimezone} onClose={onCloseTimezone} />
       <LogoutModal isOpen={isOpenLogout} onClose={onCloseLogout} />
     </Menu.Root>

Reply via email to