From b87e441435967f0f32a9dcb628c64dc68522320c Mon Sep 17 00:00:00 2001 From: IZUMI-Zu <274620705z@gmail.com> Date: Tue, 26 Nov 2024 21:04:45 +0800 Subject: [PATCH] feat: add i18n support (#34) --- App.js | 1 + CasdoorLoginPage.js | 28 +++-- EditAccountDetails.js | 6 +- EnterAccountDetails.js | 35 +++--- EnterCasdoorSdkConfig.js | 20 ++-- Header.js | 13 +-- HomePage.js | 20 ++-- ImportManager.js | 7 +- Language.js | 228 +++++++++++++++++++++++++++++++++++++++ LoginMethodSelector.js | 11 +- MSAuthImportLogic.js | 9 +- NavigationBar.js | 7 +- QRScanner.js | 8 +- ScanLogin.js | 13 +-- SearchBar.js | 4 +- SettingPage.js | 121 ++++++++++++++++----- api.js | 6 +- app.json | 3 +- i18n.js | 44 ++++++++ locales/ar/data.json | 98 +++++++++++++++++ locales/de/data.json | 98 +++++++++++++++++ locales/en/data.json | 97 +++++++++++++++++ locales/es/data.json | 98 +++++++++++++++++ locales/fr/data.json | 98 +++++++++++++++++ locales/ja/data.json | 97 +++++++++++++++++ locales/ko/data.json | 98 +++++++++++++++++ locales/pt/data.json | 98 +++++++++++++++++ locales/ru/data.json | 98 +++++++++++++++++ locales/th/data.json | 98 +++++++++++++++++ locales/uk/data.json | 98 +++++++++++++++++ locales/zh/data.json | 97 +++++++++++++++++ package-lock.json | 94 ++++++++++++++++ package.json | 8 +- syncLogic.js | 5 +- 34 files changed, 1761 insertions(+), 103 deletions(-) create mode 100644 Language.js create mode 100644 i18n.js create mode 100644 locales/ar/data.json create mode 100644 locales/de/data.json create mode 100644 locales/en/data.json create mode 100644 locales/es/data.json create mode 100644 locales/fr/data.json create mode 100644 locales/ja/data.json create mode 100644 locales/ko/data.json create mode 100644 locales/pt/data.json create mode 100644 locales/ru/data.json create mode 100644 locales/th/data.json create mode 100644 locales/uk/data.json create mode 100644 locales/zh/data.json diff --git a/App.js b/App.js index 2961ed2..1c4ed43 100644 --- a/App.js +++ b/App.js @@ -24,6 +24,7 @@ import {GestureHandlerRootView} from "react-native-gesture-handler"; import {useMigrations} from "drizzle-orm/expo-sqlite/migrator"; import {ActionSheetProvider} from "@expo/react-native-action-sheet"; +import "./i18n"; import Header from "./Header"; import NavigationBar from "./NavigationBar"; import {db} from "./db/client"; diff --git a/CasdoorLoginPage.js b/CasdoorLoginPage.js index 16029e9..a8f2179 100644 --- a/CasdoorLoginPage.js +++ b/CasdoorLoginPage.js @@ -23,6 +23,7 @@ import EnterCasdoorSdkConfig from "./EnterCasdoorSdkConfig"; import ScanQRCodeForLogin from "./ScanLogin"; import useStore from "./useStorage"; import DefaultCasdoorSdkConfig from "./DefaultCasdoorSdkConfig"; +import {useTranslation} from "react-i18next"; let sdk = null; @@ -33,6 +34,7 @@ function CasdoorLoginPage({onWebviewClose, initialMethod}) { }; const {notify} = useNotifications(); + const {t} = useTranslation(); const [casdoorLoginURL, setCasdoorLoginURL] = useState(""); const [currentView, setCurrentView] = useState(initialMethod === "scan" ? "scanner" : "config"); @@ -113,11 +115,21 @@ function CasdoorLoginPage({onWebviewClose, initialMethod}) { const userInfo = sdk.JwtDecode(accessToken); setToken(accessToken); setUserInfo(userInfo); - notify("success", {params: {title: "Success", description: "Logged in successfully!"}}); + notify("success", { + params: { + title: t("common.success"), + description: t("casdoorLoginPage.Logged in successfully!"), + }, + }); setCurrentView("config"); onWebviewClose(); } catch (error) { - notify("error", {params: {title: "Error in login", description: error.message}}); + notify("error", { + params: { + title: t("common.error"), + description: error.message, + }, + }); } }; @@ -139,20 +151,25 @@ function CasdoorLoginPage({onWebviewClose, initialMethod}) { }} onLogin={handleQRLogin} onError={(message) => { - notify("error", {params: {title: "Error", description: message}}); + notify("error", {params: {title: t("common.error"), description: message}}); }} /> ), webview: casdoorLoginURL && !token && ( setCurrentView("config")}> - Back to Config + {t("casdoorLoginPage.Back to Config")} { - notify("error", {params: {title: "Error", description: nativeEvent.description}}); + notify("error", { + params: { + title: t("common.error"), + description: nativeEvent.description, + }, + }); setCurrentView("config"); }} style={styles.webview} @@ -186,7 +203,6 @@ const styles = StyleSheet.create({ flex: 1, backgroundColor: "white", paddingTop: Platform.OS === "android" ? StatusBar.currentHeight : 0, - }, }); diff --git a/EditAccountDetails.js b/EditAccountDetails.js index a74cc12..ad2253b 100644 --- a/EditAccountDetails.js +++ b/EditAccountDetails.js @@ -16,6 +16,7 @@ import React, {useState} from "react"; import {Text, TextInput, View} from "react-native"; import {Button, IconButton} from "react-native-paper"; import PropTypes from "prop-types"; +import {useTranslation} from "react-i18next"; export default function EnterAccountDetails({onClose, onEdit, placeholder}) { EnterAccountDetails.propTypes = { @@ -24,6 +25,7 @@ export default function EnterAccountDetails({onClose, onEdit, placeholder}) { placeholder: PropTypes.string.isRequired, }; + const {t} = useTranslation(); const [accountName, setAccountName] = useState(""); const handleConfirm = () => { @@ -32,7 +34,7 @@ export default function EnterAccountDetails({onClose, onEdit, placeholder}) { return ( - Enter new account name + {t("editAccount.Enter new account name")} - Confirm + {t("common.confirm")} diff --git a/EnterAccountDetails.js b/EnterAccountDetails.js index 5d088e9..f7d32d1 100644 --- a/EnterAccountDetails.js +++ b/EnterAccountDetails.js @@ -17,6 +17,7 @@ import {View} from "react-native"; import {Button, IconButton, Menu, Text, TextInput} from "react-native-paper"; import {useNotifications} from "react-native-notificated"; import PropTypes from "prop-types"; +import {useTranslation} from "react-i18next"; const EnterAccountDetails = ({onClose, onAdd, validateSecret}) => { EnterAccountDetails.propTypes = { @@ -26,7 +27,7 @@ const EnterAccountDetails = ({onClose, onAdd, validateSecret}) => { }; const {notify} = useNotifications(); - + const {t} = useTranslation(); const [accountName, setAccountName] = useState(""); const [secretKey, setSecretKey] = useState(""); const [secretError, setSecretError] = useState(""); @@ -45,25 +46,25 @@ const EnterAccountDetails = ({onClose, onAdd, validateSecret}) => { const handleAddAccount = useCallback(() => { if (accountName.trim() === "") { - setAccountNameError("Account Name is required"); + setAccountNameError(t("editAccount.Account Name is required")); } if (secretKey.trim() === "") { - setSecretError("Secret Key is required"); + setSecretError(t("editAccount.Secret Key is required")); } if (accountName.trim() === "" || secretKey.trim() === "") { notify("error", { - title: "Error", - description: "Please fill in all the fields!", + title: t("common.error"), + description: t("editAccount.Please fill in all the fields!"), }); return; } if (secretError) { notify("error", { - title: "Error", - description: "Invalid Secret Key", + title: t("common.error"), + description: t("editAccount.Invalid Secret Key"), }); return; } @@ -94,7 +95,7 @@ const EnterAccountDetails = ({onClose, onAdd, validateSecret}) => { - Add Account + {t("editAccount.Add Account")} { /> { mode="outlined" /> { contentStyle={styles.menuButtonContent} style={styles.menuButton} > - {selectedItem} + {t(`editAccount.${selectedItem}`)} } contentStyle={styles.menuContent} > - handleMenuItemPress("Time based")} title="Time based" /> - handleMenuItemPress("Counter based")} title="Counter based" /> + handleMenuItemPress("Time based")} + title={t("editAccount.Time based")} + /> + handleMenuItemPress("Counter based")} + title={t("editAccount.Counter based")} + /> diff --git a/EnterCasdoorSdkConfig.js b/EnterCasdoorSdkConfig.js index d5e1a4a..ee6ba43 100644 --- a/EnterCasdoorSdkConfig.js +++ b/EnterCasdoorSdkConfig.js @@ -18,8 +18,10 @@ import {Button, Portal, TextInput} from "react-native-paper"; import {useNotifications} from "react-native-notificated"; import PropTypes from "prop-types"; import useStore from "./useStorage"; +import {useTranslation} from "react-i18next"; function EnterCasdoorSdkConfig({onClose, onWebviewClose, usePortal = true}) { + const {t} = useTranslation(); const { serverUrl, clientId, @@ -43,8 +45,8 @@ function EnterCasdoorSdkConfig({onClose, onWebviewClose, usePortal = true}) { if (!serverUrl || !clientId || !appName || !organizationName || !redirectPath) { notify("error", { params: { - title: "Error", - description: "Please fill in all the fields!", + title: t("common.error"), + description: t("enterCasdoorSDKConfig.Please fill in all the fields!"), }, }); return; @@ -55,10 +57,10 @@ function EnterCasdoorSdkConfig({onClose, onWebviewClose, usePortal = true}) { const content = ( - Casdoor Configuration + {t("enterCasdoorSDKConfig.Casdoor Configuration")} - Cancel + {t("common.cancel")} diff --git a/Header.js b/Header.js index 9265ad0..2409c02 100644 --- a/Header.js +++ b/Header.js @@ -21,6 +21,7 @@ import CasdoorLoginPage, {CasdoorLogout} from "./CasdoorLoginPage"; import useStore from "./useStorage"; import {useAccountSync} from "./useAccountStore"; import LoginMethodSelector from "./LoginMethodSelector"; +import {useTranslation} from "react-i18next"; const {width} = Dimensions.get("window"); @@ -31,7 +32,7 @@ const Header = () => { const [menuVisible, setMenuVisible] = React.useState(false); const [loginMethod, setLoginMethod] = React.useState(null); const {notify} = useNotifications(); - + const {t} = useTranslation(); const openMenu = () => setMenuVisible(true); const closeMenu = () => setMenuVisible(false); @@ -64,8 +65,8 @@ const Header = () => { const handleSyncErrorPress = () => { notify("error", { params: { - title: "Error", - description: syncError || "An unknown error occurred during synchronization.", + title: t("common.error"), + description: syncError || t("header.An unknown error occurred during synchronization"), }, }); }; @@ -75,7 +76,7 @@ const Header = () => { - Casdoor + {t("header.Casdoor")} } style={styles.titleWrapper} @@ -119,13 +120,13 @@ const Header = () => { styles.buttonText, userInfo === null && {marginLeft: 0}, ]}> - {userInfo === null ? "Login" : userInfo.name} + {userInfo === null ? t("common.login") : userInfo.name} } > - + {showLoginPage && ( diff --git a/HomePage.js b/HomePage.js index f432b78..d5d4961 100644 --- a/HomePage.js +++ b/HomePage.js @@ -20,6 +20,7 @@ import {CountdownCircleTimer} from "react-native-countdown-circle-timer"; import {useNetInfo} from "@react-native-community/netinfo"; import {FlashList} from "@shopify/flash-list"; import {useNotifications} from "react-native-notificated"; +import {useTranslation} from "react-i18next"; import SearchBar from "./SearchBar"; import EnterAccountDetails from "./EnterAccountDetails"; @@ -57,12 +58,15 @@ export default function HomePage() { const {accounts, refreshAccounts} = useAccountStore(); const {setAccount, updateAccount, insertAccount, insertAccounts, deleteAccount} = useEditAccount(); const {notify} = useNotifications(); - + const {t} = useTranslation(); const {showImportOptions} = useImportManager((data) => { handleAddAccount(data); }, (err) => { notify("error", { - params: {title: "Import error", description: err.message}, + params: { + title: t("homepage.Import error"), + description: err.message, + }, }); }, () => { setShowScanner(true); @@ -164,7 +168,7 @@ export default function HomePage() { setShowScanner(false); notify("error", { params: { - title: "Error scanning QR code", + title: t("homepage.Error scanning QR code"), description: error, }, }); @@ -228,13 +232,13 @@ export default function HomePage() { style={{height: 70, width: 80, backgroundColor: "#E6DFF3", alignItems: "center", justifyContent: "center"}} onPress={() => handleEditAccount(item)} > - Edit + {t("common.edit")} onAccountDelete(item)} > - Delete + {t("common.delete")} )} @@ -319,21 +323,21 @@ export default function HomePage() { onPress={handleScanPress} > - Scan QR code + {t("homepage.Scan QR Code")} - Enter Secret code + {t("homepage.Enter Secret Code")} - Import from other app + {t("homepage.Import from other app")} diff --git a/ImportManager.js b/ImportManager.js index ddcffd6..141a37a 100644 --- a/ImportManager.js +++ b/ImportManager.js @@ -14,6 +14,7 @@ import {useActionSheet} from "@expo/react-native-action-sheet"; import {importFromMSAuth} from "./MSAuthImportLogic"; +import i18next from "i18next"; const importApps = [ {name: "Google Authenticator", useScanner: true}, @@ -24,14 +25,14 @@ export const useImportManager = (onImportComplete, onError, onOpenScanner) => { const {showActionSheetWithOptions} = useActionSheet(); const showImportOptions = () => { - const options = [...importApps.map(app => app.name), "Cancel"]; + const options = [...importApps.map(app => app.name), i18next.t("common.cancel")]; const cancelButtonIndex = options.length - 1; showActionSheetWithOptions( { options, cancelButtonIndex, - title: "Select app to import from", + title: i18next.t("importManager.Select app to import from"), }, (selectedIndex) => { if (selectedIndex !== cancelButtonIndex) { @@ -45,7 +46,7 @@ export const useImportManager = (onImportComplete, onError, onOpenScanner) => { }) .catch(onError); } else { - onError(new Error(`Import function not implemented for ${selectedApp.name}`)); + onError(new Error(`${i18next.t("importManager.Import function not implemented for")} ${selectedApp.name}`)); } } } diff --git a/Language.js b/Language.js new file mode 100644 index 0000000..a768bbc --- /dev/null +++ b/Language.js @@ -0,0 +1,228 @@ +// Copyright 2024 The Casdoor Authors. All Rights Reserved. +// +// Licensed 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, {useState} from "react"; +import {Modal, ScrollView, StyleSheet, TouchableOpacity, View} from "react-native"; +import {SvgUri} from "react-native-svg"; +import {Text} from "react-native-paper"; +import {useTranslation} from "react-i18next"; +import AsyncStorage from "@react-native-async-storage/async-storage"; + +import en from "./locales/en/data.json"; +import zh from "./locales/zh/data.json"; +import ja from "./locales/ja/data.json"; +import fr from "./locales/fr/data.json"; +import de from "./locales/de/data.json"; +import ko from "./locales/ko/data.json"; +import ar from "./locales/ar/data.json"; +import es from "./locales/es/data.json"; +import ru from "./locales/ru/data.json"; +import pt from "./locales/pt/data.json"; +import th from "./locales/th/data.json"; +import uk from "./locales/uk/data.json"; + +const StaticBaseUrl = "https://cdn.casbin.org"; + +const languageResources = {en, zh, ja, fr, de, ko, ar, es, ru, pt, th, uk}; + +export const languages = [ + {label: "English", key: "en", country: "US", alt: "English"}, + {label: "中文", key: "zh", country: "CN", alt: "中文"}, + {label: "日本語", key: "ja", country: "JP", alt: "Japanese"}, + {label: "Français", key: "fr", country: "FR", alt: "French"}, + {label: "Deutsch", key: "de", country: "DE", alt: "German"}, + {label: "한국어", key: "ko", country: "KR", alt: "Korean"}, + {label: "العربية", key: "ar", country: "SA", alt: "Arabic"}, + {label: "Español", key: "es", country: "ES", alt: "Spanish"}, + {label: "Русский", key: "ru", country: "RU", alt: "Russian"}, + {label: "Português", key: "pt", country: "PT", alt: "Portuguese"}, + {label: "Thai", key: "th", country: "TH", alt: "Thai"}, + {label: "Українська", key: "uk", country: "UA", alt: "Ukrainian"}, +]; + +const rtlLanguages = ["ar"]; + +export const isRTL = languageKey => { + return rtlLanguages.includes(languageKey); +}; + +export const getLanguageResources = () => { + const resources = {}; + languages.forEach(({key}) => { + resources[key] = { + translation: languageResources[key], + }; + }); + return resources; +}; + +function flagIcon(country, alt) { + return ( + + ); +} + +export function Language() { + const {i18n, t} = useTranslation(); + const [isOpen, setIsOpen] = useState(false); + const currentLanguage = i18n.language; + const currentLangDetails = languages.find(lang => lang.key === currentLanguage) || languages[0]; + const isRTLLanguage = isRTL(currentLanguage); + + const handleLanguageChange = async key => { + i18n.changeLanguage(key); + await AsyncStorage.setItem("language", key); + setIsOpen(false); + }; + + return ( + + setIsOpen(true)} + > + {flagIcon(currentLangDetails.country, currentLangDetails.alt)} + + + + setIsOpen(false)} + > + setIsOpen(false)} + > + + {t("settings.Language")} + + {languages.map(({key, country, alt, label}) => ( + {handleLanguageChange(key);}} + style={[ + styles.languageButton, + isRTLLanguage && styles.languageButtonRTL, + currentLanguage === key && styles.activeButton, + ]} + > + {flagIcon(country, alt)} + + {label} + + + ))} + + + + + + ); +} + +const styles = StyleSheet.create({ + dropdownTrigger: { + flexDirection: "row", + alignItems: "center", + paddingHorizontal: 6, + }, + dropdownTriggerRTL: { + flexDirection: "row-reverse", + }, + selectedLabel: { + fontSize: 16, + color: "#333", + marginLeft: 8, + flex: 1, + }, + dropdownIcon: { + fontSize: 12, + color: "#666", + marginLeft: 12, + }, + modalOverlay: { + flex: 1, + backgroundColor: "rgba(0, 0, 0, 0.5)", + justifyContent: "center", + padding: 20, + }, + dropdownMenu: { + backgroundColor: "#ffffff", + borderRadius: 12, + padding: 16, + maxHeight: "80%", + }, + title: { + fontSize: 20, + fontWeight: "600", + color: "#333", + marginBottom: 16, + }, + languageButton: { + flexDirection: "row", + alignItems: "center", + backgroundColor: "#f5f5f5", + padding: 16, + borderRadius: 8, + marginBottom: 8, + borderWidth: 1, + borderColor: "#eee", + }, + languageButtonRTL: { + flexDirection: "row-reverse", + }, + activeButton: { + backgroundColor: "#e7f3ff", + borderColor: "#2196f3", + }, + languageLabel: { + fontSize: 16, + color: "#333", + marginLeft: 12, + fontWeight: "500", + }, + languageLabelRTL: { + marginLeft: 0, + marginRight: 12, + }, + activeLabel: { + color: "#2196f3", + fontWeight: "600", + }, + flagIcon: { + width: 24, + height: 24, + resizeMode: "contain", + borderRadius: 4, + }, +}); diff --git a/LoginMethodSelector.js b/LoginMethodSelector.js index 4106362..77315b7 100644 --- a/LoginMethodSelector.js +++ b/LoginMethodSelector.js @@ -13,22 +13,23 @@ // limitations under the License. import {useActionSheet} from "@expo/react-native-action-sheet"; +import i18next from "i18next"; const LoginMethodSelector = ({onSelectMethod}) => { const {showActionSheetWithOptions} = useActionSheet(); const openActionSheet = () => { const options = [ - "Manual Server Setup", - "Login Using QR Code", - "Try Casdoor Demo Site", - "Cancel", + i18next.t("loginMethod.Manual Server Setup"), + i18next.t("loginMethod.Login Using QR Code"), + i18next.t("loginMethod.Try Casdoor Demo Site"), + i18next.t("common.cancel"), ]; const cancelButtonIndex = 3; showActionSheetWithOptions( { - title: "Select Login Method", + title: i18next.t("loginMethod.Select Login Method"), cancelButtonTintColor: "red", options, cancelButtonIndex, diff --git a/MSAuthImportLogic.js b/MSAuthImportLogic.js index c109a8b..56b52f8 100644 --- a/MSAuthImportLogic.js +++ b/MSAuthImportLogic.js @@ -15,6 +15,7 @@ import * as DocumentPicker from "expo-document-picker"; import * as FileSystem from "expo-file-system"; import {openDatabaseSync} from "expo-sqlite/next"; +import i18next from "i18next"; const SQLITE_DIR = `${FileSystem.documentDirectory}SQLite`; @@ -30,7 +31,7 @@ const createDirectory = async(dir) => { await FileSystem.makeDirectoryAsync(dir, {intermediates: true}); } } catch (error) { - throw new Error(`Error creating directory: ${error.message}`); + throw new Error(`${i18next.t("msAuthImport.Error creating directory")}: ${error.message}`); } }; @@ -65,19 +66,19 @@ export const importFromMSAuth = async() => { const rows = await queryMicrosoftAuthenticatorDatabase(db); if (rows.length === 0) { - throw new Error("No data found in Microsoft Authenticator database"); + throw new Error(i18next.t("msAuthImport.No data found in Microsoft Authenticator database")); } return formatMicrosoftAuthenticatorData(rows); } catch (dbError) { if (dbError.message.includes("file is not a database")) { - throw new Error("file is not a database"); + throw new Error(i18next.t("msAuthImport.file is not a database")); } throw new Error(dbError.message); } } } } catch (error) { - throw new Error(`Error importing from Microsoft Authenticator: ${error.message}`); + throw new Error(`${i18next.t("msAuthImport.Error importing from Microsoft Authenticator")}: ${error.message}`); } finally { await FileSystem.deleteAsync(internalDbName, {idempotent: true}); } diff --git a/NavigationBar.js b/NavigationBar.js index 561a885..9d236a6 100644 --- a/NavigationBar.js +++ b/NavigationBar.js @@ -20,10 +20,13 @@ import Icon from "react-native-vector-icons/MaterialCommunityIcons"; import HomePage from "./HomePage"; import {CommonActions} from "@react-navigation/native"; import SettingPage from "./SettingPage"; +import {useTranslation} from "react-i18next"; const Tab = createBottomTabNavigator(); export default function NavigationBar() { + const {t} = useTranslation(); + return ( { return ; }, @@ -86,7 +89,7 @@ export default function NavigationBar() { name="Settings" component={SettingPage} options={{ - tabBarLabel: "Settings", + tabBarLabel: t("navBar.Settings"), tabBarIcon: ({color, size}) => { return ; }, diff --git a/QRScanner.js b/QRScanner.js index 9895200..d3071eb 100644 --- a/QRScanner.js +++ b/QRScanner.js @@ -18,8 +18,10 @@ import {Button, IconButton, Portal} from "react-native-paper"; import {Camera, CameraView, scanFromURLAsync} from "expo-camera"; import * as ImagePicker from "expo-image-picker"; import PropTypes from "prop-types"; +import {useTranslation} from "react-i18next"; const QRScanner = ({onScan, onClose, children}) => { + const {t} = useTranslation(); const [hasPermission, setHasPermission] = useState(null); useEffect(() => { @@ -50,11 +52,11 @@ const QRScanner = ({onScan, onClose, children}) => { }; if (hasPermission === null) { - return Requesting permissions...; + return {t("qrScanner.Requesting permissions")}; } if (hasPermission === false) { - return No access to camera or media library; + return {t("qrScanner.No access to camera or media library")}; } return ( @@ -88,7 +90,7 @@ const QRScanner = ({onScan, onClose, children}) => { onPress={pickImage} style={children ? {flex: 1} : {width: 200}} > - Choose Image + {t("qrScanner.Choose Image")} {children} diff --git a/ScanLogin.js b/ScanLogin.js index 71bc7b6..b453e28 100644 --- a/ScanLogin.js +++ b/ScanLogin.js @@ -17,18 +17,19 @@ import PropTypes from "prop-types"; import * as Clipboard from "expo-clipboard"; import QRScanner from "./QRScanner"; import {Button} from "react-native-paper"; +import i18next from "i18next"; const ScanQRCodeForLogin = ({onClose, showScanner, onLogin, onError}) => { const handleClipboardPaste = async() => { const text = await Clipboard.getStringAsync(); if (!isValidLoginQR(text)) { - onError?.("Invalid QR code format"); + onError?.(i18next.t("scanLogin.Invalid QR code format")); return; } const loginInfo = parseLoginQR(text); if (!loginInfo) { - onError?.("Missing required fields: serverUrl and accessToken"); + onError?.(i18next.t("scanLogin.Missing required fields: serverUrl and accessToken")); return; } @@ -38,13 +39,13 @@ const ScanQRCodeForLogin = ({onClose, showScanner, onLogin, onError}) => { const handleScan = (type, data) => { if (!isValidLoginQR(data)) { - onError?.("Invalid QR code format"); + onError?.(i18next.t("scanLogin.Invalid QR code format")); return; } const loginInfo = parseLoginQR(data); if (!loginInfo) { - onError?.("Missing required fields: serverUrl and accessToken"); + onError?.(i18next.t("scanLogin.Missing required fields: serverUrl and accessToken")); return; } @@ -65,7 +66,7 @@ const ScanQRCodeForLogin = ({onClose, showScanner, onLogin, onError}) => { const accessToken = params.get("accessToken"); if (!serverUrl || !accessToken) { - throw new Error("Missing required fields"); + throw new Error(i18next.t("scanLogin.Missing required fields")); } return { @@ -89,7 +90,7 @@ const ScanQRCodeForLogin = ({onClose, showScanner, onLogin, onError}) => { onPress={handleClipboardPaste} style={{flex: 1}} > - Paste QR Code + {i18next.t("scanLogin.Paste QR Code")} ); diff --git a/SearchBar.js b/SearchBar.js index e6a44a8..9cce449 100644 --- a/SearchBar.js +++ b/SearchBar.js @@ -15,8 +15,10 @@ import * as React from "react"; import {View} from "react-native"; import {Searchbar} from "react-native-paper"; +import {useTranslation} from "react-i18next"; const SearchBar = ({onSearch}) => { + const {t} = useTranslation(); const [searchQuery, setSearchQuery] = React.useState(""); const onChangeSearch = (query) => { @@ -27,7 +29,7 @@ const SearchBar = ({onSearch}) => { return ( { const [showLoginPage, setShowLoginPage] = useState(false); const [loginMethod, setLoginMethod] = useState(null); const {userInfo, clearAll} = useStore(); - + const theme = useTheme(); + const {t} = useTranslation(); const {openActionSheet} = LoginMethodSelector({ onSelectMethod: (method) => { setLoginMethod(method); @@ -50,49 +55,111 @@ const SettingPage = () => { return ( - - - Account Settings - + + + + {userInfo ? ( + + + + {userInfo.name} + + {userInfo.email} + + + + + ) : ( + + )} + + + {/* Settings Sections */} + + {t("settings.Preferences")} + + } + right={() => } + /> + + + + + {t("settings.About")} + + } + /> + + + {showLoginPage && ( )} - + ); }; const styles = StyleSheet.create({ + scrollView: { + flex: 1, + backgroundColor: "#F2F2F2", + }, container: { flex: 1, - justifyContent: "center", - alignItems: "center", padding: 16, + width: width > 600 ? 600 : "100%", + alignSelf: "center", + backgroundColor: "#F2F2F2", }, - surface: { - padding: 16, - width: width > 600 ? 400 : "100%", - maxWidth: 400, + profileCard: { + padding: 12, + marginBottom: 14, + borderRadius: 14, + }, + profileInfo: { + flexDirection: "row", alignItems: "center", + padding: 8, + }, + profileText: { + flex: 1, + marginLeft: 16, + }, + loginButton: { + borderRadius: 8, + alignSelf: "center", + paddingHorizontal: 20, }, - title: { - fontSize: 24, - marginBottom: 24, + loginButtonLabel: { + fontSize: 18, }, - button: { - marginTop: 16, - width: "100%", + logoutButton: { + marginLeft: 8, }, }); diff --git a/api.js b/api.js index 567d070..d8b9fcb 100644 --- a/api.js +++ b/api.js @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +import i18next from "i18next"; + const TIMEOUT_MS = 5000; const timeout = (ms) => { @@ -44,7 +46,7 @@ export const getMfaAccounts = async(serverUrl, owner, name, token, timeoutMs = T }; } catch (error) { if (error.name === "AbortError") { - throw new Error("Request timed out"); + throw new Error(i18next.t("api.Request timed out")); } throw error; } finally { @@ -93,7 +95,7 @@ export const updateMfaAccounts = async(serverUrl, owner, name, newMfaAccounts, t return {status: res.status, data: res.data}; } catch (error) { if (error.name === "AbortError") { - throw new Error("Request timed out"); + throw new Error(i18next.t("api.Request timed out")); } throw error; } finally { diff --git a/app.json b/app.json index 2f5b427..b95069f 100644 --- a/app.json +++ b/app.json @@ -51,7 +51,8 @@ } ], "expo-asset", - "expo-font" + "expo-font", + "expo-localization" ], "owner": "casdoor" } diff --git a/i18n.js b/i18n.js new file mode 100644 index 0000000..90dace0 --- /dev/null +++ b/i18n.js @@ -0,0 +1,44 @@ +// Copyright 2024 The Casdoor Authors. All Rights Reserved. +// +// Licensed 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 i18n from "i18next"; +import {initReactI18next} from "react-i18next"; +import {getLocales} from "expo-localization"; +import AsyncStorage from "@react-native-async-storage/async-storage"; +import {getLanguageResources, isRTL} from "./Language"; + +const initI18n = async() => { + let savedLanguage = await AsyncStorage.getItem("language"); + + if (!savedLanguage) { + savedLanguage = getLocales()[0].languageCode; + } + + i18n + .use(initReactI18next) + .init({ + compatibilityJSON: "v3", + resources: getLanguageResources(), + lng: savedLanguage, + fallbackLng: "en", + interpolation: { + escapeValue: false, + }, + direction: isRTL(savedLanguage) ? "rtl" : "ltr", + }); +}; + +initI18n(); + +export default i18n; diff --git a/locales/ar/data.json b/locales/ar/data.json new file mode 100644 index 0000000..e1683ad --- /dev/null +++ b/locales/ar/data.json @@ -0,0 +1,98 @@ +{ + "common": { + "success": "نجاح", + "error": "خطأ", + "back": "رجوع", + "cancel": "إلغاء", + "confirm": "تأكيد", + "save": "حفظ", + "delete": "حذف", + "edit": "تعديل", + "login": "تسجيل الدخول", + "logout": "تسجيل الخروج" + }, + "api": { + "Request timed out": "انتهت مهلة الطلب" + }, + "casdoorLoginPage": { + "Logged in successfully!": "تم تسجيل الدخول بنجاح!", + "Back to Config": "العودة إلى الإعدادات" + }, + "editAccount": { + "Enter new account name": "أدخل اسم الحساب الجديد", + "Account Name is required": "اسم الحساب مطلوب", + "Secret Key is required": "المفتاح السري مطلوب", + "Please fill in all the fields!": "يرجى ملء جميع الحقول!", + "Invalid Secret Key": "المفتاح السري غير صالح", + "Add Account": "إضافة حساب", + "Account Name": "اسم الحساب", + "Secret Key": "المفتاح السري", + "Time based": "مبني على الوقت", + "Counter based": "مبني على العداد" + }, + "enterCasdoorSDKConfig": { + "Please fill in all the fields!": "يرجى ملء جميع الحقول!", + "Casdoor Configuration": "إعداد Casdoor", + "Server URL": "عنوان URL للخادم", + "Client ID": "معرّف العميل", + "Application Name": "اسم التطبيق", + "Organization Name": "اسم المنظمة" + }, + "header": { + "Casdoor": "كاسدور", + "An unknown error occurred during synchronization": "حدث خطأ غير معروف أثناء المزامنة" + }, + "homepage": { + "Import error": "خطأ في الاستيراد", + "Error scanning QR code": "خطأ في مسح رمز الاستجابة السريعة", + "Scan QR Code": "مسح رمز الاستجابة السريعة", + "Enter Secret Code": "أدخل الكود السري", + "Import from other app": "استيراد من تطبيق آخر" + }, + "importManager": { + "Select app to import from": "حدد التطبيق للاستيراد منه", + "Import function not implemented for": "وظيفة الاستيراد غير متوفرة لـ" + }, + "loginMethod": { + "Select Login Method": "اختر طريقة تسجيل الدخول", + "Manual Server Setup": "إعداد الخادم يدويًا", + "Login Using QR Code": "تسجيل الدخول باستخدام رمز الاستجابة السريعة", + "Try Casdoor Demo Site": "جرب موقع كاسدور التجريبي" + }, + "msAuthImport": { + "Error creating directory": "خطأ في إنشاء الدليل", + "No data found in Microsoft Authenticator database": "لم يتم العثور على بيانات في قاعدة بيانات Microsoft Authenticator", + "file is not a database": "الملف ليس قاعدة بيانات", + "Error importing from Microsoft Authenticator": "خطأ في الاستيراد من Microsoft Authenticator" + }, + "navBar": { + "Home": "الرئيسية", + "Settings": "الإعدادات" + }, + "qrScanner": { + "Requesting permissions": "طلب الأذونات", + "No access to camera or media library": "لا يوجد وصول إلى الكاميرا أو مكتبة الوسائط", + "Choose Image": "اختر صورة" + }, + "scanLogin": { + "Invalid QR code format": "تنسيق رمز الاستجابة السريعة غير صالح", + "Missing required fields: serverUrl and accessToken": "الحقول المطلوبة مفقودة: serverUrl و accessToken", + "Missing required fields": "الحقول المطلوبة مفقودة", + "Paste QR Code": "الصق رمز الاستجابة السريعة" + }, + "searchBar": { + "Search": "بحث" + }, + "settings": { + "Sign In": "تسجيل الدخول", + "Preferences": "التفضيلات", + "Language": "اللغة", + "About": "حول", + "Version": "الإصدار" + }, + "syncLogic": { + "Sync failed": "فشل المزامنة", + "Access token has expired, please login again": "انتهت صلاحية رمز الوصول، يرجى تسجيل الدخول مرة أخرى" + } + } + \ No newline at end of file diff --git a/locales/de/data.json b/locales/de/data.json new file mode 100644 index 0000000..9d32c25 --- /dev/null +++ b/locales/de/data.json @@ -0,0 +1,98 @@ +{ + "common": { + "success": "Erfolg", + "error": "Fehler", + "back": "Zurück", + "cancel": "Abbrechen", + "confirm": "Bestätigen", + "save": "Speichern", + "delete": "Löschen", + "edit": "Bearbeiten", + "login": "Anmelden", + "logout": "Abmelden" + }, + "api": { + "Request timed out": "Zeitüberschreitung der Anfrage" + }, + "casdoorLoginPage": { + "Logged in successfully!": "Erfolgreich angemeldet!", + "Back to Config": "Zurück zur Konfiguration" + }, + "editAccount": { + "Enter new account name": "Neuen Kontonamen eingeben", + "Account Name is required": "Kontoname ist erforderlich", + "Secret Key is required": "Geheimschlüssel ist erforderlich", + "Please fill in all the fields!": "Bitte füllen Sie alle Felder aus!", + "Invalid Secret Key": "Ungültiger Geheimschlüssel", + "Add Account": "Konto hinzufügen", + "Account Name": "Kontoname", + "Secret Key": "Geheimschlüssel", + "Time based": "Zeitbasiert", + "Counter based": "Zählerbasiert" + }, + "enterCasdoorSDKConfig": { + "Please fill in all the fields!": "Bitte füllen Sie alle Felder aus!", + "Casdoor Configuration": "Casdoor Konfiguration", + "Server URL": "Server-URL", + "Client ID": "Client-ID", + "Application Name": "Anwendungsname", + "Organization Name": "Organisationsname" + }, + "header": { + "Casdoor": "Casdoor", + "An unknown error occurred during synchronization": "Ein unbekannter Fehler ist bei der Synchronisierung aufgetreten" + }, + "homepage": { + "Import error": "Importfehler", + "Error scanning QR code": "Fehler beim Scannen des QR-Codes", + "Scan QR Code": "QR-Code scannen", + "Enter Secret Code": "Geheimcode eingeben", + "Import from other app": "Aus einer anderen App importieren" + }, + "importManager": { + "Select app to import from": "App zum Importieren auswählen", + "Import function not implemented for": "Importfunktion für nicht implementiert" + }, + "loginMethod": { + "Select Login Method": "Anmeldemethode auswählen", + "Manual Server Setup": "Manuelle Servereinrichtung", + "Login Using QR Code": "Mit QR-Code anmelden", + "Try Casdoor Demo Site": "Casdoor-Demo-Site ausprobieren" + }, + "msAuthImport": { + "Error creating directory": "Fehler beim Erstellen des Verzeichnisses", + "No data found in Microsoft Authenticator database": "Keine Daten in der Microsoft Authenticator-Datenbank gefunden", + "file is not a database": "Die Datei ist keine Datenbank", + "Error importing from Microsoft Authenticator": "Fehler beim Importieren aus Microsoft Authenticator" + }, + "navBar": { + "Home": "Startseite", + "Settings": "Einstellungen" + }, + "qrScanner": { + "Requesting permissions": "Berechtigungen werden angefordert", + "No access to camera or media library": "Kein Zugriff auf Kamera oder Medienbibliothek", + "Choose Image": "Bild auswählen" + }, + "scanLogin": { + "Invalid QR code format": "Ungültiges QR-Code-Format", + "Missing required fields: serverUrl and accessToken": "Fehlende erforderliche Felder: serverUrl und accessToken", + "Missing required fields": "Fehlende erforderliche Felder", + "Paste QR Code": "QR-Code einfügen" + }, + "searchBar": { + "Search": "Suche" + }, + "settings": { + "Sign In": "Anmelden", + "Preferences": "Einstellungen", + "Language": "Sprache", + "About": "Über", + "Version": "Version" + }, + "syncLogic": { + "Sync failed": "Synchronisierung fehlgeschlagen", + "Access token has expired, please login again": "Das Zugriffstoken ist abgelaufen, bitte melden Sie sich erneut an" + } + } + \ No newline at end of file diff --git a/locales/en/data.json b/locales/en/data.json new file mode 100644 index 0000000..551e4d5 --- /dev/null +++ b/locales/en/data.json @@ -0,0 +1,97 @@ +{ + "common": { + "success": "Success", + "error": "Error", + "back": "Back", + "cancel": "Cancel", + "confirm": "Confirm", + "save": "Save", + "delete": "Delete", + "edit": "Edit", + "login": "Login", + "logout": "Logout" + }, + "api": { + "Request timed out": "Request timed out" + }, + "casdoorLoginPage": { + "Logged in successfully!": "Logged in successfully!", + "Back to Config": "Back to Config" + }, + "editAccount": { + "Enter new account name": "Enter new account name", + "Account Name is required": "Account Name is required", + "Secret Key is required": "Secret Key is required", + "Please fill in all the fields!": "Please fill in all the fields!", + "Invalid Secret Key": "Invalid Secret Key", + "Add Account": "Add Account", + "Account Name": "Account Name", + "Secret Key": "Secret Key", + "Time based": "Time based", + "Counter based": "Counter based" + }, + "enterCasdoorSDKConfig": { + "Please fill in all the fields!": "Please fill in all the fields!", + "Casdoor Configuration": "Casdoor Configuration", + "Server URL": "Server URL", + "Client ID": "Client ID", + "Application Name": "Application Name", + "Organization Name": "Organization Name" + }, + "header": { + "Casdoor": "Casdoor", + "An unknown error occurred during synchronization": "An unknown error occurred during synchronization" + }, + "homepage": { + "Import error": "Import error", + "Error scanning QR code": "Error scanning QR code", + "Scan QR Code": "Scan QR Code", + "Enter Secret Code": "Enter Secret Code", + "Import from other app": "Import from other app" + }, + "importManager": { + "Select app to import from": "Select app to import from", + "Import function not implemented for": "Import function not implemented for" + }, + "loginMethod": { + "Select Login Method": "Select Login Method", + "Manual Server Setup": "Manual Server Setup", + "Login Using QR Code": "Login Using QR Code", + "Try Casdoor Demo Site": "Try Casdoor Demo Site" + }, + "msAuthImport": { + "Error creating directory": "Error creating directory", + "No data found in Microsoft Authenticator database": "No data found in Microsoft Authenticator database", + "file is not a database": "file is not a database", + "Error importing from Microsoft Authenticator": "Error importing from Microsoft Authenticator" + }, + "navBar": { + "Home": "Home", + "Settings": "Settings" + }, + "qrScanner": { + "Requesting permissions": "Requesting permissions", + "No access to camera or media library": "No access to camera or media library", + "Choose Image": "Choose Image" + }, + "scanLogin": { + "Invalid QR code format": "Invalid QR code format", + "Missing required fields: serverUrl and accessToken": "Missing required fields: serverUrl and accessToken", + "Missing required fields": "Missing required fields", + "Paste QR Code": "Paste QR Code" + }, + "searchBar": { + "Search": "Search" + }, + "settings": { + "Sign In": "Sign In", + "Preferences": "Preferences", + "Language": "Language", + "About": "About", + "Version": "Version" + }, + "syncLogic": { + "Sync failed": "Sync failed", + "Access token has expired, please login again": "Access token has expired, please login again" + } +} diff --git a/locales/es/data.json b/locales/es/data.json new file mode 100644 index 0000000..d1c4150 --- /dev/null +++ b/locales/es/data.json @@ -0,0 +1,98 @@ +{ + "common": { + "success": "Éxito", + "error": "Error", + "back": "Atrás", + "cancel": "Cancelar", + "confirm": "Confirmar", + "save": "Guardar", + "delete": "Eliminar", + "edit": "Editar", + "login": "Iniciar sesión", + "logout": "Cerrar sesión" + }, + "api": { + "Request timed out": "Tiempo de solicitud agotado" + }, + "casdoorLoginPage": { + "Logged in successfully!": "¡Inicio de sesión exitoso!", + "Back to Config": "Volver a la configuración" + }, + "editAccount": { + "Enter new account name": "Introduce el nuevo nombre de cuenta", + "Account Name is required": "El nombre de la cuenta es obligatorio", + "Secret Key is required": "La clave secreta es obligatoria", + "Please fill in all the fields!": "¡Por favor, rellene todos los campos!", + "Invalid Secret Key": "Clave secreta no válida", + "Add Account": "Añadir cuenta", + "Account Name": "Nombre de la cuenta", + "Secret Key": "Clave secreta", + "Time based": "Basado en el tiempo", + "Counter based": "Basado en el contador" + }, + "enterCasdoorSDKConfig": { + "Please fill in all the fields!": "¡Por favor, rellene todos los campos!", + "Casdoor Configuration": "Configuración de Casdoor", + "Server URL": "URL del servidor", + "Client ID": "ID de cliente", + "Application Name": "Nombre de la aplicación", + "Organization Name": "Nombre de la organización" + }, + "header": { + "Casdoor": "Casdoor", + "An unknown error occurred during synchronization": "Ocurrió un error desconocido durante la sincronización" + }, + "homepage": { + "Import error": "Error de importación", + "Error scanning QR code": "Error al escanear el código QR", + "Scan QR Code": "Escanear código QR", + "Enter Secret Code": "Introduzca el código secreto", + "Import from other app": "Importar desde otra aplicación" + }, + "importManager": { + "Select app to import from": "Seleccionar la aplicación desde la que importar", + "Import function not implemented for": "Función de importación no implementada para" + }, + "loginMethod": { + "Select Login Method": "Seleccionar método de inicio de sesión", + "Manual Server Setup": "Configuración manual del servidor", + "Login Using QR Code": "Iniciar sesión con código QR", + "Try Casdoor Demo Site": "Probar el sitio demo de Casdoor" + }, + "msAuthImport": { + "Error creating directory": "Error al crear el directorio", + "No data found in Microsoft Authenticator database": "No se encontraron datos en la base de datos de Microsoft Authenticator", + "file is not a database": "el archivo no es una base de datos", + "Error importing from Microsoft Authenticator": "Error al importar desde Microsoft Authenticator" + }, + "navBar": { + "Home": "Inicio", + "Settings": "Configuración" + }, + "qrScanner": { + "Requesting permissions": "Solicitando permisos", + "No access to camera or media library": "Sin acceso a la cámara o a la biblioteca de medios", + "Choose Image": "Elegir imagen" + }, + "scanLogin": { + "Invalid QR code format": "Formato de código QR no válido", + "Missing required fields: serverUrl and accessToken": "Faltan campos obligatorios: serverUrl y accessToken", + "Missing required fields": "Faltan campos obligatorios", + "Paste QR Code": "Pegar código QR" + }, + "searchBar": { + "Search": "Buscar" + }, + "settings": { + "Sign In": "Iniciar sesión", + "Preferences": "Preferencias", + "Language": "Idioma", + "About": "Acerca de", + "Version": "Versión" + }, + "syncLogic": { + "Sync failed": "Error de sincronización", + "Access token has expired, please login again": "El token de acceso ha expirado, por favor inicie sesión nuevamente" + } + } + \ No newline at end of file diff --git a/locales/fr/data.json b/locales/fr/data.json new file mode 100644 index 0000000..b5c1033 --- /dev/null +++ b/locales/fr/data.json @@ -0,0 +1,98 @@ +{ + "common": { + "success": "Succès", + "error": "Erreur", + "back": "Retour", + "cancel": "Annuler", + "confirm": "Confirmer", + "save": "Enregistrer", + "delete": "Supprimer", + "edit": "Modifier", + "login": "Connexion", + "logout": "Déconnexion" + }, + "api": { + "Request timed out": "La requête a expiré" + }, + "casdoorLoginPage": { + "Logged in successfully!": "Connexion réussie !", + "Back to Config": "Retour à la configuration" + }, + "editAccount": { + "Enter new account name": "Entrez un nouveau nom de compte", + "Account Name is required": "Le nom du compte est requis", + "Secret Key is required": "La clé secrète est requise", + "Please fill in all the fields!": "Veuillez remplir tous les champs !", + "Invalid Secret Key": "Clé secrète invalide", + "Add Account": "Ajouter un compte", + "Account Name": "Nom du compte", + "Secret Key": "Clé secrète", + "Time based": "Basé sur le temps", + "Counter based": "Basé sur un compteur" + }, + "enterCasdoorSDKConfig": { + "Please fill in all the fields!": "Veuillez remplir tous les champs !", + "Casdoor Configuration": "Configuration Casdoor", + "Server URL": "URL du serveur", + "Client ID": "ID client", + "Application Name": "Nom de l'application", + "Organization Name": "Nom de l'organisation" + }, + "header": { + "Casdoor": "Casdoor", + "An unknown error occurred during synchronization": "Une erreur inconnue s'est produite lors de la synchronisation" + }, + "homepage": { + "Import error": "Erreur d'importation", + "Error scanning QR code": "Erreur lors du scan du code QR", + "Scan QR Code": "Scanner le code QR", + "Enter Secret Code": "Entrez le code secret", + "Import from other app": "Importer depuis une autre application" + }, + "importManager": { + "Select app to import from": "Sélectionnez l'application à partir de laquelle importer", + "Import function not implemented for": "Fonction d'importation non implémentée pour" + }, + "loginMethod": { + "Select Login Method": "Sélectionnez une méthode de connexion", + "Manual Server Setup": "Configuration manuelle du serveur", + "Login Using QR Code": "Connexion via code QR", + "Try Casdoor Demo Site": "Essayez le site de démonstration Casdoor" + }, + "msAuthImport": { + "Error creating directory": "Erreur lors de la création du répertoire", + "No data found in Microsoft Authenticator database": "Aucune donnée trouvée dans la base de données de Microsoft Authenticator", + "file is not a database": "Le fichier n'est pas une base de données", + "Error importing from Microsoft Authenticator": "Erreur lors de l'importation depuis Microsoft Authenticator" + }, + "navBar": { + "Home": "Accueil", + "Settings": "Paramètres" + }, + "qrScanner": { + "Requesting permissions": "Demande d'autorisations", + "No access to camera or media library": "Pas d'accès à la caméra ou à la bibliothèque multimédia", + "Choose Image": "Choisir une image" + }, + "scanLogin": { + "Invalid QR code format": "Format de code QR invalide", + "Missing required fields: serverUrl and accessToken": "Champs requis manquants : serverUrl et accessToken", + "Missing required fields": "Champs requis manquants", + "Paste QR Code": "Coller le code QR" + }, + "searchBar": { + "Search": "Rechercher" + }, + "settings": { + "Sign In": "Se connecter", + "Preferences": "Préférences", + "Language": "Langue", + "About": "À propos", + "Version": "Version" + }, + "syncLogic": { + "Sync failed": "Échec de la synchronisation", + "Access token has expired, please login again": "Le jeton d'accès a expiré, veuillez vous reconnecter" + } + } + \ No newline at end of file diff --git a/locales/ja/data.json b/locales/ja/data.json new file mode 100644 index 0000000..61e75a1 --- /dev/null +++ b/locales/ja/data.json @@ -0,0 +1,97 @@ +{ + "common": { + "success": "成功", + "error": "エラー", + "back": "戻る", + "cancel": "キャンセル", + "confirm": "確認", + "save": "保存", + "delete": "削除", + "edit": "編集", + "login": "ログイン", + "logout": "ログアウト" + }, + "api": { + "Request timed out": "リクエストがタイムアウトしました" + }, + "casdoorLoginPage": { + "Logged in successfully!": "ログインに成功しました!", + "Back to Config": "設定に戻る" + }, + "editAccount": { + "Enter new account name": "新しいアカウント名を入力してください", + "Account Name is required": "アカウント名は必須です", + "Secret Key is required": "シークレットキーは必須です", + "Please fill in all the fields!": "すべてのフィールドを入力してください!", + "Invalid Secret Key": "無効なシークレットキー", + "Add Account": "アカウントを追加", + "Account Name": "アカウント名", + "Secret Key": "シークレットキー", + "Time based": "時間ベース", + "Counter based": "カウンターベース" + }, + "enterCasdoorSDKConfig": { + "Please fill in all the fields!": "すべてのフィールドを入力してください!", + "Casdoor Configuration": "Casdoor設定", + "Server URL": "サーバーURL", + "Client ID": "クライアントID", + "Application Name": "アプリケーション名", + "Organization Name": "組織名" + }, + "header": { + "Casdoor": "Casdoor", + "An unknown error occurred during synchronization": "同期中に不明なエラーが発生しました" + }, + "homepage": { + "Import error": "インポートエラー", + "Error scanning QR code": "QRコードのスキャンエラー", + "Scan QR Code": "QRコードをスキャン", + "Enter Secret Code": "シークレットコードを入力", + "Import from other app": "他のアプリからインポート" + }, + "importManager": { + "Select app to import from": "インポート元のアプリを選択", + "Import function not implemented for": "インポート機能は実装されていません" + }, + "loginMethod": { + "Select Login Method": "ログイン方法を選択", + "Manual Server Setup": "手動サーバー設定", + "Login Using QR Code": "QRコードでログイン", + "Try Casdoor Demo Site": "Casdoorデモサイトを試す" + }, + "msAuthImport": { + "Error creating directory": "ディレクトリの作成エラー", + "No data found in Microsoft Authenticator database": "Microsoft Authenticatorデータベースにデータが見つかりません", + "file is not a database": "ファイルはデータベースではありません", + "Error importing from Microsoft Authenticator": "Microsoft Authenticatorからのインポートエラー" + }, + "navBar": { + "Home": "ホーム", + "Settings": "設定" + }, + "qrScanner": { + "Requesting permissions": "権限をリクエストしています", + "No access to camera or media library": "カメラまたはメディアライブラリへのアクセスがありません", + "Choose Image": "画像を選択" + }, + "scanLogin": { + "Invalid QR code format": "無効なQRコード形式", + "Missing required fields: serverUrl and accessToken": "必要なフィールドが不足しています: serverUrl と accessToken", + "Missing required fields": "必要なフィールドが不足しています", + "Paste QR Code": "QRコードを貼り付け" + }, + "searchBar": { + "Search": "検索" + }, + "settings": { + "Sign In": "サインイン", + "Preferences": "設定", + "Language": "言語", + "About": "について", + "Version": "バージョン" + }, + "syncLogic": { + "Sync failed": "同期に失敗しました", + "Access token has expired, please login again": "アクセストークンの有効期限が切れました。再度ログインしてください" + } +} diff --git a/locales/ko/data.json b/locales/ko/data.json new file mode 100644 index 0000000..2223968 --- /dev/null +++ b/locales/ko/data.json @@ -0,0 +1,98 @@ +{ + "common": { + "success": "성공", + "error": "오류", + "back": "뒤로", + "cancel": "취소", + "confirm": "확인", + "save": "저장", + "delete": "삭제", + "edit": "편집", + "login": "로그인", + "logout": "로그아웃" + }, + "api": { + "Request timed out": "요청 시간이 초과되었습니다" + }, + "casdoorLoginPage": { + "Logged in successfully!": "로그인에 성공했습니다!", + "Back to Config": "설정으로 돌아가기" + }, + "editAccount": { + "Enter new account name": "새로운 계정 이름을 입력하세요", + "Account Name is required": "계정 이름은 필수입니다", + "Secret Key is required": "비밀 키는 필수입니다", + "Please fill in all the fields!": "모든 필드를 작성해주세요!", + "Invalid Secret Key": "잘못된 비밀 키", + "Add Account": "계정 추가", + "Account Name": "계정 이름", + "Secret Key": "비밀 키", + "Time based": "시간 기반", + "Counter based": "카운터 기반" + }, + "enterCasdoorSDKConfig": { + "Please fill in all the fields!": "모든 필드를 작성해주세요!", + "Casdoor Configuration": "Casdoor 설정", + "Server URL": "서버 URL", + "Client ID": "클라이언트 ID", + "Application Name": "애플리케이션 이름", + "Organization Name": "조직 이름" + }, + "header": { + "Casdoor": "Casdoor", + "An unknown error occurred during synchronization": "동기화 중 알 수 없는 오류가 발생했습니다" + }, + "homepage": { + "Import error": "가져오기 오류", + "Error scanning QR code": "QR 코드 스캔 오류", + "Scan QR Code": "QR 코드 스캔", + "Enter Secret Code": "비밀 코드 입력", + "Import from other app": "다른 앱에서 가져오기" + }, + "importManager": { + "Select app to import from": "가져올 앱 선택", + "Import function not implemented for": "가져오기 기능이 구현되지 않음" + }, + "loginMethod": { + "Select Login Method": "로그인 방법 선택", + "Manual Server Setup": "수동 서버 설정", + "Login Using QR Code": "QR 코드로 로그인", + "Try Casdoor Demo Site": "Casdoor 데모 사이트 사용" + }, + "msAuthImport": { + "Error creating directory": "디렉터리 생성 오류", + "No data found in Microsoft Authenticator database": "Microsoft Authenticator 데이터베이스에서 데이터가 없습니다", + "file is not a database": "파일이 데이터베이스가 아닙니다", + "Error importing from Microsoft Authenticator": "Microsoft Authenticator에서 가져오기 오류" + }, + "navBar": { + "Home": "홈", + "Settings": "설정" + }, + "qrScanner": { + "Requesting permissions": "권한 요청 중", + "No access to camera or media library": "카메라 또는 미디어 라이브러리에 접근할 수 없습니다", + "Choose Image": "이미지 선택" + }, + "scanLogin": { + "Invalid QR code format": "잘못된 QR 코드 형식", + "Missing required fields: serverUrl and accessToken": "필수 필드가 누락되었습니다: serverUrl 및 accessToken", + "Missing required fields": "필수 필드가 누락되었습니다", + "Paste QR Code": "QR 코드 붙여넣기" + }, + "searchBar": { + "Search": "검색" + }, + "settings": { + "Sign In": "로그인", + "Preferences": "설정", + "Language": "언어", + "About": "정보", + "Version": "버전" + }, + "syncLogic": { + "Sync failed": "동기화 실패", + "Access token has expired, please login again": "액세스 토큰이 만료되었습니다. 다시 로그인해주세요" + } + } + \ No newline at end of file diff --git a/locales/pt/data.json b/locales/pt/data.json new file mode 100644 index 0000000..d578e0a --- /dev/null +++ b/locales/pt/data.json @@ -0,0 +1,98 @@ +{ + "common": { + "success": "Sucesso", + "error": "Erro", + "back": "Voltar", + "cancel": "Cancelar", + "confirm": "Confirmar", + "save": "Salvar", + "delete": "Excluir", + "edit": "Editar", + "login": "Entrar", + "logout": "Sair" + }, + "api": { + "Request timed out": "Tempo de solicitação esgotado" + }, + "casdoorLoginPage": { + "Logged in successfully!": "Login realizado com sucesso!", + "Back to Config": "Voltar para Configuração" + }, + "editAccount": { + "Enter new account name": "Digite o novo nome da conta", + "Account Name is required": "O nome da conta é obrigatório", + "Secret Key is required": "A chave secreta é obrigatória", + "Please fill in all the fields!": "Por favor, preencha todos os campos!", + "Invalid Secret Key": "Chave secreta inválida", + "Add Account": "Adicionar Conta", + "Account Name": "Nome da Conta", + "Secret Key": "Chave Secreta", + "Time based": "Baseado em tempo", + "Counter based": "Baseado em contador" + }, + "enterCasdoorSDKConfig": { + "Please fill in all the fields!": "Por favor, preencha todos os campos!", + "Casdoor Configuration": "Configuração Casdoor", + "Server URL": "URL do Servidor", + "Client ID": "ID do Cliente", + "Application Name": "Nome da Aplicação", + "Organization Name": "Nome da Organização" + }, + "header": { + "Casdoor": "Casdoor", + "An unknown error occurred during synchronization": "Ocorreu um erro desconhecido durante a sincronização" + }, + "homepage": { + "Import error": "Erro de importação", + "Error scanning QR code": "Erro ao escanear o código QR", + "Scan QR Code": "Escanear Código QR", + "Enter Secret Code": "Digite o Código Secreto", + "Import from other app": "Importar de outro aplicativo" + }, + "importManager": { + "Select app to import from": "Selecione o aplicativo para importar", + "Import function not implemented for": "Função de importação não implementada para" + }, + "loginMethod": { + "Select Login Method": "Selecione o Método de Login", + "Manual Server Setup": "Configuração Manual do Servidor", + "Login Using QR Code": "Entrar com Código QR", + "Try Casdoor Demo Site": "Experimentar o Site Demo do Casdoor" + }, + "msAuthImport": { + "Error creating directory": "Erro ao criar o diretório", + "No data found in Microsoft Authenticator database": "Nenhum dado encontrado no banco de dados do Microsoft Authenticator", + "file is not a database": "O arquivo não é um banco de dados", + "Error importing from Microsoft Authenticator": "Erro ao importar do Microsoft Authenticator" + }, + "navBar": { + "Home": "Início", + "Settings": "Configurações" + }, + "qrScanner": { + "Requesting permissions": "Solicitando permissões", + "No access to camera or media library": "Sem acesso à câmera ou biblioteca de mídia", + "Choose Image": "Escolher Imagem" + }, + "scanLogin": { + "Invalid QR code format": "Formato de código QR inválido", + "Missing required fields: serverUrl and accessToken": "Campos obrigatórios ausentes: serverUrl e accessToken", + "Missing required fields": "Campos obrigatórios ausentes", + "Paste QR Code": "Colar Código QR" + }, + "searchBar": { + "Search": "Buscar" + }, + "settings": { + "Sign In": "Entrar", + "Preferences": "Preferências", + "Language": "Idioma", + "About": "Sobre", + "Version": "Versão" + }, + "syncLogic": { + "Sync failed": "Sincronização falhou", + "Access token has expired, please login again": "O token de acesso expirou, por favor faça login novamente" + } + } + \ No newline at end of file diff --git a/locales/ru/data.json b/locales/ru/data.json new file mode 100644 index 0000000..7540597 --- /dev/null +++ b/locales/ru/data.json @@ -0,0 +1,98 @@ +{ + "common": { + "success": "Успех", + "error": "Ошибка", + "back": "Назад", + "cancel": "Отмена", + "confirm": "Подтвердить", + "save": "Сохранить", + "delete": "Удалить", + "edit": "Редактировать", + "login": "Войти", + "logout": "Выйти" + }, + "api": { + "Request timed out": "Время запроса истекло" + }, + "casdoorLoginPage": { + "Logged in successfully!": "Успешный вход!", + "Back to Config": "Назад к настройкам" + }, + "editAccount": { + "Enter new account name": "Введите новое имя аккаунта", + "Account Name is required": "Имя аккаунта обязательно", + "Secret Key is required": "Секретный ключ обязателен", + "Please fill in all the fields!": "Пожалуйста, заполните все поля!", + "Invalid Secret Key": "Недействительный секретный ключ", + "Add Account": "Добавить аккаунт", + "Account Name": "Имя аккаунта", + "Secret Key": "Секретный ключ", + "Time based": "Основано на времени", + "Counter based": "Основано на счетчике" + }, + "enterCasdoorSDKConfig": { + "Please fill in all the fields!": "Пожалуйста, заполните все поля!", + "Casdoor Configuration": "Конфигурация Casdoor", + "Server URL": "URL сервера", + "Client ID": "ID клиента", + "Application Name": "Имя приложения", + "Organization Name": "Имя организации" + }, + "header": { + "Casdoor": "Casdoor", + "An unknown error occurred during synchronization": "Произошла неизвестная ошибка во время синхронизации" + }, + "homepage": { + "Import error": "Ошибка импорта", + "Error scanning QR code": "Ошибка сканирования QR-кода", + "Scan QR Code": "Сканировать QR-код", + "Enter Secret Code": "Введите секретный код", + "Import from other app": "Импорт из другого приложения" + }, + "importManager": { + "Select app to import from": "Выберите приложение для импорта", + "Import function not implemented for": "Функция импорта не реализована для" + }, + "loginMethod": { + "Select Login Method": "Выберите метод входа", + "Manual Server Setup": "Ручная настройка сервера", + "Login Using QR Code": "Войти с помощью QR-кода", + "Try Casdoor Demo Site": "Попробуйте демонстрационный сайт Casdoor" + }, + "msAuthImport": { + "Error creating directory": "Ошибка при создании каталога", + "No data found in Microsoft Authenticator database": "Данные в базе Microsoft Authenticator не найдены", + "file is not a database": "Файл не является базой данных", + "Error importing from Microsoft Authenticator": "Ошибка импорта из Microsoft Authenticator" + }, + "navBar": { + "Home": "Главная", + "Settings": "Настройки" + }, + "qrScanner": { + "Requesting permissions": "Запрос разрешений", + "No access to camera or media library": "Нет доступа к камере или медиатеке", + "Choose Image": "Выбрать изображение" + }, + "scanLogin": { + "Invalid QR code format": "Недействительный формат QR-кода", + "Missing required fields: serverUrl and accessToken": "Отсутствуют обязательные поля: serverUrl и accessToken", + "Missing required fields": "Отсутствуют обязательные поля", + "Paste QR Code": "Вставить QR-код" + }, + "searchBar": { + "Search": "Поиск" + }, + "settings": { + "Sign In": "Войти", + "Preferences": "Настройки", + "Language": "Язык", + "About": "О программе", + "Version": "Версия" + }, + "syncLogic": { + "Sync failed": "Синхронизация не удалась", + "Access token has expired, please login again": "Токен доступа истёк, пожалуйста, войдите снова" + } + } + \ No newline at end of file diff --git a/locales/th/data.json b/locales/th/data.json new file mode 100644 index 0000000..1759f3a --- /dev/null +++ b/locales/th/data.json @@ -0,0 +1,98 @@ +{ + "common": { + "success": "สำเร็จ", + "error": "ข้อผิดพลาด", + "back": "ย้อนกลับ", + "cancel": "ยกเลิก", + "confirm": "ยืนยัน", + "save": "บันทึก", + "delete": "ลบ", + "edit": "แก้ไข", + "login": "เข้าสู่ระบบ", + "logout": "ออกจากระบบ" + }, + "api": { + "Request timed out": "การร้องขอล้มเหลว" + }, + "casdoorLoginPage": { + "Logged in successfully!": "เข้าสู่ระบบสำเร็จ!", + "Back to Config": "กลับไปที่การตั้งค่า" + }, + "editAccount": { + "Enter new account name": "กรอกชื่อบัญชีใหม่", + "Account Name is required": "ชื่อบัญชีเป็นสิ่งที่จำเป็น", + "Secret Key is required": "คีย์ลับเป็นสิ่งที่จำเป็น", + "Please fill in all the fields!": "กรุณากรอกข้อมูลให้ครบถ้วน!", + "Invalid Secret Key": "คีย์ลับไม่ถูกต้อง", + "Add Account": "เพิ่มบัญชี", + "Account Name": "ชื่อบัญชี", + "Secret Key": "คีย์ลับ", + "Time based": "อิงตามเวลา", + "Counter based": "อิงตามเคาน์เตอร์" + }, + "enterCasdoorSDKConfig": { + "Please fill in all the fields!": "กรุณากรอกข้อมูลให้ครบถ้วน!", + "Casdoor Configuration": "การตั้งค่า Casdoor", + "Server URL": "URL เซิร์ฟเวอร์", + "Client ID": "ID ลูกค้า", + "Application Name": "ชื่อแอปพลิเคชัน", + "Organization Name": "ชื่อองค์กร" + }, + "header": { + "Casdoor": "Casdoor", + "An unknown error occurred during synchronization": "เกิดข้อผิดพลาดที่ไม่รู้จักระหว่างการซิงค์ข้อมูล" + }, + "homepage": { + "Import error": "ข้อผิดพลาดในการนำเข้า", + "Error scanning QR code": "ข้อผิดพลาดในการสแกน QR โค้ด", + "Scan QR Code": "สแกน QR โค้ด", + "Enter Secret Code": "กรอกรหัสลับ", + "Import from other app": "นำเข้าจากแอปอื่น" + }, + "importManager": { + "Select app to import from": "เลือกแอปที่จะนำเข้า", + "Import function not implemented for": "ฟังก์ชันการนำเข้ามายังไม่ได้รับการพัฒนา" + }, + "loginMethod": { + "Select Login Method": "เลือกวิธีการเข้าสู่ระบบ", + "Manual Server Setup": "ตั้งค่าเซิร์ฟเวอร์ด้วยตนเอง", + "Login Using QR Code": "เข้าสู่ระบบโดยใช้ QR โค้ด", + "Try Casdoor Demo Site": "ลองใช้เว็บไซต์สาธิต Casdoor" + }, + "msAuthImport": { + "Error creating directory": "ข้อผิดพลาดในการสร้างไดเรกทอรี", + "No data found in Microsoft Authenticator database": "ไม่พบข้อมูลในฐานข้อมูลของ Microsoft Authenticator", + "file is not a database": "ไฟล์ไม่ใช่ฐานข้อมูล", + "Error importing from Microsoft Authenticator": "ข้อผิดพลาดในการนำเข้าจาก Microsoft Authenticator" + }, + "navBar": { + "Home": "หน้าหลัก", + "Settings": "การตั้งค่า" + }, + "qrScanner": { + "Requesting permissions": "ขอสิทธิ์", + "No access to camera or media library": "ไม่มีการเข้าถึงกล้องหรือห้องสมุดสื่อ", + "Choose Image": "เลือกภาพ" + }, + "scanLogin": { + "Invalid QR code format": "รูปแบบ QR โค้ดไม่ถูกต้อง", + "Missing required fields: serverUrl and accessToken": "ขาดฟิลด์ที่จำเป็น: serverUrl และ accessToken", + "Missing required fields": "ขาดฟิลด์ที่จำเป็น", + "Paste QR Code": "วาง QR โค้ด" + }, + "searchBar": { + "Search": "ค้นหา" + }, + "settings": { + "Sign In": "เข้าสู่ระบบ", + "Preferences": "การตั้งค่า", + "Language": "ภาษา", + "About": "เกี่ยวกับ", + "Version": "เวอร์ชัน" + }, + "syncLogic": { + "Sync failed": "การซิงค์ล้มเหลว", + "Access token has expired, please login again": "โทเค็นการเข้าถึงหมดอายุ กรุณาล็อกอินอีกครั้ง" + } + } + \ No newline at end of file diff --git a/locales/uk/data.json b/locales/uk/data.json new file mode 100644 index 0000000..a1b1340 --- /dev/null +++ b/locales/uk/data.json @@ -0,0 +1,98 @@ +{ + "common": { + "success": "Успіх", + "error": "Помилка", + "back": "Назад", + "cancel": "Скасувати", + "confirm": "Підтвердити", + "save": "Зберегти", + "delete": "Видалити", + "edit": "Редагувати", + "login": "Увійти", + "logout": "Вийти" + }, + "api": { + "Request timed out": "Час запиту вичерпано" + }, + "casdoorLoginPage": { + "Logged in successfully!": "Успішно увійшли!", + "Back to Config": "Назад до налаштувань" + }, + "editAccount": { + "Enter new account name": "Введіть нове ім'я акаунта", + "Account Name is required": "Ім'я акаунта обов'язкове", + "Secret Key is required": "Необхідний секретний ключ", + "Please fill in all the fields!": "Будь ласка, заповніть всі поля!", + "Invalid Secret Key": "Невірний секретний ключ", + "Add Account": "Додати акаунт", + "Account Name": "Ім'я акаунта", + "Secret Key": "Секретний ключ", + "Time based": "Залежно від часу", + "Counter based": "Залежно від лічильника" + }, + "enterCasdoorSDKConfig": { + "Please fill in all the fields!": "Будь ласка, заповніть всі поля!", + "Casdoor Configuration": "Налаштування Casdoor", + "Server URL": "URL сервера", + "Client ID": "ID клієнта", + "Application Name": "Назва програми", + "Organization Name": "Назва організації" + }, + "header": { + "Casdoor": "Casdoor", + "An unknown error occurred during synchronization": "Під час синхронізації сталася невідома помилка" + }, + "homepage": { + "Import error": "Помилка імпорту", + "Error scanning QR code": "Помилка при скануванні QR коду", + "Scan QR Code": "Сканувати QR код", + "Enter Secret Code": "Введіть секретний код", + "Import from other app": "Імпортувати з іншого додатку" + }, + "importManager": { + "Select app to import from": "Виберіть додаток для імпорту", + "Import function not implemented for": "Функція імпорту не реалізована для" + }, + "loginMethod": { + "Select Login Method": "Виберіть метод входу", + "Manual Server Setup": "Ручне налаштування сервера", + "Login Using QR Code": "Увійти за допомогою QR коду", + "Try Casdoor Demo Site": "Спробуйте демонстраційний сайт Casdoor" + }, + "msAuthImport": { + "Error creating directory": "Помилка при створенні каталогу", + "No data found in Microsoft Authenticator database": "Дані не знайдені в базі Microsoft Authenticator", + "file is not a database": "Файл не є базою даних", + "Error importing from Microsoft Authenticator": "Помилка при імпорті з Microsoft Authenticator" + }, + "navBar": { + "Home": "Головна", + "Settings": "Налаштування" + }, + "qrScanner": { + "Requesting permissions": "Запит на дозволи", + "No access to camera or media library": "Немає доступу до камери або медіатеки", + "Choose Image": "Вибрати зображення" + }, + "scanLogin": { + "Invalid QR code format": "Невірний формат QR коду", + "Missing required fields: serverUrl and accessToken": "Відсутні обов'язкові поля: serverUrl і accessToken", + "Missing required fields": "Відсутні обов'язкові поля", + "Paste QR Code": "Вставити QR код" + }, + "searchBar": { + "Search": "Пошук" + }, + "settings": { + "Sign In": "Увійти", + "Preferences": "Налаштування", + "Language": "Мова", + "About": "Про програму", + "Version": "Версія" + }, + "syncLogic": { + "Sync failed": "Синхронізація не вдалася", + "Access token has expired, please login again": "Токен доступу минув, будь ласка, увійдіть знову" + } + } + \ No newline at end of file diff --git a/locales/zh/data.json b/locales/zh/data.json new file mode 100644 index 0000000..4df584c --- /dev/null +++ b/locales/zh/data.json @@ -0,0 +1,97 @@ +{ + "common": { + "success": "成功", + "error": "错误", + "back": "返回", + "cancel": "取消", + "confirm": "确认", + "save": "保存", + "delete": "删除", + "edit": "编辑", + "login": "登录", + "logout": "退出登录" + }, + "api": { + "Request timed out": "请求超时" + }, + "casdoorLoginPage": { + "Logged in successfully!": "登录成功!", + "Back to Config": "返回配置" + }, + "editAccount": { + "Enter new account name": "输入新账户名称", + "Account Name is required": "账户名称为必填项", + "Secret Key is required": "密钥为必填项", + "Please fill in all the fields!": "请填写所有字段!", + "Invalid Secret Key": "无效的密钥", + "Add Account": "添加账户", + "Account Name": "账户名称", + "Secret Key": "密钥", + "Time based": "基于时间", + "Counter based": "基于计数器" + }, + "enterCasdoorSDKConfig": { + "Please fill in all the fields!": "请填写所有字段!", + "Casdoor Configuration": "Casdoor 配置", + "Server URL": "服务器地址", + "Client ID": "客户端 ID", + "Application Name": "应用名称", + "Organization Name": "组织名称" + }, + "header": { + "Casdoor": "Casdoor", + "An unknown error occurred during synchronization": "同步过程中发生未知错误" + }, + "homepage": { + "Import error": "导入错误", + "Error scanning QR code": "扫描二维码错误", + "Scan QR Code": "扫描二维码", + "Enter Secret Code": "输入密钥", + "Import from other app": "从其他应用导入" + }, + "importManager": { + "Select app to import from": "选择要导入的应用", + "Import function not implemented for": "导入功能未实现于" + }, + "loginMethod": { + "Select Login Method": "选择登录方式", + "Manual Server Setup": "手动服务器设置", + "Login Using QR Code": "使用二维码登录", + "Try Casdoor Demo Site": "试用 Casdoor 演示站点" + }, + "msAuthImport": { + "Error creating directory": "创建目录错误", + "No data found in Microsoft Authenticator database": "Microsoft Authenticator 数据库中未找到数据", + "file is not a database": "文件不是数据库", + "Error importing from Microsoft Authenticator": "从 Microsoft Authenticator 导入时出错" + }, + "navBar": { + "Home": "首页", + "Settings": "设置" + }, + "qrScanner": { + "Requesting permissions": "请求权限", + "No access to camera or media library": "无法访问相机或媒体库", + "Choose Image": "选择图片" + }, + "scanLogin": { + "Invalid QR code format": "无效的二维码格式", + "Missing required fields: serverUrl and accessToken": "缺少必需字段:serverUrl 和 accessToken", + "Missing required fields": "缺少必需字段", + "Paste QR Code": "粘贴二维码" + }, + "searchBar": { + "Search": "搜索" + }, + "settings": { + "Sign In": "登录", + "Preferences": "偏好设置", + "Language": "语言", + "About": "关于", + "Version": "版本" + }, + "syncLogic": { + "Sync failed": "同步失败", + "Access token has expired, please login again": "访问令牌已过期,请重新登录" + } +} diff --git a/package-lock.json b/package-lock.json index 5253e78..596057e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "drizzle-orm": "^0.33.0", "eslint-plugin-import": "^2.28.1", "expo": "~51.0.39", + "expo-application": "~5.9.1", "expo-asset": "~10.0.10", "expo-camera": "~15.0.16", "expo-clipboard": "~6.0.3", @@ -32,17 +33,20 @@ "expo-font": "~12.0.10", "expo-image": "~1.13.0", "expo-image-picker": "~15.0.7", + "expo-localization": "~15.0.3", "expo-sqlite": "^14.0.6", "expo-status-bar": "~1.12.1", "expo-system-ui": "~3.0.7", "expo-updates": "~0.25.27", "hi-base32": "^0.5.1", "hotp-totp": "^1.0.6", + "i18next": "^23.16.5", "prop-types": "^15.8.1", "protobufjs": "^7.4.0", "react": "18.2.0", "react-content-loader": "^7.0.2", "react-dom": "18.2.0", + "react-i18next": "^15.1.1", "react-native": "0.74.5", "react-native-countdown-circle-timer": "^3.2.1", "react-native-gesture-handler": "~2.16.1", @@ -11301,6 +11305,15 @@ "expo": "bin/cli" } }, + "node_modules/expo-application": { + "version": "5.9.1", + "resolved": "https://registry.npmjs.com/expo-application/-/expo-application-5.9.1.tgz", + "integrity": "sha512-uAfLBNZNahnDZLRU41ZFmNSKtetHUT9Ua557/q189ua0AWV7pQjoVAx49E4953feuvqc9swtU3ScZ/hN1XO/FQ==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-asset": { "version": "10.0.10", "resolved": "https://registry.npmjs.com/expo-asset/-/expo-asset-10.0.10.tgz", @@ -11554,6 +11567,18 @@ "expo": "*" } }, + "node_modules/expo-localization": { + "version": "15.0.3", + "resolved": "https://registry.npmjs.com/expo-localization/-/expo-localization-15.0.3.tgz", + "integrity": "sha512-IfcmlKuKRlowR9qIzL0e+nGHBeNoF7l2GQaOJstc7HZiPjNJ4J1R4D53ZNf483dt7JSkTRJBihdTadOtOEjRdg==", + "license": "MIT", + "dependencies": { + "rtl-detect": "^1.0.2" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-manifests": { "version": "0.14.3", "resolved": "https://registry.npmjs.com/expo-manifests/-/expo-manifests-0.14.3.tgz", @@ -12729,6 +12754,15 @@ "thirty-two": "^1.0.2" } }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.com/http-errors/-/http-errors-2.0.0.tgz", @@ -12782,6 +12816,29 @@ "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==", "license": "BSD-3-Clause" }, + "node_modules/i18next": { + "version": "23.16.5", + "resolved": "https://registry.npmjs.com/i18next/-/i18next-23.16.5.tgz", + "integrity": "sha512-KTlhE3EP9x6pPTAW7dy0WKIhoCpfOGhRQlO+jttQLgzVaoOjWwBWramu7Pp0i+8wDNduuzXfe3kkVbzrKyrbTA==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.com/ieee754/-/ieee754-1.2.1.tgz", @@ -17138,6 +17195,28 @@ "react": ">=17.0.0" } }, + "node_modules/react-i18next": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.com/react-i18next/-/react-i18next-15.1.1.tgz", + "integrity": "sha512-R/Vg9wIli2P3FfeI8o1eNJUJue5LWpFsQePCHdQDmX0Co3zkr6kdT8gAseb/yGeWbNz1Txc4bKDQuZYsC0kQfw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.0", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.2.3", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.com/react-is/-/react-is-16.13.1.tgz", @@ -18014,6 +18093,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/rtl-detect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.com/rtl-detect/-/rtl-detect-1.1.2.tgz", + "integrity": "sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ==", + "license": "BSD-3-Clause" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.com/run-parallel/-/run-parallel-1.2.0.tgz", @@ -19821,6 +19906,15 @@ "integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==", "license": "MIT" }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.com/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.com/walker/-/walker-1.0.8.tgz", diff --git a/package.json b/package.json index 307230e..50516a1 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,9 @@ "eslint-plugin-import": "^2.28.1", "expo": "~51.0.39", "expo-asset": "~10.0.10", + "expo-application": "~5.9.1", "expo-camera": "~15.0.16", + "expo-clipboard": "~6.0.3", "expo-crypto": "~13.0.2", "expo-dev-client": "~4.0.29", "expo-document-picker": "~12.0.2", @@ -33,17 +35,20 @@ "expo-font": "~12.0.10", "expo-image": "~1.13.0", "expo-image-picker": "~15.0.7", + "expo-localization": "~15.0.3", "expo-sqlite": "^14.0.6", "expo-status-bar": "~1.12.1", "expo-system-ui": "~3.0.7", "expo-updates": "~0.25.27", "hi-base32": "^0.5.1", "hotp-totp": "^1.0.6", + "i18next": "^23.16.5", "prop-types": "^15.8.1", "protobufjs": "^7.4.0", "react": "18.2.0", "react-content-loader": "^7.0.2", "react-dom": "18.2.0", + "react-i18next": "^15.1.1", "react-native": "0.74.5", "react-native-countdown-circle-timer": "^3.2.1", "react-native-gesture-handler": "~2.16.1", @@ -56,8 +61,7 @@ "react-native-web": "~0.19.6", "react-native-webview": "13.8.6", "totp-generator": "^0.0.14", - "zustand": "^4.5.4", - "expo-clipboard": "~6.0.3" + "zustand": "^4.5.4" }, "verifyConditions": [ "semantic-release-expo", diff --git a/syncLogic.js b/syncLogic.js index a1df988..42d92f9 100644 --- a/syncLogic.js +++ b/syncLogic.js @@ -13,6 +13,7 @@ // limitations under the License. import {eq} from "drizzle-orm"; +import i18next from "i18next"; import * as schema from "./db/schema"; import * as api from "./api"; import {generateToken} from "./totpUtil"; @@ -179,7 +180,7 @@ export async function syncWithCloud(db, userInfo, serverUrl, token) { ); if (status !== "ok") { - throw new Error("Sync failed"); + throw new Error(i18next.t("syncLogic.Sync failed")); } } @@ -188,7 +189,7 @@ export async function syncWithCloud(db, userInfo, serverUrl, token) { } catch (error) { if (error.message.includes("Access token has expired")) { handleTokenExpiration(); - throw new Error("Access token has expired, please login again."); + throw new Error(i18next.t("syncLogic.Access token has expired, please login again")); } throw error; }