From 835c97a20f298141ee7687bb938ae6378c3fab25 Mon Sep 17 00:00:00 2001 From: alex Date: Thu, 13 Jul 2023 18:05:53 +0000 Subject: [PATCH] i18n --- App.js | 32 ++-- README.md | 4 + app.json | 12 +- i18n.js | 29 ++++ locales/de.json | 34 +++++ locales/en.json | 34 +++++ package-lock.json | 70 +++++++++ package.json | 3 + src/Components/PickerModal/index.js | 90 +++++++++++ src/Components/SideBar/index.js | 13 +- src/Screens/Device/settings.js | 26 ++-- src/Screens/Settings/index.js | 225 ++++++++++++---------------- src/utils.js | 30 ++-- 13 files changed, 422 insertions(+), 180 deletions(-) create mode 100644 i18n.js create mode 100644 locales/de.json create mode 100644 locales/en.json create mode 100644 src/Components/PickerModal/index.js diff --git a/App.js b/App.js index 47bbd91..d27a3e1 100644 --- a/App.js +++ b/App.js @@ -1,6 +1,6 @@ import "react-native-gesture-handler"; import { StatusBar } from "expo-status-bar"; -import { Appearance, StyleSheet } from "react-native"; +import { Appearance, StyleSheet, Text, View } from "react-native"; import { createDrawerNavigator } from "@react-navigation/drawer"; import { NavigationContainer } from "@react-navigation/native"; import SideBar from "./src/Components/SideBar"; @@ -16,8 +16,9 @@ import { } from "./src/utils"; import DeviceScreen from "./src/Screens/Device"; import SettingsScreen from "./src/Screens/Settings"; -import { useContext, useEffect } from "react"; +import { Suspense, useContext, useEffect } from "react"; import { SafeAreaView } from "react-native-safe-area-context"; +import "./i18n"; const Drawer = createDrawerNavigator(); @@ -42,7 +43,7 @@ export function MyApp() { appLanguage === null ? Constants.defaultLanguage : appLanguage ); appContext.setAppColorScheme( - appColorScheme === null ? Appearance.getColorScheme() : appColorScheme + appColorScheme === null ? "auto" : appColorScheme ); appContext.setIsUserExpertModeEnabled( @@ -88,17 +89,18 @@ export function MyApp() { export default function App() { return ( - - - + + Loading... + + } + > + + + + ); } - -const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: "#fff", - alignItems: "center", - justifyContent: "center", - }, -}); diff --git a/README.md b/README.md index 01fbac5..4a855d5 100644 --- a/README.md +++ b/README.md @@ -9,3 +9,7 @@ https://pictogrammers.com/library/mdi/ # RNUILib https://wix.github.io/react-native-ui-lib/docs/getting-started/setup + +https://stackoverflow.com/questions/68243384/dark-mode-usecolorscheme-always-returns-light-on-android + +https://stackoverflow.com/questions/70493788/i18nextpluralresolver-your-environment-seems-not-to-be-intl-api-compatible-u diff --git a/app.json b/app.json index 73de1cf..aed4eb3 100644 --- a/app.json +++ b/app.json @@ -5,23 +5,23 @@ "version": "1.0.0", "orientation": "portrait", "icon": "./assets/icon.png", - "userInterfaceStyle": "light", + "userInterfaceStyle": "automatic", "splash": { "image": "./assets/splash.png", "resizeMode": "contain", "backgroundColor": "#ffffff" }, - "assetBundlePatterns": [ - "**/*" - ], + "assetBundlePatterns": ["**/*"], "ios": { - "supportsTablet": true + "supportsTablet": true, + "userInterfaceStyle": "automatic" }, "android": { "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", "backgroundColor": "#ffffff" - } + }, + "userInterfaceStyle": "automatic" }, "web": { "favicon": "./assets/favicon.png" diff --git a/i18n.js b/i18n.js new file mode 100644 index 0000000..67c8767 --- /dev/null +++ b/i18n.js @@ -0,0 +1,29 @@ +import i18n from "i18next"; +import { initReactI18next } from "react-i18next"; + +import de from "./locales/de.json"; +import en from "./locales/en.json"; + +const resources = { + de: { + translation: de, + }, + en: { + translation: en, + }, +}; + +i18n + .use(initReactI18next) // passes i18n down to react-i18next + .init({ + compatibilityJSON: "v3", + resources, + lng: "en", // language to use, more information here: https://www.i18next.com/overview/configuration-options#languages-namespaces-resources + // you can use the i18n.changeLanguage function to change the language manually: https://www.i18next.com/overview/api#changelanguage + // if you're using a language detector, do not define the lng option + interpolation: { + escapeValue: false, // react already safes from xss + }, + }); + +export default i18n; diff --git a/locales/de.json b/locales/de.json new file mode 100644 index 0000000..6ac2a92 --- /dev/null +++ b/locales/de.json @@ -0,0 +1,34 @@ +{ + "test": "Einstellungen", + "sideBar": { + "devicesTitle": "Geräte", + "settings": "Einstellungen", + "faq": "FAQ", + "feedback": "Feedback geben" + }, + "screens": { + "device": { + "settings": { + "settingsTitle": "Einstellungen", + "wifiStandByTitle": "WLAN im Standby", + "wifiStandByDescription": "Die WLAN-Verbindung bleibt bestehen, auch wenn das Gerät ausgeschaltet ist. Bitte beachten Sie, dass dies zu einem erhöhten Stromverbrauch führen kann.", + "deviceInformationTitle": "Geräteinformationen", + "deviceModelTitle": "Gerätemodell", + "deviceFirmwareVersionTitle": "Firmware Version", + "deviceLastUpdated": "Letzte Aktualisierung" + } + }, + "settings": { + "settingsCardTitle": "Einstellungen", + "languageText": "Sprache", + "appColorSchemeText": "Anzeigemodus", + "appColorSchemePicker": { + "auto": "Systemvoreinstellung", + "dark": "Dunkel", + "light": "Hell" + }, + "expertModeTitle": "Experten Modus", + "expertModeDescription": "Durch das Einschalten werden zusätzliche Funktionen in der App freigeschaltet, wie beispielsweise die Möglichkeit, benutzerdefinierte Farbcodes anzugeben." + } + } +} diff --git a/locales/en.json b/locales/en.json new file mode 100644 index 0000000..3660791 --- /dev/null +++ b/locales/en.json @@ -0,0 +1,34 @@ +{ + "test": "Settings", + "sideBar": { + "devicesTitle": "Devices", + "settings": "Settings", + "faq": "FAQ", + "feedback": "Give feedback" + }, + "screens": { + "device": { + "settings": { + "settingsTitle": "Settings", + "wifiStandByTitle": "WLAN in standby", + "wifiStandByDescription": "The WLAN connection remains established even if the device is switched off. Please note that this can lead to increased power consumption.", + "deviceInformationTitle": "Device information", + "deviceModelTitle": "Device model", + "deviceFirmwareVersionTitle": "Firmware Version", + "deviceLastUpdated": "Last updated" + } + }, + "settings": { + "settingsCardTitle": "Settings", + "languageText": "Language", + "appColorSchemeText": "Appearance", + "appColorSchemePicker": { + "auto": "System default", + "dark": "Dark", + "light": "Light" + }, + "expertModeTitle": "Expert mode", + "expertModeDescription": "Turning it on unlocks additional features in the app, such as the ability to specify custom color codes." + } + } +} diff --git a/package-lock.json b/package-lock.json index ea5cc2a..a97367d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,10 @@ "@react-navigation/native-stack": "^6.9.13", "expo": "~48.0.18", "expo-status-bar": "~1.4.4", + "i18next": "^23.2.11", + "i18next-browser-languagedetector": "^7.1.0", "react": "18.2.0", + "react-i18next": "^13.0.2", "react-native": "0.71.8", "react-native-gesture-handler": "~2.9.0", "react-native-pager-view": "6.1.2", @@ -8091,6 +8094,14 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -8126,6 +8137,36 @@ "node": ">= 6" } }, + "node_modules/i18next": { + "version": "23.2.11", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.2.11.tgz", + "integrity": "sha512-MA4FsxOjyCaOZtRDB4yuwjCvqYEioD4G4LlXOn7SO3rnQUlxTufyLsOqfL9MKakeLRBkefe8bqcs0D6Z/xFk1w==", + "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" + } + ], + "dependencies": { + "@babel/runtime": "^7.22.5" + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-7.1.0.tgz", + "integrity": "sha512-cr2k7u1XJJ4HTOjM9GyOMtbOA47RtUoWRAtt52z43r3AoMs2StYKyjS3URPhzHaf+mn10hY9dZWamga5WPQjhA==", + "dependencies": { + "@babel/runtime": "^7.19.4" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -12105,6 +12146,27 @@ "react": ">=17.0.0" } }, + "node_modules/react-i18next": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-13.0.2.tgz", + "integrity": "sha512-NEVxC32v0oR4egwYM0QM0WE93AiJG5r0NTXTL8mhQfAhsMfDS2fSO6jpluyfsfypP988KzUQrAXncspcJ7+GHA==", + "dependencies": { + "@babel/runtime": "^7.22.5", + "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": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -14351,6 +14413,14 @@ "resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz", "integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==" }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", diff --git a/package.json b/package.json index 41c94a0..6b76188 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,10 @@ "@react-navigation/native-stack": "^6.9.13", "expo": "~48.0.18", "expo-status-bar": "~1.4.4", + "i18next": "^23.2.11", + "i18next-browser-languagedetector": "^7.1.0", "react": "18.2.0", + "react-i18next": "^13.0.2", "react-native": "0.71.8", "react-native-gesture-handler": "~2.9.0", "react-native-pager-view": "6.1.2", diff --git a/src/Components/PickerModal/index.js b/src/Components/PickerModal/index.js new file mode 100644 index 0000000..3c3cac0 --- /dev/null +++ b/src/Components/PickerModal/index.js @@ -0,0 +1,90 @@ +import { useContext } from "react"; +import { AppContext, AppStyles } from "../../utils"; +import Icon from "@expo/vector-icons/MaterialCommunityIcons"; +import { Modal, Text, TouchableOpacity, View } from "react-native"; +import { Divider } from "../Divider"; + +export default function PickerModal({ isOpen, setIsOpen, items }) { + const appContext = useContext(AppContext); + const closeModal = () => setIsOpen(false); + + return ( + closeModal()} + > + + { + console.log("press"); + setIsOpen(false); + }} + > + + + + {items.map((item, i) => { + return item.selected ? ( + + closeModal()}> + + + {item.label} + + + + + + + ) : ( + + { + closeModal(); + item.onPress(); + }} + > + + {item.label} + + + + + ); + })} + + + ); +} diff --git a/src/Components/SideBar/index.js b/src/Components/SideBar/index.js index 7ba2cf6..9f6c7de 100644 --- a/src/Components/SideBar/index.js +++ b/src/Components/SideBar/index.js @@ -4,10 +4,11 @@ import { Image, Text, TouchableOpacity, View } from "react-native"; import { AppContext, AppStyles } from "../../utils"; import { Divider } from "../Divider"; import Icon from "@expo/vector-icons/MaterialCommunityIcons"; -import { useIsFocused } from "@react-navigation/native"; +import { useTranslation } from "react-i18next"; export default function Sidebar(props) { const appContext = useContext(AppContext); + const { t } = useTranslation(); const MyDrawerItem = ({ label, @@ -53,7 +54,7 @@ export default function Sidebar(props) { console.log("Pressed")} + onPress={() => console.log("Pressed power")} style={{ right: -30 }} > - Geräte + {t("sideBar.devicesTitle")} {["Turtle"].map((item, i) => ( @@ -137,19 +138,19 @@ export default function Sidebar(props) { props.navigation.navigate("FAQ")} iconName="frequently-asked-questions" routeName="FAQ" /> props.navigation.navigate("Feedback")} iconName="comment-quote" routeName="Feedback" /> props.navigation.navigate("Settings")} iconName="cog" routeName="Settings" diff --git a/src/Screens/Device/settings.js b/src/Screens/Device/settings.js index f061328..0d51d30 100644 --- a/src/Screens/Device/settings.js +++ b/src/Screens/Device/settings.js @@ -1,11 +1,15 @@ import { Text, View } from "react-native"; import Card from "../../Components/Card"; import { Incubator, Switch } from "react-native-ui-lib"; -import { useState } from "react"; -import { AppStyles } from "../../utils"; +import { useContext, useState } from "react"; +import { AppContext, AppStyles } from "../../utils"; import { Divider } from "../../Components/Divider"; +import { useTranslation } from "react-i18next"; export default function SettingsView() { + const appContext = useContext(AppContext); + const { t } = useTranslation(); + const [switchState, setSwitchState] = useState(false); const [sliderValue, setSliderValue] = useState(0); @@ -16,12 +20,12 @@ export default function SettingsView() { style={[ AppStyles.typography20, { - color: "#fff", + color: appContext.appTheme.text, marginBottom: 10, }, ]} > - Einstellungen + {t("screens.device.settings.settingsTitle")} - WLAN im Standby + {t("screens.device.settings.wifiStandByTitle")} - Die WLAN-Verbindung bleibt bestehen, auch wenn das Gerät - ausgeschaltet ist. Bitte beachten Sie, dass dies zu einem erhöhten - Stromverbrauch führen kann. + {t("screens.device.settings.wifiStandByDescription")} - Geräteinformationen + {t("screens.device.settings.deviceInformationTitle")} - Gerätemodell + {t("screens.device.settings.deviceModelTitle")} Shimmex Aurora - Firmware Version + {t("screens.device.settings.deviceFirmwareVersionTitle")} 1.0.1 - Letzte Aktualisierung + {t("screens.device.settings.deviceLastUpdated")} 11.07.2023 um 20:33 Uhr diff --git a/src/Screens/Settings/index.js b/src/Screens/Settings/index.js index 19cd112..df65501 100644 --- a/src/Screens/Settings/index.js +++ b/src/Screens/Settings/index.js @@ -2,13 +2,17 @@ import { useContext, useState } from "react"; import { Modal, Text, TouchableOpacity, View } from "react-native"; import { Picker, Switch } from "react-native-ui-lib"; import Card from "../../Components/Card"; -import { AppContext, AppStyles } from "../../utils"; +import { AppContext, AppStyles, Constants } from "../../utils"; import { Divider } from "../../Components/Divider"; -import Icon from "@expo/vector-icons/MaterialCommunityIcons"; +import PickerModal from "../../Components/PickerModal"; +import { useTranslation } from "react-i18next"; export default function SettingsScreen() { const appContext = useContext(AppContext); - const [modalVisible, setModalVisible] = useState(false); + const { t } = useTranslation(); + const [modalAppColorSchemeVisible, setAppColorSchemeModalVisible] = + useState(false); + const [modalAppLanguageVisible, setModalAppLanguageVisible] = useState(false); return ( - Einstellungen + {t("screens.settings.settingsCardTitle")} - appContext.setAppLanguage(v)} - fieldType={Picker.fieldTypes.settings} - showSearch - searchPlaceholder="Search a language" - searchStyle={{ color: "#fff", placeholderTextColor: "#fff" }} + setModalAppLanguageVisible(true)} + style={{ marginBottom: 6 }} > - - - - + + + {t("screens.settings.languageText")} + + + { + Constants.languages.find( + (language) => language.name === appContext.appLanguage + ).label + } + + + + + setAppColorSchemeModalVisible(true)}> + + + {t("screens.settings.appColorSchemeText")} + + + {appContext.appColorScheme === "auto" + ? t("screens.settings.appColorSchemePicker.auto") + : appContext.appColorScheme === "dark" + ? t("screens.settings.appColorSchemePicker.dark") + : t("screens.settings.appColorSchemePicker.light")} + + + - appContext.setAppColorScheme(v)} - fieldType={Picker.fieldTypes.settings} - > - - - - - Experten Modus + {t("screens.settings.expertModeTitle")} - Durch das Einschalten werden zusätzliche Funktionen in der App - freigeschaltet, wie beispielsweise die Möglichkeit, - benutzerdefinierte Farbcodes anzugeben. + {t("screens.settings.expertModeDescription")} - setModalVisible(true)}> - - - Anzeigemodus - - - {appContext.appColorScheme} - - - + appContext.setAppColorScheme("auto"), + }, + { + label: t("screens.settings.appColorSchemePicker.dark"), + onPress: () => appContext.setAppColorScheme("dark"), + selected: appContext.appColorScheme === "dark", + }, + { + label: t("screens.settings.appColorSchemePicker.light"), + onPress: () => appContext.setAppColorScheme("light"), + selected: appContext.appColorScheme === "light", + }, + ]} + /> - { - setModalVisible(!modalVisible); - }} - > - setModalVisible(false)}> - - - - - System default - - - - - {appContext.appColorScheme === "dark" ? ( - setModalVisible(false)}> - - Dark - - - - ) : ( - { - setModalVisible(false); - appContext.setAppColorScheme("dark"); - }} - > - - Dark - - - )} - - - - {appContext.appColorScheme === "light" ? ( - setModalVisible(false)}> - - Light - - - - ) : ( - { - setModalVisible(false); - appContext.setAppColorScheme("light"); - }} - > - - Light - - - )} - + { + return { + label: language.label, + onPress: () => appContext.setAppLanguage(language.name), + selected: appContext.appLanguage === language.name, + }; + })} + /> ); diff --git a/src/utils.js b/src/utils.js index 3016ac2..1a93f5e 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,10 +1,20 @@ import AsyncStorage from "@react-native-async-storage/async-storage"; import { createContext, useState } from "react"; -import { StyleSheet } from "react-native"; -import { Colors } from "react-native-ui-lib"; +import { useTranslation } from "react-i18next"; +import { Appearance, StyleSheet } from "react-native"; export const Constants = { defaultLanguage: "de", + languages: [ + { + name: "de", + label: "Deutsch", + }, + { + name: "en", + label: "English", + }, + ], }; export const AppStyles = StyleSheet.create({ @@ -45,7 +55,7 @@ const DarkAppTheme = { }, }, divider: "#ddd", - icon: "#fff", + icon: "#ddd", }; const LightAppTheme = { @@ -122,23 +132,23 @@ export function AppProvider({ children }) { // TODO: only while development const [isUserDeveloperModeEnabled, setIsUserDeveloperModeEnabled] = useState(false); + const { i18n } = useTranslation(); const saveAppColorScheme = async (value) => { StoreData("appColorScheme", value); setAppColorScheme(value); - if (value === "dark") { - setAppTheme(DarkAppTheme); - console.log("dark"); - } else { - setAppTheme(LightAppTheme); - console.log("light"); - } + let colorScheme; + + colorScheme = value === "auto" ? Appearance.getColorScheme() : value; + + setAppTheme(colorScheme === "light" ? LightAppTheme : DarkAppTheme); }; const saveAppLanguage = async (value) => { StoreData("appLanguage", value); setAppLanguage(value); + i18n.changeLanguage(value); }; const saveUserExpertMode = async (value) => {