diff --git a/.github/workflows/i18n.yml b/.github/workflows/i18n.yml new file mode 100644 index 0000000000..23f998a154 --- /dev/null +++ b/.github/workflows/i18n.yml @@ -0,0 +1,31 @@ +name: i18n + +on: + # upload strings merged into develop + push: + branches: [develop] + paths: + - app/locales/en.json + - ios/en.lproj/*.strings + - android/app/src/main/res/values/strings.xml + # for testing + # pull_request: + # branches: [develop] + # paths: + # - app/locales/en.json + # - ios/en.lproj/*.strings + # - android/app/src/main/res/values/strings.xml + +jobs: + lokalise-upload: + runs-on: ubuntu-latest + env: + UPLOAD_FLAGS: --lang-iso=en --cleanup-mode --replace-modified --include-path --detect-icu-plurals --apply-tm --convert-placeholders --project-id=${{ secrets.LOKALISE_PROJECT_ID }} --token=${{ secrets.LOKALISE_TOKEN }} + + steps: + - uses: actions/checkout@v2 + + - run: curl -sfL https://raw.githubusercontent.com/lokalise/lokalise-cli-2-go/master/install.sh | sh + + - name: upload English files + run: ./bin/lokalise2 file upload --file=app/locales/en.json,ios/en.lproj/InfoPlist.strings,ios/en.lproj/Localizable.strings,android/app/src/main/res/values/strings.xml $UPLOAD_FLAGS diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7a91310f8b..6247d9663a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,11 +1,6 @@ name: Lint on: - push: - branches: - - develop - - release-candidate - - master pull_request: branches: - develop diff --git a/android/app/build.gradle b/android/app/build.gradle index d950179ee2..db390f4e4a 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -137,8 +137,8 @@ android { testBuildType System.getProperty('testBuildType', 'debug') testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' - versionCode 22 - versionName "0.9.4" + versionCode 23 + versionName "0.9.5" } splits { abi { @@ -198,13 +198,10 @@ android { dependencies { implementation project(':react-native-reanimated') - implementation project(':react-native-gesture-handler') - implementation project(':react-native-webview') implementation fileTree(dir: "libs", include: ["*.jar"]) implementation "com.facebook.react:react-native:+" // From node_modules implementation "androidx.annotation:annotation:1.1.0" - compile project(':react-native-fs') if (enableHermes) { def hermesPath = "../../node_modules/hermes-engine/android/"; diff --git a/android/app/src/main/java/org/pathcheck/covidsafepaths/MainApplication.java b/android/app/src/main/java/org/pathcheck/covidsafepaths/MainApplication.java index e1613f26b5..0fe70414ec 100644 --- a/android/app/src/main/java/org/pathcheck/covidsafepaths/MainApplication.java +++ b/android/app/src/main/java/org/pathcheck/covidsafepaths/MainApplication.java @@ -5,8 +5,6 @@ import com.facebook.react.PackageList; import com.facebook.react.ReactApplication; import com.swmansion.reanimated.ReanimatedPackage; -import com.swmansion.gesturehandler.react.RNGestureHandlerPackage; -import com.reactnativecommunity.webview.RNCWebViewPackage; import com.facebook.react.ReactNativeHost; import com.facebook.react.ReactPackage; import com.facebook.soloader.SoLoader; diff --git a/android/settings.gradle b/android/settings.gradle index 270a128887..ef98bc2a0a 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,14 +1,10 @@ rootProject.name = 'COVIDSafePaths' include ':react-native-reanimated' project(':react-native-reanimated').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-reanimated/android') -include ':react-native-gesture-handler' -project(':react-native-gesture-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-gesture-handler/android') include ':react-native-fs' project(':react-native-fs').projectDir = new File(settingsDir, '../node_modules/react-native-fs/android') include ':react-native-zip-archive' project(':react-native-zip-archive').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-zip-archive/android') -include ':react-native-webview' -project(':react-native-webview').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webview/android') include ':react-native-push-notification' project(':react-native-push-notification').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-push-notification/android') include ':@mauron85_react-native-background-geolocation' diff --git a/app/components/Button.js b/app/components/Button.js index 6b13266162..c5f8098e11 100644 --- a/app/components/Button.js +++ b/app/components/Button.js @@ -1,9 +1,10 @@ import PropTypes from 'prop-types'; import * as React from 'react'; -import { StyleSheet, Text, TouchableOpacity } from 'react-native'; +import { StyleSheet, TouchableOpacity } from 'react-native'; import LinearGradient from 'react-native-linear-gradient'; import colors from '../constants/colors'; +import { Typography } from './Typography'; class Button extends React.Component { render() { @@ -40,7 +41,7 @@ class Button extends React.Component { accessible accessibilityLabel={title} accessibilityRole='button'> - {title} - + ); diff --git a/app/components/DynamicTextInput.js b/app/components/DynamicTextInput.js new file mode 100644 index 0000000000..81204522d7 --- /dev/null +++ b/app/components/DynamicTextInput.js @@ -0,0 +1,12 @@ +// Custome Text handling RTL and LTR text direction +import styled from '@emotion/native'; + +import languages from '../locales/languages'; + +// Themes to be added + +const getCurrentDirection = () => languages.dir(); + +export const DynamicTextInput = styled.TextInput` + writing-direction: ${getCurrentDirection}; +`; diff --git a/app/components/NativePicker.js b/app/components/NativePicker.js index a4bbc27de8..bc7e92a5b1 100644 --- a/app/components/NativePicker.js +++ b/app/components/NativePicker.js @@ -3,15 +3,31 @@ import { Modal, Picker, Platform, - StyleSheet, - Text, - TextInput, - TouchableOpacity, TouchableWithoutFeedback, View, } from 'react-native'; -// Code for the language select dropdown, for nice native handling on both iOS and Android. +import { Typography } from '../components/Typography'; +import languages from '../locales/languages'; + +/** + * Native dropdown that abstracts away the UI differences for iOS and Android. + * + * Usage: + * + * + * ```jsx + * + * {({label, openPicker}) => ( + * {label} + * )} + * + * ``` + */ export default class NativePicker extends Component { constructor(props) { super(props); @@ -21,60 +37,42 @@ export default class NativePicker extends Component { } render() { + const renderLabel = this.props.children; + const openPicker = () => { + this.setState({ modalVisible: true }); + }; + const selectedItem = this.props.items.find( + i => i.value === this.props.value, + ); + const label = selectedItem?.label || ''; + const value = selectedItem?.value; + // iOS and Android Pickers behave differently, handled below if (Platform.OS === 'android') { - const selectedItem = this.props.items.find( - i => i.value === this.props.value, - ); - const selectedLabel = selectedItem ? selectedItem.label : ''; - return ( - - this.setState({ modalVisible: true })}> - { - this.setState({ searchString }); - }} - value={selectedLabel} - /> - + + {renderLabel({ value, label, openPicker })} - {this.props.items.map((i, index) => ( - + style={{ + opacity: 0, + position: 'absolute', + left: 0, + right: 0, + top: 0, + bottom: 0, + }}> + {this.props.items.map(i => ( + ))} ); } else { - const selectedItem = this.props.items.find( - i => i.value === this.props.value, - ); - const selectedLabel = selectedItem ? selectedItem.label : ''; - return ( - - { - this.setState({ modalVisible: true }); - }} - style={[ - styles.touchableTrigger, - this.props.hidden ? styles.opacityZero : null, - ]}> - {selectedLabel} - - + <> + {renderLabel({ value, label, openPicker })} this.setState({ modalVisible: false })}> - - - + + this.setState({ modalVisible: false })}> - Done - + {languages.t('Done')} + true} - onResponderReject={evt => {}}> + onStartShouldSetResponder={() => true} + onResponderReject={() => {}}> - {this.props.items.map((i, index) => ( + {this.props.items.map(i => ( @@ -128,53 +126,8 @@ export default class NativePicker extends Component { - + ); } } } - -const styles = StyleSheet.create({ - mainContainer: { - flex: 1, - backgroundColor: '#ffffff', - }, - imageContainer: { - alignItems: 'center', - justifyContent: 'center', - margin: '3%', - }, - descriptionContainer: { - flex: 1, - flexDirection: 'row', - }, - descriptionIconContainer: { - alignSelf: 'center', - }, - descriptionIcon: { - width: 10, - height: 10, - resizeMode: 'contain', - }, - descriptionTextContainer: { - flex: 1, - justifyContent: 'center', - marginLeft: '5%', - }, - touchableTrigger: { - backgroundColor: '#ffffff', - opacity: 0.4, - paddingVertical: 4, - paddingHorizontal: 11, - borderRadius: 100, - }, - touchableText: { - fontSize: 12, - color: '#4051DB', - textAlign: 'center', - textTransform: 'uppercase', - }, - opacityZero: { - opacity: 0, - }, -}); diff --git a/app/components/NavigationBarWrapper.js b/app/components/NavigationBarWrapper.js index e7746a3878..a1c54d462b 100644 --- a/app/components/NavigationBarWrapper.js +++ b/app/components/NavigationBarWrapper.js @@ -2,7 +2,7 @@ import styled from '@emotion/native'; import { useTheme } from 'emotion-theming'; import PropTypes from 'prop-types'; import * as React from 'react'; -import { StatusBar } from 'react-native'; +import { Dimensions, StatusBar } from 'react-native'; import { SvgXml } from 'react-native-svg'; import backArrow from './../assets/svgs/backArrow'; @@ -17,6 +17,8 @@ import Colors from '../constants/colors'; * onBackPress: () => void, * }} param0 */ +const widthScale = Math.min(Dimensions.get('window').width / 400, 1.0); + const NavigationBarWrapper = ({ children, title, onBackPress }) => { const theme = useTheme(); @@ -73,8 +75,9 @@ const Title = styled.Text` align-self: center; color: ${Colors.WHITE}; font-family: IBMPlexSans-Medium; - font-size: 26px; + font-size: ${26 * widthScale + 'px'}; position: absolute; + padding-horizontal: 20; text-align: center; width: 100%; `; diff --git a/app/components/Typography.js b/app/components/Typography.js index f8b609fa70..13894dfab9 100644 --- a/app/components/Typography.js +++ b/app/components/Typography.js @@ -1,6 +1,9 @@ import styled from '@emotion/native'; +import { css } from '@emotion/native/dist/native.cjs.prod'; import * as React from 'react'; +import { useLanguageDirection } from '../locales/languages'; + export const Type = { Headline1: 'headline1', Headline2: 'headline2', @@ -42,11 +45,18 @@ export const Typography = ({ secondary, monospace, bold, + style, children, ...otherProps }) => { return ( { + NativeModules.SettingsManager = { + settings: { AppleLocale: locale }, + I18nManager: { localeIdentifier: locale }, + }; +}; + +describe('supportedDeviceLanguageOrEnglish()', () => { + afterEach(() => { + NativeModules.SettingsManager = BACKUP_SETTINGS_MANAGER; + }); + + it('resolves device locale en -> en', () => { + setDeviceLocale('en'); + expect(languages.supportedDeviceLanguageOrEnglish()).toBe('en'); + }); + + it('resolves device locale en_US -> en', () => { + setDeviceLocale('en_US'); + expect(languages.supportedDeviceLanguageOrEnglish()).toBe('en'); + }); + + it('resolves device locale en-US -> en', () => { + setDeviceLocale('en-US'); + expect(languages.supportedDeviceLanguageOrEnglish()).toBe('en'); + }); + + it('resolves device locale en-us -> en', () => { + setDeviceLocale('en-us'); + expect(languages.supportedDeviceLanguageOrEnglish()).toBe('en'); + }); + + it('resolves device locale zh_Hant -> zh_Hant', () => { + setDeviceLocale('zh_Hant'); + expect(languages.supportedDeviceLanguageOrEnglish()).toBe('zh_Hant'); + }); + + it('resolves device locale zh-Hant -> zh_Hant', () => { + setDeviceLocale('zh-Hant'); + expect(languages.supportedDeviceLanguageOrEnglish()).toBe('zh_Hant'); + }); + + it('resolves unknown device locale xx-yy -> en', () => { + setDeviceLocale('xx0-yy'); + expect(languages.supportedDeviceLanguageOrEnglish()).toBe('en'); + }); +}); diff --git a/app/locales/ar.json b/app/locales/ar.json new file mode 100644 index 0000000000..124be9b15b --- /dev/null +++ b/app/locales/ar.json @@ -0,0 +1,124 @@ +{ + "label": { + "about_title": "نبذة عنا", + "authorities_add_button_label": "إضافة هيئة صحية موثوقة", + "authorities_add_url": "إضافة هيئة صحية عبر رابط", + "authorities_desc": "اختر الهيئة الصحية الموثوقة في منطقتك للحصول على بيانات التعرض و الانتشار. حدد إما اسمًا من السجل العام ، أو أدخل عنوان الويب الذي قدمته السلطة", + "authorities_input_placeholder": "الصق عنوان الرابط هنا", + "authorities_no_sources": "لا توجد بيانات حتى الآن", + "authorities_removal_alert_cancel": "إلغاء", + "authorities_removal_alert_desc": "هل أنت متأكد من أنك تريد إزالة مصدر بيانات هذه الهيئة", + "authorities_removal_alert_proceed": "تقدم", + "authorities_removal_alert_title": "إزالة الهيئة الموثوقة", + "authorities_title": "هيئة موثوقة", + "choose_provider_subtitle": "لتكون على علم بالتعرض والانتشار ، ستحتاج إلى الاشتراك في هيئة صحية", + "choose_provider_title": "اختر الهيئة الصحية", + "commitment": "التزام", + "commitment_para": "تسجل المسارات الآمنة بشكل آمن وتتحقق من تفاعلك مع الأشخاص الذين يستخدمون موقعك. لن تترك بياناتك هاتفك أبدًا بدون موافقتك", + "default_news_site_name": "أخبار المسارات الآمنة", + "event_history_subtitle": "افهم تعرضك الشخصي بناءً على المعلومات التي تشاركها السلطات الصحية", + "event_history_title": "سجلات التعرض", + "export_para_1": "إذا كانت نتيجة اختبار COVID-19 إيجابية ، فالرجاء القيام بدورك من خلال مشاركة سجل المواقع مع الهيئات المحلية", + "export_para_2": "تتم مشاركة بيانات الموقع كقائمة بسيطة بالأوقات والأماكن ، ولا توجد معلومات إضافية", + "home_at_risk_header": "ربما تكون قد تعرضت", + "home_at_risk_subsubtext": "هذا لا يعني أنك مصاب", + "home_at_risk_subtext": "استنادًا إلى سجل GPS الخاص بك ، من المحتمل أنك كنت على اتصال أو قريبًا من شخص تم تشخيصه بـ COVID19", + "home_enable_location": "تفعيل خدمة الموقع", + "home_mayo_link_heading": "مزيد من المعلومات عن COVID-19", + "home_mayo_link_label": " من (مايو كلينك) Mayo Clinic ", + "home_no_contact_header": "لم يتم التعرض", + "home_no_contact_subtext": "استنادًا إلى البيانات المتاحة ، لم تكن بالقرب من أي شخص تم الإبلاغ عنه إيجابيًا بـ COVID-19", + "home_unknown_header": "غير معروف", + "home_unknown_subtext": "لا يمكننا معرفة ما إذا كنت معرضًا للخطر ما لم يكن للتطبيق حق الوصول إلى بيانات موقعك", + "home_setting_off_header": "غير معروف", + "home_setting_off_subtext": "لا يمكننا معرفة ما إذا كنت في خطر ما لم تقم بتفعيل سجل المواقع المتتبعة الموجودة في شاشة الإعدادات", + "import_title": "تحميل المواقع المتعقبة", + "import_step_1":"ستمنحك إضافة بيانات موقعك من Google السبق في بناء مواقعك الأخيرة", + "import_step_2":"قبل أن تتمكن من التحميل ، يجب عليك أولاً الحصول على بيانات موقعك من Google", + "import_step_3":"قم بزيارة Google Takeout وقم بتحميل سجل المواقع باستخدام الإعدادات التالية: \n1. طريقة التحميل: \"الإضافة إلى Google Drive \" \n2. التكرار: \"التحميل مرة واحدة \" \n3. نوع الملف وحجمه: \". zip \" و \"1 جيجابايت \" \n4. ترسل Google بريدًا إلكترونيًا عندما يكون التحميل جاهزًا \n5. عد هنا لتحميل المواقع. خيارات التحميل: \n- التحميل من Google Drive \n- التحميل من المتصفح ، ثم التحميل من ملفات الهاتف المحلية. تأكد من أن تكون على شبكة WiFi لأن الملفات يمكن أن تكون كبيرة.", + "import_takeout":"قم بزيارة Google Takeout", + "import_success":"تم تحميل المواقع الأخيرة بنجاح!", + "import_already_imported":"تم تحميل ملف Takeout المقدم بالفعل", + "import_error":"حدث خطأ أثناء تحميل بياناتك", + "import_no_recent_locations": "ليس لدى Takeout أي مواقع حديثة", + "import_invalid_file_format": "تنسيق الملف المقدم غير معتمد. \n التنسيقات المدعومة: \". zip \".", + "latest_news": "أحدث الأخبار", + "launch_done_header": "انتهينا", + "launch_done_subheader": "أنت على استعداد لبدء التنفيذ.\n تذكر ، يمكنك دائمًا تحديث تفضيلاتك لاحقًا", + "launch_enable_location": "فعل تحديد الموقع", + "launch_enable_notif": "فعل الإشعارات", + "launch_finish_set_up": "إنهاء الإعداد", + "launch_get_started": "ابدأ", + "launch_location_access": "السماح بتحديد الموقع", + "launch_location_header": "من اجل تذكر الاماكن الذي تذهب إليها ، يحتاج هاتفك إلى حفظ موقعك", + "launch_location_subheader": "لا تقلق ، لا تترك المعلومات جهازك أبدًا ما لم تقرر المشاركة بها", + "launch_next": "التالى", + "launch_notif_header": "تتيح لك الإشعارات معرفة ما إذا كنت قريب من شخص مصاب أم لا", + "launch_notif_subheader": "لن نزعجك إلا لمشاركة التحديثات حول مخاطر التعرض المحتملة", + "launch_notification_access": "السماح بالإشعارات", + "launch_screen1_header": "طريق العودة إلى الوضع الطبيعي يبدأ من هنا", + "launch_screen2_header": "احصل على إشعار إذا قمت بالعبور بجانب شخص تم تشخيصه بـ COVID-19", + "launch_screen2_subheader": "المعرفة قوة", + "launch_screen3_header": "إذا كان اختبارك إيجابيًا، فيمكنك اختيار التبرع بياناتك بشكل مجهول", + "launch_screen3_subheader": "مما يساعد على الحفاظ على مجتمعك بأكمله آمنًا", + "launch_screen4_header": "أنت المسيطرة بالكامل. يتم حفظ البيانات فقط على هاتفك", + "launch_screen4_subheader": "إذا كان الاختبار إيجابيًا ، انت فقط يمكنك اختيار المشاركة", + "launch_set_up_phone": "إعداد هاتفي", + "legal_page_title": "النظام القانوني", + "less_than_one_minute": "أقل من دقيقة واحدة", + "loading_public_data": "جاري تحميل البيانات...", + "location_disabled_message": "يتطلب تطبيق COVID Safe Paths خدمات الموقع", + "location_disabled_title": "تم تعطيل خدمة تتبع الموقع", + "location_enabled_message": "يقوم تطبيق COVID Safe Paths بتخزين إحداثيات موقعك بأمان مرة واحدة كل خمس دقائق على هذا الجهاز", + "location_enabled_title": "تم تفعيل تطبيق COVID Safe Paths", + "maps_import_button_text": "تحميل المواقع المتعقبة السابقة", + "maps_import_disclaimer": "لا يرتبط تطبيق Safe Paths بـ Google ولا يشارك بياناتك مطلقًا", + "maps_import_text": "لمعرفة ما إذا كنت قد تعرفت على شخص لديه COVID-19 قبل تحميل هذا التطبيق ، يمكنك استيراد سجل موقعك الشخصي", + "maps_import_title": "خرائط جوجل", + "nCoV2019_url_info": "لمزيد من المعلومات حول مجموعة البيانات لهذه الخريطة", + "news_subtitle": "اقرأ عن آخر تحديثات COVID من السلطات الصحية الخاصة بك وبشكل عام", + "news_title": "أحدث الأخبار", + "no_data": "لا يوجد بيانات", + "notification_2_weeks_ago": "قبل اسبوعين", + "notification_data_not_available": "لا توجد بيانات تعرض متاحة", + "notification_select_authority": "حدد هيئة الرعاية الصحية", + "notification_title": "الملف التعريفي للتعرض لمدة أسبوعين", + "notification_today": "اليوم", + "notification_warning_text": "في حالة وجود هيئة رعاية صحية في منطقتك ، يمكنك الاشتراك للحصول على تحديثات منتظمة لمخاطر التعرض", + "notifications_exposure_format": "قبل {{daysAgo}} يومًا ، تعرّضت لشخص معدي لمدة {{exposureTime}} دقيقة", + "notifications_exposure_format_today": "لقد تعرّضت اليوم لشخص معدي لمدة {{exposureTime}} دقيقة", + "notifications_exposure_format_yesterday": "لقد تعرّضت أمس لشخص معدي لمدة {{exposureTime}} دقيقة", + "notifications_no_exposure": "لم يتم التعرض لـ COVID-19 خلال الأسبوعين الماضيين", + "overlap_found_button_label": "تم تحميل البيانات العامة", + "overlap_no_results_button_label": "تم تحميل البيانات العامة", + "overlap_para_1": "يمثل الممر الأخضر سجل موقعك \n \n تمثل الدوائر ذات اللون الأرجواني الفاتح مجموعة البيانات العامة", + "overlap_title": "تحقق من التداخل", + "push_at_risk_message": "لقد عبرت بجانب مريض COVID-19", + "push_at_risk_title": "قد تكون في خطر", + "see_exposure_history": "إظهار بيانات التعرض", + "settings_title": "لوحة القيادة", + "share_location_data": "مشاركة بيانات تتبع الموقع", + "show_overlap": "انقر لعرض مجموعة البيانات العامة", + "team": "الفريق", + "team_para": "يتكون فريقنا من مجموعة من علماء الأوبئة والمهندسين وعلماء البيانات وأخصائيين الخصوصية الرقمية والأساتذة والباحثين من المؤسسات المرموقة ، بما في ذلك: MIT و Harvard و The Mayo Clinic و TripleBlind و EyeNetra و Ernst & Young و Link Ventures", + "terms_of_use": "تعليمات الاستخدام", + "terms_of_use_url": "https://docs.google.com/document/d/1mtdal_pywsKZVMXLHjjj5eKznipPLP8sM1HwFTIhjo0/edit#", + "tested_positive_subtitle": "يمكن نقل بياناتك الخاصة إلى السلطات الصحية أو نسخها احتياطيًا أو مشاركتها بأي طريقة أخرى", + "tested_positive_title": "مشاركة سجل تتبع الموقع", + "logging_active": " خدمة تتبع الموقع مفعلة", + "logging_inactive": "خدمة تتبع الموقع غير مفعلة", + "language_change_alert_title": "يحتاج التطبيق إلى إعادة التشغيل لاستخدام هذه اللغة", + "language_change_alert_proced": "إعادة التشغيل", + "language_change_alert_cancel": "إلغاء" + }, + "history": { + "no_exposure": "لم يتم التعرض", + "possible_exposure_para": "من الممكن أن تكون قد تعرضت من شخص مصاب بمرض COVID-19 أو كنت قريبًا منه", + "possible_exposure": "التعرض محتمل", + "timeline": "الجدول الزمني", + "what_does_this_mean_para": "استنادًا إلى سجل نظام تحديد المواقع العالمي (GPS) ، من المحتمل أنك قد تكون تعرضت أو قريب لشخص تم تشخيصه بـ COVID19. هذا لا يعني أنك مصاب ولكن قد تكون مصابًا. \n \n لمزيد من المعلومات حول ما يجب عليك القيام به ، يمكنك الرجوع إلى موقع Mayo Clinic الإلكتروني.", + "what_does_this_mean": "ماذا يعني هذا؟", + "what_if_no_symptoms_para": "إذا لم يكن لديك أعراض ولكنك لا تزال ترغب في الاختبار ، يمكنك الذهاب إلى أقرب موقع اختبار. \n \n يمكن للأفراد الذين لا تظهر عليهم الأعراض أحيانًا حمل العدوى وإصابة الآخرين. إن توخي الحذر بشأن التباعد الاجتماعي والتواصل مع مجموعات كبيرة أو الأفراد المعرضين للخطر (كبار السن ، والذين يعانون من مشاكل طبية أخرى مهمة) مهم لإدارة كل من المخاطر الخاصة بك والمخاطر التي يتعرض لها الآخرون.", + "what_if_no_symptoms": "ماذا لو لم تظهر الأعراض؟" + } +} diff --git a/app/locales/en.json b/app/locales/en.json index b1d95289c5..77a4bae8c4 100644 --- a/app/locales/en.json +++ b/app/locales/en.json @@ -1,4 +1,13 @@ { + "history": { + "no_exposure": "No exposure", + "possible_exposure": "Possible exposure", + "possible_exposure_para": "It is possible you were in contact with or close to someone who tested positive for COVID-19", + "what_does_this_mean": "What does this mean?", + "what_does_this_mean_para": "Based on your GPS history, it is possible that you may have been in contact with or close to somebody who was diagnosed with COVID-19. This does not mean you are infected but that you might be.\n\nFor further information on what you should do you can refer to the Mayo Clinic’s website.", + "what_if_no_symptoms": "What if I’m not showing symptoms?", + "what_if_no_symptoms_para": "If you have no symptoms but still would like to be tested you can go to your nearest testing site.\n\nIndividuals who don't exhibit symptoms can sometimes still carry the infection and infect others. Being careful about social distancing and coming in contact with large groups or at risk individuals (the elderly, those with significant other medical issues) is important to manage both your risk and the risk to others." + }, "label": { "about_title": "About", "authorities_add_button_label": "Add Trusted Source", @@ -28,20 +37,15 @@ "home_mayo_link_label": "from the Mayo Clinic", "home_no_contact_header": "No known contact", "home_no_contact_subtext": "Based on available data you haven’t been near anyone reported positive for COVID-19.", - "home_unknown_header": "Unknown", - "home_unknown_subtext": "We can’t tell if you’re at risk unless you enable the app to access your location.", "home_setting_off_header": "Unknown", "home_setting_off_subtext": "We can’t tell if you’re at risk unless you enable location history in the settings screen.", - "import_title":"Import Locations", - "import_step_1":"Adding location data from Google will give you a head start on building your recent locations.", - "import_step_2":"Before you can import, you must first 'Take out' your location data from Google.", - "import_step_3":"Visit Google Takeout and export your Location History using following settings: \n1. Delivery method: \"Add to Drive\" \n2. Frequency: \"Export once\" \n3. File type & size: \".zip\" and \"1GB\" \n4. Google sends an email when the export is ready \n5. Return here to import locations. Import options: \n- Import from Google Drive \n- Download from browser, then import from local phone files. Make sure to be on WiFi network as files can be big.", - "import_takeout":"Visit Google Takeout", - "import_success":"Recent locations has been successfully imported!", - "import_already_imported":"Provided Takeout file has already been imported.", - "import_error":"Something went wrong while importing your data.", - "import_no_recent_locations": "Takeout doesn't have any recent locations.", - "import_invalid_file_format": "Provided file format is not supported. \nSupported formats: \".zip\".", + "home_unknown_header": "Unknown", + "home_unknown_subtext": "We can’t tell if you’re at risk unless you enable the app to access your location.", + "import_step_1": "Adding location data from Google will give you a head start on building your recent locations.", + "import_step_2": "Before you can import, you must first 'Take out' your location data from Google.", + "import_step_3": "Visit Google Takeout and export your Location History using following settings: \n1. Delivery method: \"Add to Drive\" \n2. Frequency: \"Export once\" \n3. File type & size: \".zip\" and \"1GB\" \n4. Google sends an email when the export is ready \n5. Return here to import locations. Import options: \n- Import from Google Drive \n- Download from browser, then import from local phone files. Make sure to be on WiFi network as files can be big.", + "import_takeout": "Visit Google Takeout", + "import_title": "Import Locations", "latest_news": "Latest News", "launch_done_header": "All finished", "launch_done_subheader": "You’re ready to roll. Remember, you can always update your preferences later.", @@ -65,40 +69,25 @@ "launch_screen4_subheader": "If you test positive, you alone can choose whether to share.", "launch_set_up_phone": "Set up my phone", "legal_page_title": "Legal", - "less_than_one_minute": "less than 1 minute", "loading_public_data": "loading data...", "location_disabled_message": "COVID Safe Paths requires location services.", "location_disabled_title": "Location Tracking Was Disabled", "location_enabled_message": "COVID Safe Paths is securely storing your GPS coordinates once every five minutes on this device.", "location_enabled_title": "COVID Safe Paths Enabled", + "logging_active": "Location Active", + "logging_inactive": "Location Inactive", "maps_import_button_text": "Import past locations", "maps_import_disclaimer": "Safe Paths has no affiliation with Google and never shares your data.", "maps_import_text": "To see if you encountered someone with COVID-19 prior to downloading this app, you can import your personal location history.", "maps_import_title": "Google Maps", - "nCoV2019_url_info": "For more information on the dataset for this map", "news_subtitle": "Read about the latest COVID updates from your health authority and in general.", "news_title": "Latest news", "no_data": "No data", - "notification_2_weeks_ago": "2 weeks ago", - "notification_data_not_available": "No exposure data is available.", - "notification_select_authority": "Select Healthcare Authority", - "notification_title": "2-Week Exposure Profile", - "notification_today": "today", - "notification_warning_text": "If a Healthcare Authority exists in your area you can subscribe to obtain regular updates of exposure risks.", - "notifications_exposure_format": "{{daysAgo}} days ago you crossed paths someone infectious for {{exposureTime}} minutes.", - "notifications_exposure_format_today": "Today you crossed paths with someone infectious for {{exposureTime}} minutes.", - "notifications_exposure_format_yesterday": "Yesterday you crossed paths with someone infectious for {{exposureTime}} minutes.", - "notifications_no_exposure": "No known exposures to COVID-19 during the last two weeks.", - "overlap_found_button_label": "Public Data Loaded", - "overlap_no_results_button_label": "Public Data Loaded", - "overlap_para_1": "The green trail represents your location history\n\nThe light purple circles represent the public dataset", - "overlap_title": "Check Overlap", "push_at_risk_message": "You have crossed paths with a COVID-19 patient", "push_at_risk_title": "You may be at risk", "see_exposure_history": "See exposure history", "settings_title": "Dashboard", "share_location_data": "Share location data", - "show_overlap": "Click to view the public dataset", "team": "Team", "team_para": "Our team is composed of a consortium of epidemiologists, engineers, data scientists, digital privacy evangelists, professors and researchers from reputable institutions, including: MIT, Harvard, The Mayo Clinic, TripleBlind, EyeNetra, Ernst & Young and Link Ventures.", "terms_of_use": "Terms of use", @@ -106,16 +95,19 @@ "tested_positive_subtitle": "Your private data can be transferred to health authorities, backed up, or otherwise shared.", "tested_positive_title": "Share location history", "logging_active": "Location Active", - "logging_inactive": "Location Inactive" + "logging_inactive": "Location Inactive", + "language_change_alert_title": "App need to restart in order to use this language", + "language_change_alert_proced": "Restart", + "language_change_alert_cancel": "Cancel" }, "history": { - "no_exposure": "No exposure", + "no_exposure": "No known", "possible_exposure_para": "It is possible you were in contact with or close to someone who tested positive for COVID-19", - "possible_exposure": "Possible exposure", + "possible_exposure": "Possible", "timeline": "Timeline", "what_does_this_mean_para": "Based on your GPS history, it is possible that you may have been in contact with or close to somebody who was diagnosed with COVID-19. This does not mean you are infected but that you might be.\n\nFor further information on what you should do you can refer to the Mayo Clinic’s website.", "what_does_this_mean": "What does this mean?", "what_if_no_symptoms_para": "If you have no symptoms but still would like to be tested you can go to your nearest testing site.\n\nIndividuals who don't exhibit symptoms can sometimes still carry the infection and infect others. Being careful about social distancing and coming in contact with large groups or at risk individuals (the elderly, those with significant other medical issues) is important to manage both your risk and the risk to others.", "what_if_no_symptoms": "What if I’m not showing symptoms?" } -} +} \ No newline at end of file diff --git a/app/locales/languages.js b/app/locales/languages.js index 69bddef172..26161119f8 100644 --- a/app/locales/languages.js +++ b/app/locales/languages.js @@ -3,10 +3,12 @@ import './all-dayjs-locales'; import dayjs from 'dayjs'; import i18next from 'i18next'; import { initReactI18next } from 'react-i18next'; +import { useTranslation } from 'react-i18next'; import { NativeModules, Platform } from 'react-native'; import { LANG_OVERRIDE } from '../constants/storage'; import { GetStoreData, SetStoreData } from '../helpers/General'; +import ar from './ar.json'; import en from './en.json'; import es from './es.json'; import fr from './fr.json'; @@ -33,23 +35,14 @@ import zh_Hant from './zh-Hant.json'; // 5. REMOVE all empty translations. e.g. "key": "", this will allow fallback to the default: English // 6. import xyIndex from `./xy.json` and add the language to the block at the bottom -// detect and set device locale to i18n and dates -setLocale(getDeviceLocale()); - -// detect user override and set i18n and date locales -getUserLocaleOverride().then(locale => locale && setLocale(locale)); - /** Fetch the user language override, if any */ export async function getUserLocaleOverride() { return await GetStoreData(LANG_OVERRIDE); } -/** Get the device locale e.g. en_US */ -export function getDeviceLocale() { - return Platform.OS === 'ios' - ? NativeModules.SettingsManager.settings.AppleLocale || // iOS < 13 - NativeModules.SettingsManager.settings.AppleLanguages[0] // iOS 13 - : NativeModules.I18nManager.localeIdentifier; // Android +export function getLanguageFromLocale(locale) { + const [languageCode] = toIETFLanguageTag(locale).split('-'); + return languageCode; } /** @@ -66,41 +59,45 @@ async function setLocale(locale) { return await i18next.changeLanguage(locale); } +export function useLanguageDirection() { + const { i18n } = useTranslation(); + return i18n.dir(); +} + export async function setUserLocaleOverride(locale) { await setLocale(locale); - if (locale === getDeviceLocale()) { + if (locale === supportedDeviceLanguageOrEnglish()) { locale = undefined; } await SetStoreData(LANG_OVERRIDE, locale); } -i18next - // so that prettier lists plugins on single line - .use(initReactI18next) - .init({ - interpolation: { - // React already does escaping - escapeValue: false, - }, - lng: 'en', // 'en' | 'es', - fallbackLng: 'en', // If language detector fails - resources: { - en: { label: 'English', translation: en }, - es: { label: 'Español', translation: es }, - fr: { label: 'Français', translation: fr }, - ht: { label: 'Kreyòl ayisyen', translation: ht }, - id: { label: 'Indonesia', translation: id }, - it: { label: 'Italiano', translation: it }, - ml: { label: 'മലയാളം', translation: ml }, - nl: { label: 'Nederlands', translation: nl }, - pl: { label: 'Polski', translation: pl }, - ro: { label: 'Română', translation: ro }, - ru: { label: 'Русский', translation: ru }, - sk: { label: 'Slovak', translation: sk }, - vi: { label: 'Vietnamese', translation: vi }, - zh_Hant: { label: '繁體中文', translation: zh_Hant }, - }, - }); +i18next.use(initReactI18next).init({ + interpolation: { + // React already does escaping + escapeValue: false, + }, + lng: 'en', // 'en' | 'es', + fallbackLng: 'en', // If language detector fails + returnEmptyString: false, + resources: { + ar: { label: 'العربية', translation: ar }, + en: { label: 'English', translation: en }, + es: { label: 'Español', translation: es }, + fr: { label: 'Français', translation: fr }, + ht: { label: 'Kreyòl ayisyen', translation: ht }, + id: { label: 'Indonesia', translation: id }, + it: { label: 'Italiano', translation: it }, + ml: { label: 'മലയാളം', translation: ml }, + nl: { label: 'Nederlands', translation: nl }, + pl: { label: 'Polski', translation: pl }, + ro: { label: 'Română', translation: ro }, + ru: { label: 'Русский', translation: ru }, + sk: { label: 'Slovak', translation: sk }, + vi: { label: 'Vietnamese', translation: vi }, + zh_Hant: { label: '繁體中文', translation: zh_Hant }, + }, +}); /** The known locale list */ export const LOCALE_LIST = Object.entries(i18next.options.resources).map( @@ -119,4 +116,33 @@ export const LOCALE_NAME = Object.entries(i18next.options.resources).reduce( {}, ); +/** Get the device locale e.g. en_US */ +export function getDeviceLocale() { + return Platform.OS === 'ios' + ? NativeModules.SettingsManager.settings.AppleLocale || // iOS < 13 + NativeModules.SettingsManager.settings.AppleLanguages[0] // iOS 13 + : NativeModules.I18nManager.localeIdentifier; // Android +} + +/** + * Find compatible supported i18n language + * + * e.g. device locale `en_AU` would find `en` + * device locale `pt_BR` would find `pt-BR` + */ +export function supportedDeviceLanguageOrEnglish() { + const locale = getDeviceLocale(); // en_US + const langCode = getLanguageFromLocale(locale); // en + const found = Object.keys(LOCALE_NAME).find( + l => l === langCode || toIETFLanguageTag(l) === toIETFLanguageTag(locale), + ); + return found || 'en'; +} + +// detect and set device locale, must go after i18next.init() +setLocale(supportedDeviceLanguageOrEnglish()); + +// detect user override +getUserLocaleOverride().then(locale => locale && setLocale(locale)); + export default i18next; diff --git a/app/locales/nl.json b/app/locales/nl.json index c9f1a060b3..78d8f888c9 100644 --- a/app/locales/nl.json +++ b/app/locales/nl.json @@ -91,7 +91,6 @@ "team": "Team", "team_para": "Ons team bestaat uit een consortium van epidemiologen, ingenieurs, data wetenschappers, digitale privacy-evangelisten, professoren en onderzoekers van gerenommeerde instellingen, waaronder: MIT, Harvard, The Mayo Clinic, TripleBlind, EyeNetra, Ernst & Young en Link Ventures.", "terms_of_use": "Gebruiksvoorwaarden", - "tested_positive_subtitle": "", "tested_positive_title": "Deel locatie geschiedenis" } } diff --git a/app/services/LocationService.js b/app/services/LocationService.js index 0bf8e787fb..da3a52e212 100644 --- a/app/services/LocationService.js +++ b/app/services/LocationService.js @@ -4,7 +4,7 @@ import PushNotification from 'react-native-push-notification'; import { LOCATION_DATA, PARTICIPATE } from '../constants/storage'; import { GetStoreData, SetStoreData } from '../helpers/General'; -import { isLocationsNearby as areLocationsNearby } from '../helpers/Intersect'; +import { areLocationsNearby } from '../helpers/Intersect'; import languages from '../locales/languages'; import { isPlatformAndroid } from '../Util'; diff --git a/app/views/About.js b/app/views/About.js index fcb86eaade..1c96703c43 100644 --- a/app/views/About.js +++ b/app/views/About.js @@ -17,6 +17,7 @@ import fontFamily from './../constants/fonts'; import languages from './../locales/languages'; import lock from '../assets/svgs/lock'; import NavigationBarWrapper from '../components/NavigationBarWrapper'; +import { Typography } from '../components/Typography'; import Colors from '../constants/colors'; import { DEBUG_MODE } from '../constants/storage'; import { GetStoreData } from '../helpers/General'; @@ -84,12 +85,12 @@ class AboutScreen extends Component { - + {languages.t('label.commitment')} - - + + {languages.t('label.commitment_para')} - + {this.state.tapCount > 3 && ( - In exposure demo mode, tap to disable - + )} - + {languages.t('label.team')} - - + + {languages.t('label.team_para')} - + - Version: - {packageJson.version} + + Version:{' '} + + + {packageJson.version} + - OS: - + OS: + {' '} {Platform.OS + ' v' + Platform.Version} - + - Dimensions: - + + Dimensions: + + {' '} {Math.trunc(Dimensions.get('screen').width) + ' x ' + Math.trunc(Dimensions.get('screen').height)} - + diff --git a/app/views/ChooseProvider.js b/app/views/ChooseProvider.js index 31ff7712ad..4380ca4e0e 100644 --- a/app/views/ChooseProvider.js +++ b/app/views/ChooseProvider.js @@ -8,8 +8,6 @@ import { Image, SafeAreaView, StyleSheet, - Text, - TextInput, TouchableOpacity, View, } from 'react-native'; @@ -26,7 +24,9 @@ import RNFetchBlob from 'rn-fetch-blob'; import backArrow from './../assets/images/backArrow.png'; import closeIcon from './../assets/images/closeIcon.png'; import saveIcon from './../assets/images/saveIcon.png'; +import { DynamicTextInput } from '../components/DynamicTextInput'; import NavigationBarWrapper from '../components/NavigationBarWrapper'; +import { Typography } from '../components/Typography'; import { AUTHORITIES_LIST_URL } from '../constants/authorities'; import colors from '../constants/colors'; import Colors from '../constants/colors'; @@ -226,18 +226,18 @@ class ChooseProviderScreen extends Component { title={languages.t('label.choose_provider_title')} onBackPress={this.backToMain.bind(this)}> - + {languages.t('label.authorities_title')} - - + + {languages.t('label.authorities_desc')} - + {Object.keys(this.state.selectedAuthorities).length == 0 ? ( <> - {languages.t('label.authorities_no_sources')} - + - { this.setState({ urlText: text, @@ -283,7 +283,7 @@ class ChooseProviderScreen extends Component { styles.flatlistRowView, { display: this.state.displayUrlEntry }, ]}> - { this.setState({ urlText: text, @@ -306,7 +306,7 @@ class ChooseProviderScreen extends Component { data={this.state.selectedAuthorities} renderItem={({ item }) => ( - {item.key} + {item.key} this.removeAuthorityFromState(item)}> @@ -329,9 +329,9 @@ class ChooseProviderScreen extends Component { this.props.ctx.menuActions.openMenu('AuthoritiesMenu') } disabled={this.state.urlEditInProgress}> - + {languages.t('label.authorities_add_button_label')} - + @@ -348,7 +348,9 @@ class ChooseProviderScreen extends Component { this.addAuthorityToState(name); }} disabled={this.state.authoritiesList.length === 1}> - {name} + + {name} + ); })} @@ -359,9 +361,9 @@ class ChooseProviderScreen extends Component { urlEntryInProgress: true, }); }}> - + {languages.t('label.authorities_add_url')} - + @@ -389,7 +391,7 @@ const styles = StyleSheet.create({ alignSelf: 'center', }, listContainer: { - flex: 3, + flex: 1, flexDirection: 'column', textAlignVertical: 'top', justifyContent: 'flex-start', @@ -452,8 +454,8 @@ const styles = StyleSheet.create({ width: 18.48, }, sectionDescription: { - fontSize: 18, - lineHeight: 24, + fontSize: 16, + lineHeight: 22, marginTop: 12, overflow: 'scroll', color: Colors.VIOLET_TEXT, diff --git a/app/views/Export.js b/app/views/Export.js index a023b0bcad..d37dcb5a93 100644 --- a/app/views/Export.js +++ b/app/views/Export.js @@ -20,6 +20,7 @@ import RNFetchBlob from 'rn-fetch-blob'; import close from './../assets/svgs/close'; import exportIcon from './../assets/svgs/export'; import { isPlatformiOS } from './../Util'; +import { Typography } from '../components/Typography'; import Colors from '../constants/colors'; import fontFamily from '../constants/fonts'; import { LocationData } from '../services/LocationService'; @@ -128,20 +129,20 @@ function ExportScreen({ navigation }) { - + {t('label.tested_positive_title')} - - + + {t('label.export_para_1')} - - + + {t('label.export_para_2')} - + - + {t('label.share_location_data')} - + diff --git a/app/views/ExposureHistory/__tests__/__snapshots__/SingleExposureDetails.spec.js.snap b/app/views/ExposureHistory/__tests__/__snapshots__/SingleExposureDetails.spec.js.snap index 62a93e96ef..553aa079a5 100644 --- a/app/views/ExposureHistory/__tests__/__snapshots__/SingleExposureDetails.spec.js.snap +++ b/app/views/ExposureHistory/__tests__/__snapshots__/SingleExposureDetails.spec.js.snap @@ -113,7 +113,7 @@ exports[`matches snapshot 1`] = ` } } > - Possible exposure + Possible { onBackPress={goBack}> - + {languages.t('label.import_step_1')} - - + + {languages.t('label.import_step_2')} - - + + {languages.t('label.import_step_3')} - + @@ -84,27 +84,27 @@ const ImportScreen = props => { ) } style={styles.buttonTouchable}> - + {languages.t('label.import_takeout').toUpperCase()} - + - + {languages.t('label.import_title').toUpperCase()} - + {importResults.label ? ( - {languages.t(importResults.label)} - + ) : null} diff --git a/app/views/Licenses.js b/app/views/Licenses.js index a0d15590d1..85bee24222 100644 --- a/app/views/Licenses.js +++ b/app/views/Licenses.js @@ -1,13 +1,10 @@ import React, { Component } from 'react'; import { BackHandler, - Dimensions, Image, Linking, - Platform, ScrollView, StyleSheet, - Text, TouchableOpacity, View, } from 'react-native'; @@ -17,6 +14,7 @@ import foreArrow from './../assets/images/foreArrow.png'; import licenses from './../assets/LICENSE.json'; import languages from './../locales/languages'; import NavigationBarWrapper from '../components/NavigationBarWrapper'; +import { Typography } from '../components/Typography'; import Colors from '../constants/colors'; import fontFamily from '../constants/fonts'; @@ -87,13 +85,13 @@ class LicensesScreen extends Component { onPress={this.handleTermsOfUsePressed.bind(this)} style={styles.termsInfoRow}> - Linking.openURL(languages.t('label.terms_of_use_url')) }> {languages.t('label.terms_of_use')} - + @@ -156,6 +154,7 @@ const styles = StyleSheet.create({ arrowContainer: { alignSelf: 'center', paddingRight: 20, + paddingLeft: 20, }, }); diff --git a/app/views/LocationTracking.js b/app/views/LocationTracking.js index da5de781d3..329f41cd12 100644 --- a/app/views/LocationTracking.js +++ b/app/views/LocationTracking.js @@ -32,6 +32,7 @@ import StateNoContact from './../assets/svgs/stateNoContact'; import StateUnknown from './../assets/svgs/stateUnknown'; import { isPlatformAndroid, isPlatformiOS } from './../Util'; import ButtonWrapper from '../components/ButtonWrapper'; +import { Typography } from '../components/Typography'; import Colors from '../constants/colors'; import fontFamily from '../constants/fonts'; import { CROSSED_PATHS, DEBUG_MODE, PARTICIPATE } from '../constants/storage'; @@ -339,21 +340,21 @@ class LocationTracking extends Component { switch (this.state.currentState) { case StateEnum.NO_CONTACT: return ( - + {languages.t('label.home_no_contact_header')} - + ); case StateEnum.AT_RISK: return ( - + {languages.t('label.home_at_risk_header')} - + ); case StateEnum.UNKNOWN: return ( - + {languages.t('label.home_unknown_header')} - + ); case StateEnum.SETTING_OFF: return ( @@ -444,12 +445,16 @@ class LocationTracking extends Component { {this.state.currentState === StateEnum.AT_RISK && this.getMainText()} - {this.getSubSubText()} + + {this.getSubSubText()} + {this.state.currentState !== StateEnum.AT_RISK && this.getMainText()} - {this.getSubText()} + + {this.getSubText()} + {this.getCTAIfNeeded()} @@ -459,16 +464,16 @@ class LocationTracking extends Component { onPress={this.getMayoInfoPressed.bind(this)} style={styles.mayoInfoRow}> - Linking.openURL(MAYO_COVID_URL)}> {languages.t('label.home_mayo_link_heading')} - - + Linking.openURL(MAYO_COVID_URL)}> {languages.t('label.home_mayo_link_label')} - + @@ -592,6 +597,7 @@ const styles = StyleSheet.create({ arrowContainer: { alignSelf: 'center', paddingRight: 20, + paddingLeft: 20, }, }); diff --git a/app/views/News.js b/app/views/News.js index 568f8c9659..bf5224cc9e 100644 --- a/app/views/News.js +++ b/app/views/News.js @@ -13,6 +13,7 @@ import { WebView } from 'react-native-webview'; import languages from './../locales/languages'; import NavigationBarWrapper from '../components/NavigationBarWrapper'; +import { Typography } from '../components/Typography'; import Colors from '../constants/colors'; import fontFamily from '../constants/fonts'; import { AUTHORITY_NEWS } from '../constants/storage'; @@ -57,7 +58,9 @@ class NewsScreen extends Component { return ( - {item.item.name} + + {item.item.name} + { const { t } = useTranslation(); - const [isLogging, setIsLogging] = useState(undefined); - const [userLocale, setUserLocale] = useState(getDeviceLocale()); const backToMain = () => { navigation.goBack(); @@ -37,6 +34,12 @@ export const SettingsScreen = ({ navigation }) => { return true; }; + const [isLogging, setIsLogging] = useState(undefined); + + const [userLocale, setUserLocale] = useState( + supportedDeviceLanguageOrEnglish(), + ); + useEffect(() => { BackHandler.addEventListener('hardwareBackPress', handleBackPress); @@ -46,7 +49,7 @@ export const SettingsScreen = ({ navigation }) => { .catch(error => console.log(error)); // TODO: extract into service or hook - getUserLocaleOverride().then(locale => setUserLocale(locale)); + getUserLocaleOverride().then(locale => locale && setUserLocale(locale)); return () => { BackHandler.removeEventListener('hardwareBackPress', handleBackPress); @@ -88,25 +91,19 @@ export const SettingsScreen = ({ navigation }) => { icon={isLogging ? checkmarkIcon : xmarkIcon} onPress={locationToggleButtonPressed} /> - - {/* TODO: allow picker to render custom UI, remove need for negative - margins */} - - + + {({ label, openPicker }) => ( + + )} +
diff --git a/app/views/Settings/GoogleMapsImport.js b/app/views/Settings/GoogleMapsImport.js index c0ba85779b..ebd93e095e 100644 --- a/app/views/Settings/GoogleMapsImport.js +++ b/app/views/Settings/GoogleMapsImport.js @@ -12,9 +12,7 @@ export const GoogleMapsImport = ({ navigation }) => { const { t } = useTranslation(); const importPressed = () => { - if (__DEV__) { - navigation.navigate('ImportScreen'); - } + navigation.navigate('ImportScreen'); }; return ( @@ -29,7 +27,7 @@ export const GoogleMapsImport = ({ navigation }) => { { + + const { i18n } = useTranslation() + + let getCurrentMarginDirection = () => + i18n.dir() === 'rtl' ? 'margin-left: 12px;' : 'margin-right: 12px;'; + + let getCurrentRowDirection = () => + i18n.dir() === 'rtl' ? 'row-reverse' : 'row'; + return ( <> - - {icon && } + + {icon && }