From cffbd1ada406658d82a53029efd98f9536d69892 Mon Sep 17 00:00:00 2001 From: Coki Date: Wed, 3 Jul 2024 11:52:52 +0800 Subject: [PATCH 1/4] feat: add support for multiple languages --- app/components/LanguageMenu.tsx | 81 ++++++++ app/components/SidePanelChat.tsx | 4 +- app/components/editor/index.tsx | 34 ++-- app/context/LangContext.tsx | 38 ++++ app/globals.css | 4 + app/layout.tsx | 5 +- messages/de.json | 14 ++ messages/en.json | 14 ++ messages/fr.json | 14 ++ messages/ja.json | 14 ++ messages/zh-Hant.json | 14 ++ messages/zh.json | 15 ++ package.json | 1 + public/LanguageSwitching.svg | 1 + yarn.lock | 311 ++++++++++++++++++++++++++++++- 15 files changed, 547 insertions(+), 17 deletions(-) create mode 100644 app/components/LanguageMenu.tsx create mode 100644 app/context/LangContext.tsx create mode 100644 messages/de.json create mode 100644 messages/en.json create mode 100644 messages/fr.json create mode 100644 messages/ja.json create mode 100644 messages/zh-Hant.json create mode 100644 messages/zh.json create mode 100644 public/LanguageSwitching.svg diff --git a/app/components/LanguageMenu.tsx b/app/components/LanguageMenu.tsx new file mode 100644 index 0000000..cd8f50c --- /dev/null +++ b/app/components/LanguageMenu.tsx @@ -0,0 +1,81 @@ +'use client'; +import React, { useEffect, useState } from 'react'; +import clsx from 'clsx'; +import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; +import { useLang } from '@/app/context/LangContext'; + +const LanguageMenu = () => { + const { lang, setLang } = useLang(); + const [mounted, setMounted] = useState(false); + + const languageNames = { + en: 'English', + zh: '简体中文', + hant: '繁體中文', + ja: '日本語', + fr: 'Français', + de: 'Deutsch', + }; + + const handleLangChange = (newLang) => { + setLang(newLang); + }; + + const currentLanguage = languageNames[lang] || 'Unknown'; + + useEffect(() => { + setMounted(true); + }, []); + + if (!mounted) { + return null; + } + + return ( + + + + + + {Object.keys(languageNames).map((code) => { + return ( + { + handleLangChange(code); + }} + className="p-2 cursor-pointer" + > + {languageNames[code]} + + ); + })} + + + ); +}; + +export default LanguageMenu; diff --git a/app/components/SidePanelChat.tsx b/app/components/SidePanelChat.tsx index 2b75a95..46d82e0 100644 --- a/app/components/SidePanelChat.tsx +++ b/app/components/SidePanelChat.tsx @@ -1,11 +1,13 @@ import React, { useState, useEffect, forwardRef, useImperativeHandle } from 'react'; import { extractPageContent } from '../utils/contentExtractor'; +import { useLang } from '@/app/context/LangContext'; const SidePanelChat = forwardRef((props, ref) => { const [isOpen, setIsOpen] = useState(false); const [message, setMessage] = useState(''); const [pageContent, setPageContent] = useState(''); const [boxType, setBoxType] = useState(''); + const { t } = useLang(); const toggleDrawer = () => { setIsOpen(!isOpen); @@ -33,7 +35,7 @@ const SidePanelChat = forwardRef((props, ref) => { return ( <>
- Why this result? + {t('Why this result')}
{isOpen &&
}
{ const { @@ -48,6 +50,7 @@ export const EditorScreen = () => { const { message } = extractPageContent(boxType); return message; }; + const { t } = useLang(); useEffect(() => { const fetchCasbinVersion = async () => { @@ -112,7 +115,7 @@ export const EditorScreen = () => { -
{open &&
Custom config
}
+
{open &&
{t('Custom config')}
}
{open && (
@@ -139,7 +142,7 @@ export const EditorScreen = () => {
-
Model
+
{t('Model')}
{
-
Enforcement Result
+
{t('Enforcement Result')}
@@ -340,7 +343,7 @@ export const EditorScreen = () => {
-
+
{!share ? ( @@ -427,7 +430,7 @@ export const EditorScreen = () => { }); }} > - SHARE + {t('SHARE')} ) : ( @@ -446,16 +449,19 @@ export const EditorScreen = () => { return copy( () => { setShare(''); - setEcho(
Copied.
); + setEcho(
{t('Copied')}
); }, `${window.location.origin + window.location.pathname}#${share}`, ); }} > - COPY + {t('COPY')} )} -
{echo}
+
{echo}
+
+ +
diff --git a/app/context/LangContext.tsx b/app/context/LangContext.tsx new file mode 100644 index 0000000..e4f1f23 --- /dev/null +++ b/app/context/LangContext.tsx @@ -0,0 +1,38 @@ +'use client'; +import { createContext, useContext, useState, ReactNode } from 'react'; + +const translations = { + en: require('../../messages/en.json'), + zh: require('../../messages/zh.json'), + hant: require('../../messages/zh-Hant.json'), + ja: require('../../messages/ja.json'), + fr: require('../../messages/fr.json'), + de: require('../../messages/de.json'), +}; + +type LangContextType = { + lang: string; + setLang: (lang: string) => void; + t: (key: string) => string; +}; + +const LangContext = createContext(undefined); + +export const LangProvider = ({ children }: { children: ReactNode }) => { + const [lang, setLang] = useState('en'); + + const t = (key: string) => { + const value = translations[lang][key]; + return value || key; + }; + + return {children}; +}; + +export const useLang = () => { + const context = useContext(LangContext); + if (!context) { + throw new Error('useLang must be used within a LangProvider'); + } + return context; +}; diff --git a/app/globals.css b/app/globals.css index b5c61c9..2ec87a4 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,3 +1,7 @@ @tailwind base; @tailwind components; @tailwind utilities; + +button:hover img { + filter: brightness(0) invert(1); +} diff --git a/app/layout.tsx b/app/layout.tsx index 7022896..dc33995 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -14,6 +14,7 @@ import type { Metadata } from 'next'; import './globals.css'; +import { LangProvider } from './context/LangContext'; export const metadata: Metadata = { title: 'casbin-editor', @@ -27,7 +28,9 @@ export default function RootLayout({ }>) { return ( - {children} + + {children} + ); } diff --git a/messages/de.json b/messages/de.json new file mode 100644 index 0000000..f7d5177 --- /dev/null +++ b/messages/de.json @@ -0,0 +1,14 @@ +{ + "Custom config": "Benutzerdefinierte Konfiguration", + "Model": "Modell", + "Select your model": "Wählen Sie Ihr Modell", + "RESET": "ZURÜCKSETZEN", + "Policy": "Strategie", + "Request": "Anfrage", + "Enforcement Result": "Durchsetzungsergebnis", + "Why this result": "Warum dieses Ergebnis?", + "SYNTAX VALIDATE": "SYNTAX VALIDIEREN", + "RUN THE TEST": "DEN TEST AUSFÜHREN", + "SHARE": "TEILEN", + "COPY": "KOPIEREN" +} diff --git a/messages/en.json b/messages/en.json new file mode 100644 index 0000000..4c630b1 --- /dev/null +++ b/messages/en.json @@ -0,0 +1,14 @@ +{ + "Custom config": "Custom config", + "Model": "Model", + "Select your model": "Select your model", + "RESET": "RESET", + "Policy": "Policy", + "Request": "Request", + "Enforcement Result": "Enforcement Result", + "Why this result": "Why this result?", + "SYNTAX VALIDATE": "SYNTAX VALIDATE", + "RUN THE TEST": "RUN THE TEST", + "SHARE": "SHARE", + "COPY": "COPY" +} diff --git a/messages/fr.json b/messages/fr.json new file mode 100644 index 0000000..e907888 --- /dev/null +++ b/messages/fr.json @@ -0,0 +1,14 @@ +{ + "Custom config": "Configuration personnalisée", + "Model": "Modèle", + "Select your model": "Sélectionnez votre modèle", + "RESET": "RÉINITIALISER", + "Policy": "Stratégie", + "Request": "Demande", + "Enforcement Result": "Résultat de l'exécution", + "Why this result": "Pourquoi ce résultat?", + "SYNTAX VALIDATE": "VALIDER LA SYNTAXE", + "RUN THE TEST": "EXÉCUTER LE TEST", + "SHARE": "PARTAGER", + "COPY": "COPIER" +} diff --git a/messages/ja.json b/messages/ja.json new file mode 100644 index 0000000..f1e0eb7 --- /dev/null +++ b/messages/ja.json @@ -0,0 +1,14 @@ +{ + "Custom config": "カスタム設定", + "Model": "モデル", + "Select your model": "モデルを選択", + "RESET": "リセット", + "Policy": "戦略", + "Request": "リクエスト", + "Enforcement Result": "実行結果", + "Why this result": "なぜこの結果?", + "SYNTAX VALIDATE": "構文検証", + "RUN THE TEST": "テストを実行", + "SHARE": "共有", + "COPY": "コピー" +} diff --git a/messages/zh-Hant.json b/messages/zh-Hant.json new file mode 100644 index 0000000..3c9b4dd --- /dev/null +++ b/messages/zh-Hant.json @@ -0,0 +1,14 @@ +{ + "Custom config": "自訂設定", + "Model": "模型", + "Select your model": "選擇您的模型", + "RESET": "重置", + "Policy": "策略", + "Request": "請求", + "Enforcement Result": "執行結果", + "Why this result": "為什麼這個結果?", + "SYNTAX VALIDATE": "語法驗證", + "RUN THE TEST": "運行測試", + "SHARE": "分享", + "COPY": "複製" +} diff --git a/messages/zh.json b/messages/zh.json new file mode 100644 index 0000000..91d8c3a --- /dev/null +++ b/messages/zh.json @@ -0,0 +1,15 @@ +{ + "Custom config": "自定义配置", + "Model": "模型", + "Select your model": "选择模型", + "RESET": "重置", + "Policy": "策略", + "Request": "请求", + "Enforcement Result": "执行结果", + "Why this result": "为何此结果?", + "SYNTAX VALIDATE": "验证语法", + "RUN THE TEST": "运行", + "SHARE": "分享", + "Copied": "已复制", + "COPY": "复制" +} diff --git a/package.json b/package.json index 4b32c4a..e2fe628 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "@codemirror/state": "^6.4.1", "@codemirror/view": "^6.24.1", "@lezer/highlight": "^1.2.0", + "@radix-ui/react-dropdown-menu": "^2.1.1", "@uiw/codemirror-theme-monokai": "^4.21.22", "@uiw/react-codemirror": "^4.21.22", "casbin": "^5.30.0", diff --git a/public/LanguageSwitching.svg b/public/LanguageSwitching.svg new file mode 100644 index 0000000..73ba151 --- /dev/null +++ b/public/LanguageSwitching.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 2b59270..bcad127 100644 --- a/yarn.lock +++ b/yarn.lock @@ -211,6 +211,33 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== +"@floating-ui/core@^1.6.0": + version "1.6.4" + resolved "https://registry.npmmirror.com/@floating-ui/core/-/core-1.6.4.tgz#0140cf5091c8dee602bff9da5ab330840ff91df6" + integrity sha512-a4IowK4QkXl4SCWTGUR0INAfEOX3wtsYw3rKK5InQEHMGObkR8Xk44qYQD9P4r6HHw0iIfK6GUKECmY8sTkqRA== + dependencies: + "@floating-ui/utils" "^0.2.4" + +"@floating-ui/dom@^1.0.0": + version "1.6.7" + resolved "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.6.7.tgz#85d22f731fcc5b209db504478fb1df5116a83015" + integrity sha512-wmVfPG5o2xnKDU4jx/m4w5qva9FWHcnZ8BvzEe90D/RpwsJaTAVYPEPdQ8sbr/N8zZTAHlZUTQdqg8ZUbzHmng== + dependencies: + "@floating-ui/core" "^1.6.0" + "@floating-ui/utils" "^0.2.4" + +"@floating-ui/react-dom@^2.0.0": + version "2.1.1" + resolved "https://registry.npmmirror.com/@floating-ui/react-dom/-/react-dom-2.1.1.tgz#cca58b6b04fc92b4c39288252e285e0422291fb0" + integrity sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg== + dependencies: + "@floating-ui/dom" "^1.0.0" + +"@floating-ui/utils@^0.2.4": + version "0.2.4" + resolved "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.4.tgz#1d459cee5031893a08a0e064c406ad2130cced7c" + integrity sha512-dWO2pw8hhi+WrXq1YJy2yCuWoL20PddgGaqTgVe4cOS9Q6qklXCiA1tJEqX6BEwRNSCP84/afac9hd4MS+zEUA== + "@humanwhocodes/config-array@^0.11.14": version "0.11.14" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" @@ -402,6 +429,216 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== +"@radix-ui/primitive@1.1.0": + version "1.1.0" + resolved "https://registry.npmmirror.com/@radix-ui/primitive/-/primitive-1.1.0.tgz#42ef83b3b56dccad5d703ae8c42919a68798bbe2" + integrity sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA== + +"@radix-ui/react-arrow@1.1.0": + version "1.1.0" + resolved "https://registry.npmmirror.com/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz#744f388182d360b86285217e43b6c63633f39e7a" + integrity sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw== + dependencies: + "@radix-ui/react-primitive" "2.0.0" + +"@radix-ui/react-collection@1.1.0": + version "1.1.0" + resolved "https://registry.npmmirror.com/@radix-ui/react-collection/-/react-collection-1.1.0.tgz#f18af78e46454a2360d103c2251773028b7724ed" + integrity sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw== + dependencies: + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-slot" "1.1.0" + +"@radix-ui/react-compose-refs@1.1.0": + version "1.1.0" + resolved "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz#656432461fc8283d7b591dcf0d79152fae9ecc74" + integrity sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw== + +"@radix-ui/react-context@1.1.0": + version "1.1.0" + resolved "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.1.0.tgz#6df8d983546cfd1999c8512f3a8ad85a6e7fcee8" + integrity sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A== + +"@radix-ui/react-direction@1.1.0": + version "1.1.0" + resolved "https://registry.npmmirror.com/@radix-ui/react-direction/-/react-direction-1.1.0.tgz#a7d39855f4d077adc2a1922f9c353c5977a09cdc" + integrity sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg== + +"@radix-ui/react-dismissable-layer@1.1.0": + version "1.1.0" + resolved "https://registry.npmmirror.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.0.tgz#2cd0a49a732372513733754e6032d3fb7988834e" + integrity sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig== + dependencies: + "@radix-ui/primitive" "1.1.0" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-use-escape-keydown" "1.1.0" + +"@radix-ui/react-dropdown-menu@^2.1.1": + version "2.1.1" + resolved "https://registry.npmmirror.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.1.tgz#3dc578488688250dbbe109d9ff2ca28a9bca27ec" + integrity sha512-y8E+x9fBq9qvteD2Zwa4397pUVhYsh9iq44b5RD5qu1GMJWBCBuVg1hMyItbc6+zH00TxGRqd9Iot4wzf3OoBQ== + dependencies: + "@radix-ui/primitive" "1.1.0" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-menu" "2.1.1" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-use-controllable-state" "1.1.0" + +"@radix-ui/react-focus-guards@1.1.0": + version "1.1.0" + resolved "https://registry.npmmirror.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz#8e9abb472a9a394f59a1b45f3dd26cfe3fc6da13" + integrity sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw== + +"@radix-ui/react-focus-scope@1.1.0": + version "1.1.0" + resolved "https://registry.npmmirror.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz#ebe2891a298e0a33ad34daab2aad8dea31caf0b2" + integrity sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA== + dependencies: + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-use-callback-ref" "1.1.0" + +"@radix-ui/react-id@1.1.0": + version "1.1.0" + resolved "https://registry.npmmirror.com/@radix-ui/react-id/-/react-id-1.1.0.tgz#de47339656594ad722eb87f94a6b25f9cffae0ed" + integrity sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA== + dependencies: + "@radix-ui/react-use-layout-effect" "1.1.0" + +"@radix-ui/react-menu@2.1.1": + version "2.1.1" + resolved "https://registry.npmmirror.com/@radix-ui/react-menu/-/react-menu-2.1.1.tgz#bd623ace0e1ae1ac78023a505fec0541d59fb346" + integrity sha512-oa3mXRRVjHi6DZu/ghuzdylyjaMXLymx83irM7hTxutQbD+7IhPKdMdRHD26Rm+kHRrWcrUkkRPv5pd47a2xFQ== + dependencies: + "@radix-ui/primitive" "1.1.0" + "@radix-ui/react-collection" "1.1.0" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-direction" "1.1.0" + "@radix-ui/react-dismissable-layer" "1.1.0" + "@radix-ui/react-focus-guards" "1.1.0" + "@radix-ui/react-focus-scope" "1.1.0" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-popper" "1.2.0" + "@radix-ui/react-portal" "1.1.1" + "@radix-ui/react-presence" "1.1.0" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-roving-focus" "1.1.0" + "@radix-ui/react-slot" "1.1.0" + "@radix-ui/react-use-callback-ref" "1.1.0" + aria-hidden "^1.1.1" + react-remove-scroll "2.5.7" + +"@radix-ui/react-popper@1.2.0": + version "1.2.0" + resolved "https://registry.npmmirror.com/@radix-ui/react-popper/-/react-popper-1.2.0.tgz#a3e500193d144fe2d8f5d5e60e393d64111f2a7a" + integrity sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg== + dependencies: + "@floating-ui/react-dom" "^2.0.0" + "@radix-ui/react-arrow" "1.1.0" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-use-layout-effect" "1.1.0" + "@radix-ui/react-use-rect" "1.1.0" + "@radix-ui/react-use-size" "1.1.0" + "@radix-ui/rect" "1.1.0" + +"@radix-ui/react-portal@1.1.1": + version "1.1.1" + resolved "https://registry.npmmirror.com/@radix-ui/react-portal/-/react-portal-1.1.1.tgz#1957f1eb2e1aedfb4a5475bd6867d67b50b1d15f" + integrity sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g== + dependencies: + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-use-layout-effect" "1.1.0" + +"@radix-ui/react-presence@1.1.0": + version "1.1.0" + resolved "https://registry.npmmirror.com/@radix-ui/react-presence/-/react-presence-1.1.0.tgz#227d84d20ca6bfe7da97104b1a8b48a833bfb478" + integrity sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ== + dependencies: + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-use-layout-effect" "1.1.0" + +"@radix-ui/react-primitive@2.0.0": + version "2.0.0" + resolved "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz#fe05715faa9203a223ccc0be15dc44b9f9822884" + integrity sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw== + dependencies: + "@radix-ui/react-slot" "1.1.0" + +"@radix-ui/react-roving-focus@1.1.0": + version "1.1.0" + resolved "https://registry.npmmirror.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz#b30c59daf7e714c748805bfe11c76f96caaac35e" + integrity sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA== + dependencies: + "@radix-ui/primitive" "1.1.0" + "@radix-ui/react-collection" "1.1.0" + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-context" "1.1.0" + "@radix-ui/react-direction" "1.1.0" + "@radix-ui/react-id" "1.1.0" + "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/react-use-controllable-state" "1.1.0" + +"@radix-ui/react-slot@1.1.0": + version "1.1.0" + resolved "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.1.0.tgz#7c5e48c36ef5496d97b08f1357bb26ed7c714b84" + integrity sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw== + dependencies: + "@radix-ui/react-compose-refs" "1.1.0" + +"@radix-ui/react-use-callback-ref@1.1.0": + version "1.1.0" + resolved "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz#bce938ca413675bc937944b0d01ef6f4a6dc5bf1" + integrity sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw== + +"@radix-ui/react-use-controllable-state@1.1.0": + version "1.1.0" + resolved "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz#1321446857bb786917df54c0d4d084877aab04b0" + integrity sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw== + dependencies: + "@radix-ui/react-use-callback-ref" "1.1.0" + +"@radix-ui/react-use-escape-keydown@1.1.0": + version "1.1.0" + resolved "https://registry.npmmirror.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz#31a5b87c3b726504b74e05dac1edce7437b98754" + integrity sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw== + dependencies: + "@radix-ui/react-use-callback-ref" "1.1.0" + +"@radix-ui/react-use-layout-effect@1.1.0": + version "1.1.0" + resolved "https://registry.npmmirror.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz#3c2c8ce04827b26a39e442ff4888d9212268bd27" + integrity sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w== + +"@radix-ui/react-use-rect@1.1.0": + version "1.1.0" + resolved "https://registry.npmmirror.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz#13b25b913bd3e3987cc9b073a1a164bb1cf47b88" + integrity sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ== + dependencies: + "@radix-ui/rect" "1.1.0" + +"@radix-ui/react-use-size@1.1.0": + version "1.1.0" + resolved "https://registry.npmmirror.com/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz#b4dba7fbd3882ee09e8d2a44a3eed3a7e555246b" + integrity sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw== + dependencies: + "@radix-ui/react-use-layout-effect" "1.1.0" + +"@radix-ui/rect@1.1.0": + version "1.1.0" + resolved "https://registry.npmmirror.com/@radix-ui/rect/-/rect-1.1.0.tgz#f817d1d3265ac5415dadc67edab30ae196696438" + integrity sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg== + "@rushstack/eslint-patch@^1.3.3": version "1.10.3" resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.3.tgz#391d528054f758f81e53210f1a1eebcf1a8b1d20" @@ -743,6 +980,13 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +aria-hidden@^1.1.1: + version "1.2.4" + resolved "https://registry.npmmirror.com/aria-hidden/-/aria-hidden-1.2.4.tgz#b78e383fdbc04d05762c78b4a25a501e736c4522" + integrity sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A== + dependencies: + tslib "^2.0.0" + aria-query@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" @@ -1376,6 +1620,11 @@ dequal@^2.0.3: resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== +detect-node-es@^1.1.0: + version "1.1.0" + resolved "https://registry.npmmirror.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493" + integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ== + detect-node@^2.0.4: version "2.1.0" resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" @@ -2104,6 +2353,11 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@ has-symbols "^1.0.3" hasown "^2.0.0" +get-nonce@^1.0.0: + version "1.0.1" + resolved "https://registry.npmmirror.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" + integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q== + get-stream@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" @@ -2386,6 +2640,13 @@ internal-slot@^1.0.7: hasown "^2.0.0" side-channel "^1.0.4" +invariant@^2.2.4: + version "2.2.4" + resolved "https://registry.npmmirror.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== + dependencies: + loose-envify "^1.0.0" + is-array-buffer@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" @@ -2778,7 +3039,7 @@ lodash@^4.17.15: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -loose-envify@^1.1.0, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -3319,6 +3580,34 @@ react-is@^16.13.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-remove-scroll-bar@^2.3.4: + version "2.3.6" + resolved "https://registry.npmmirror.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz#3e585e9d163be84a010180b18721e851ac81a29c" + integrity sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g== + dependencies: + react-style-singleton "^2.2.1" + tslib "^2.0.0" + +react-remove-scroll@2.5.7: + version "2.5.7" + resolved "https://registry.npmmirror.com/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz#15a1fd038e8497f65a695bf26a4a57970cac1ccb" + integrity sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA== + dependencies: + react-remove-scroll-bar "^2.3.4" + react-style-singleton "^2.2.1" + tslib "^2.1.0" + use-callback-ref "^1.3.0" + use-sidecar "^1.1.2" + +react-style-singleton@^2.2.1: + version "2.2.1" + resolved "https://registry.npmmirror.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4" + integrity sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g== + dependencies: + get-nonce "^1.0.0" + invariant "^2.2.4" + tslib "^2.0.0" + react@^18: version "18.3.1" resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" @@ -3900,6 +4189,11 @@ tsconfig-paths@^3.15.0: minimist "^1.2.6" strip-bom "^3.0.0" +tslib@^2.0.0, tslib@^2.1.0: + version "2.6.3" + resolved "https://registry.npmmirror.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== + tslib@^2.4.0: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" @@ -4011,6 +4305,21 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +use-callback-ref@^1.3.0: + version "1.3.2" + resolved "https://registry.npmmirror.com/use-callback-ref/-/use-callback-ref-1.3.2.tgz#6134c7f6ff76e2be0b56c809b17a650c942b1693" + integrity sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA== + dependencies: + tslib "^2.0.0" + +use-sidecar@^1.1.2: + version "1.1.2" + resolved "https://registry.npmmirror.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2" + integrity sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw== + dependencies: + detect-node-es "^1.1.0" + tslib "^2.0.0" + utf8-byte-length@^1.0.1: version "1.0.5" resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.5.tgz#f9f63910d15536ee2b2d5dd4665389715eac5c1e" From 6f58c8211153348d5673996346606e2ebd0c43bb Mon Sep 17 00:00:00 2001 From: Coki Date: Wed, 3 Jul 2024 12:54:19 +0800 Subject: [PATCH 2/4] feat: persist language preference --- app/context/LangContext.tsx | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/app/context/LangContext.tsx b/app/context/LangContext.tsx index e4f1f23..907f534 100644 --- a/app/context/LangContext.tsx +++ b/app/context/LangContext.tsx @@ -1,5 +1,5 @@ 'use client'; -import { createContext, useContext, useState, ReactNode } from 'react'; +import { createContext, useContext, useState, useEffect, ReactNode } from 'react'; const translations = { en: require('../../messages/en.json'), @@ -19,13 +19,29 @@ type LangContextType = { const LangContext = createContext(undefined); export const LangProvider = ({ children }: { children: ReactNode }) => { - const [lang, setLang] = useState('en'); + const [lang, setLangState] = useState('en'); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const savedLang = localStorage.getItem('lang') || 'en'; + setLangState(savedLang); + setIsLoading(false); + }, []); + + const setLang = (newLang: string) => { + setLangState(newLang); + localStorage.setItem('lang', newLang); + }; const t = (key: string) => { const value = translations[lang][key]; return value || key; }; + if (isLoading) { + return null; + } + return {children}; }; From 12f814e8af11198af04d0fae240ccb366493759d Mon Sep 17 00:00:00 2001 From: Coki Date: Wed, 3 Jul 2024 17:00:15 +0800 Subject: [PATCH 3/4] feat: add browser language detection and set current page language --- app/context/LangContext.tsx | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/app/context/LangContext.tsx b/app/context/LangContext.tsx index 907f534..3953509 100644 --- a/app/context/LangContext.tsx +++ b/app/context/LangContext.tsx @@ -4,7 +4,7 @@ import { createContext, useContext, useState, useEffect, ReactNode } from 'react const translations = { en: require('../../messages/en.json'), zh: require('../../messages/zh.json'), - hant: require('../../messages/zh-Hant.json'), + zhHant: require('../../messages/zh-Hant.json'), ja: require('../../messages/ja.json'), fr: require('../../messages/fr.json'), de: require('../../messages/de.json'), @@ -18,13 +18,31 @@ type LangContextType = { const LangContext = createContext(undefined); +const langMapping = { + 'zh-Hant': 'zhHant', +}; + +const getTranslationKey = (lang) => { + return langMapping[lang] || lang; +}; + export const LangProvider = ({ children }: { children: ReactNode }) => { const [lang, setLangState] = useState('en'); const [isLoading, setIsLoading] = useState(true); useEffect(() => { - const savedLang = localStorage.getItem('lang') || 'en'; - setLangState(savedLang); + localStorage.clear(); + const savedLang = localStorage.getItem('lang'); + + if (savedLang) { + setLangState(savedLang); + } else { + const browserLang = navigator.language.split('-')[0]; + const supportedLangs = ['en', 'zh', 'zh-Hant', 'ja', 'fr', 'de']; + const defaultLang = supportedLangs.includes(browserLang) ? browserLang : 'en'; + setLangState(defaultLang); + localStorage.setItem('lang', defaultLang); + } setIsLoading(false); }, []); @@ -34,7 +52,8 @@ export const LangProvider = ({ children }: { children: ReactNode }) => { }; const t = (key: string) => { - const value = translations[lang][key]; + const langKey = getTranslationKey(lang); + const value = translations[langKey][key]; return value || key; }; From bbcc9f566c5d556804148a860e4646d2de2172f9 Mon Sep 17 00:00:00 2001 From: Coki Date: Wed, 3 Jul 2024 17:05:34 +0800 Subject: [PATCH 4/4] remove localStorage.clear() used for testing --- app/context/LangContext.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/context/LangContext.tsx b/app/context/LangContext.tsx index 3953509..d2624b6 100644 --- a/app/context/LangContext.tsx +++ b/app/context/LangContext.tsx @@ -31,7 +31,6 @@ export const LangProvider = ({ children }: { children: ReactNode }) => { const [isLoading, setIsLoading] = useState(true); useEffect(() => { - localStorage.clear(); const savedLang = localStorage.getItem('lang'); if (savedLang) {