From 56918c5a6c83aa367e0fc9b66ae685faf8672e6f Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Thu, 26 Sep 2024 13:46:28 -0700 Subject: [PATCH 1/6] Update date-fns --- boilerplate/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/boilerplate/package.json b/boilerplate/package.json index b8dd2dea2..3fa686c53 100644 --- a/boilerplate/package.json +++ b/boilerplate/package.json @@ -38,7 +38,7 @@ "@react-navigation/native-stack": "^6.0.2", "@shopify/flash-list": "^1.6.4", "apisauce": "3.0.1", - "date-fns": "^2.30.0", + "date-fns": "^4.1.0", "expo": "~51.0.8", "expo-application": "~5.9.1", "expo-build-properties": "~0.12.1", From 6ad1264fb4663a6a40efd3b02d7cfbd9129e5175 Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Thu, 26 Sep 2024 13:50:58 -0700 Subject: [PATCH 2/6] Fix language loading with i18next - `hasOwnProperty` does not work with i18n.languages, as it's an array. It is also empty on init. - Create function to find first supported locale among devices locales. (I experimented with passing multiple languages to i18next with a language detector, but its fallback language is not ideal and will just fall back to the global `fallbackLng` instead of trying the second option in the list.) - Do a little more to ensure we only set RTL to true if the locale we end up picking is RTL, although it's still not perfectly guaranteed here, as i18next could fall back to `en` for some reason. The problem is that we either need to set RTL immediately, or restart the app if RTL setting needs to change. --- boilerplate/app/i18n/i18n.ts | 61 +++++++++++++++++------------------- 1 file changed, 29 insertions(+), 32 deletions(-) diff --git a/boilerplate/app/i18n/i18n.ts b/boilerplate/app/i18n/i18n.ts index 34603e8e3..011ccb398 100644 --- a/boilerplate/app/i18n/i18n.ts +++ b/boilerplate/app/i18n/i18n.ts @@ -13,56 +13,53 @@ import fr from "./fr" import ja from "./ja" import hi from "./hi" -// to use regional locales use { "en-US": enUS } etc const fallbackLocale = "en-US" export let i18n: i18next.i18n -const systemLocale = Localization.getLocales()[0] -const systemLocaleTag = systemLocale?.languageTag ?? fallbackLocale +const systemLocales = Localization.getLocales() + +const resources = { ar, en, ko, es, fr, ja, hi } +const supportedTags = Object.keys(resources) + +// Checks to see if the device locale matches any of the supported locales +// Device locale may be more specific and still match (e.g., en-US matches en) +const systemTagMatchesSupportedTags = (deviceTag: string) => { + const primaryTag = deviceTag.split("-")[0] + return supportedTags.includes(primaryTag) +} + +const pickSupportedLocale: () => Localization.Locale | undefined = () => { + return systemLocales.find((locale) => systemTagMatchesSupportedTags(locale.languageTag)) +} + +const locale = pickSupportedLocale() + +export let isRTL = false + +// Need to set RTL ASAP to ensure the app is rendered correctly. Waiting for i18n to init is too late. +if (locale?.languageTag && locale?.textDirection === "rtl") { + I18nManager.allowRTL(true) + isRTL = true +} else { + I18nManager.allowRTL(false) +} export const initI18n = async () => { i18n = i18next.use(initReactI18next) await i18n.init({ - resources: { - ar, - en, - "en-US": en, - ko, - es, - fr, - ja, - hi, - }, - lng: fallbackLocale, + resources, + lng: locale?.languageTag ?? fallbackLocale, fallbackLng: fallbackLocale, interpolation: { escapeValue: false, }, }) - if (Object.prototype.hasOwnProperty.call(i18n.languages, systemLocaleTag)) { - // if specific locales like en-FI or en-US is available, set it - await i18n.changeLanguage(systemLocaleTag) - } else { - // otherwise try to fallback to the general locale (dropping the -XX suffix) - const generalLocale = systemLocaleTag.split("-")[0] - if (Object.prototype.hasOwnProperty.call(i18n.languages, generalLocale)) { - await i18n.changeLanguage(generalLocale) - } else { - await i18n.changeLanguage(fallbackLocale) - } - } - return i18n } -// handle RTL languages -export const isRTL = systemLocale?.textDirection === "rtl" -I18nManager.allowRTL(isRTL) -I18nManager.forceRTL(isRTL) - /** * Builds up valid keypaths for translations. */ From c93a7be123ac66bebc3567e88ccc2df668a5fade Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Thu, 26 Sep 2024 13:57:01 -0700 Subject: [PATCH 3/6] Fix fast refresh issues with i18n --- boilerplate/app/i18n/i18n.ts | 6 ++---- boilerplate/app/i18n/translate.ts | 7 ++++--- boilerplate/app/utils/formatDate.ts | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/boilerplate/app/i18n/i18n.ts b/boilerplate/app/i18n/i18n.ts index 011ccb398..0cc03f500 100644 --- a/boilerplate/app/i18n/i18n.ts +++ b/boilerplate/app/i18n/i18n.ts @@ -1,6 +1,6 @@ import * as Localization from "expo-localization" import { I18nManager } from "react-native" -import * as i18next from "i18next" +import i18n from "i18next" import { initReactI18next } from "react-i18next" import "intl-pluralrules" @@ -15,8 +15,6 @@ import hi from "./hi" const fallbackLocale = "en-US" -export let i18n: i18next.i18n - const systemLocales = Localization.getLocales() const resources = { ar, en, ko, es, fr, ja, hi } @@ -46,7 +44,7 @@ if (locale?.languageTag && locale?.textDirection === "rtl") { } export const initI18n = async () => { - i18n = i18next.use(initReactI18next) + i18n.use(initReactI18next) await i18n.init({ resources, diff --git a/boilerplate/app/i18n/translate.ts b/boilerplate/app/i18n/translate.ts index aeb5a6a6d..22f99984c 100644 --- a/boilerplate/app/i18n/translate.ts +++ b/boilerplate/app/i18n/translate.ts @@ -1,10 +1,11 @@ -import { TOptions } from "i18next" -import { i18n, TxKeyPath } from "./i18n" +import i18n from "i18next" +import type { TOptions } from "i18next" +import { TxKeyPath } from "./i18n" /** * Translates text. * @param {TxKeyPath} key - The i18n key. - * @param {i18n.TOptions} options - The i18n options. + * @param {TOptions} options - The i18n options. * @returns {string} - The translated text. * @example * Translations: diff --git a/boilerplate/app/utils/formatDate.ts b/boilerplate/app/utils/formatDate.ts index be5b89b96..dfc3b9a51 100644 --- a/boilerplate/app/utils/formatDate.ts +++ b/boilerplate/app/utils/formatDate.ts @@ -8,7 +8,7 @@ import parseISO from "date-fns/parseISO" import ar from "date-fns/locale/ar-SA" import ko from "date-fns/locale/ko" import en from "date-fns/locale/en-US" -import { i18n } from "@/i18n" +import i18n from "i18next" type Options = Parameters[2] From 3d0d05a1f3ab1121b20f42693a04b6e7c49f1448 Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Thu, 26 Sep 2024 14:04:04 -0700 Subject: [PATCH 4/6] Update for date-fns 4 - Fix imports - Conditionally require locales - Add more locale support --- boilerplate/app/app.tsx | 5 +++- boilerplate/app/utils/formatDate.ts | 44 +++++++++++++++++++++-------- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/boilerplate/app/app.tsx b/boilerplate/app/app.tsx index dd7b7f2d1..28f5fef27 100644 --- a/boilerplate/app/app.tsx +++ b/boilerplate/app/app.tsx @@ -30,6 +30,7 @@ import * as storage from "./utils/storage" import { customFontsToLoad } from "./theme" import Config from "./config" import { KeyboardProvider } from "react-native-keyboard-controller" +import { loadDateFnsLocale } from "./utils/formatDate" export const NAVIGATION_PERSISTENCE_KEY = "NAVIGATION_STATE" @@ -75,7 +76,9 @@ function App(props: AppProps) { const [isI18nInitialized, setIsI18nInitialized] = useState(false) useEffect(() => { - initI18n().then(() => setIsI18nInitialized(true)) + initI18n() + .then(() => setIsI18nInitialized(true)) + .then(() => loadDateFnsLocale()) }, []) // @mst replace-next-line React.useEffect(() => { diff --git a/boilerplate/app/utils/formatDate.ts b/boilerplate/app/utils/formatDate.ts index dfc3b9a51..3bbcde0a7 100644 --- a/boilerplate/app/utils/formatDate.ts +++ b/boilerplate/app/utils/formatDate.ts @@ -2,26 +2,48 @@ // If you import with the syntax: import { format } from "date-fns" the ENTIRE library // will be included in your production bundle (even if you only use one function). // This is because react-native does not support tree-shaking. -import type { Locale } from "date-fns" -import format from "date-fns/format" -import parseISO from "date-fns/parseISO" -import ar from "date-fns/locale/ar-SA" -import ko from "date-fns/locale/ko" -import en from "date-fns/locale/en-US" +import { type Locale } from "date-fns/locale" +import { format } from "date-fns/format" +import { parseISO } from "date-fns/parseISO" import i18n from "i18next" type Options = Parameters[2] -const getLocale = (): Locale => { - const locale = i18n.language.split("-")[0] - return locale === "ar" ? ar : locale === "ko" ? ko : en +let dateFnsLocale: Locale +export const loadDateFnsLocale = () => { + const primaryTag = i18n.language.split("-")[0] + switch (primaryTag) { + case "en": + dateFnsLocale = require("date-fns/locale/en-US").default + break + case "ar": + dateFnsLocale = require("date-fns/locale/ar").default + break + case "ko": + dateFnsLocale = require("date-fns/locale/ko").default + break + case "es": + dateFnsLocale = require("date-fns/locale/es").default + break + case "fr": + dateFnsLocale = require("date-fns/locale/fr").default + break + case "hi": + dateFnsLocale = require("date-fns/locale/hi").default + break + case "ja": + dateFnsLocale = require("date-fns/locale/ja").default + break + default: + dateFnsLocale = require("date-fns/locale/en-US").default + break + } } export const formatDate = (date: string, dateFormat?: string, options?: Options) => { - const locale = getLocale() const dateOptions = { ...options, - locale, + locale: dateFnsLocale, } return format(parseISO(date), dateFormat ?? "MMM dd, yyyy", dateOptions) } From 9cacb264015d87c70f8d4274e46f6aa469c19933 Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Thu, 26 Sep 2024 14:31:06 -0700 Subject: [PATCH 5/6] Fix Episode test --- boilerplate/app/models/Episode.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/boilerplate/app/models/Episode.test.ts b/boilerplate/app/models/Episode.test.ts index c33436e0c..81b23c2e6 100644 --- a/boilerplate/app/models/Episode.test.ts +++ b/boilerplate/app/models/Episode.test.ts @@ -27,14 +27,14 @@ const episode = EpisodeModel.create(data) test("publish date format", () => { expect(episode.datePublished.textLabel).toBe("Jan 20, 2022") expect(episode.datePublished.accessibilityLabel).toBe( - 'demoPodcastListScreen:accessibility.publishLabel {"date":"Jan 20, 2022"}', + 'demoPodcastListScreen:accessibility.publishLabel', ) }) test("duration format", () => { expect(episode.duration.textLabel).toBe("42:58") expect(episode.duration.accessibilityLabel).toBe( - 'demoPodcastListScreen:accessibility.durationLabel {"hours":0,"minutes":42,"seconds":58}', + 'demoPodcastListScreen:accessibility.durationLabel', ) }) From 0beff0d78b997e4967a7427530f3cbd69941b920 Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Thu, 26 Sep 2024 14:48:25 -0700 Subject: [PATCH 6/6] lint fix --- boilerplate/app/models/Episode.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/boilerplate/app/models/Episode.test.ts b/boilerplate/app/models/Episode.test.ts index 81b23c2e6..635d72d04 100644 --- a/boilerplate/app/models/Episode.test.ts +++ b/boilerplate/app/models/Episode.test.ts @@ -27,14 +27,14 @@ const episode = EpisodeModel.create(data) test("publish date format", () => { expect(episode.datePublished.textLabel).toBe("Jan 20, 2022") expect(episode.datePublished.accessibilityLabel).toBe( - 'demoPodcastListScreen:accessibility.publishLabel', + "demoPodcastListScreen:accessibility.publishLabel", ) }) test("duration format", () => { expect(episode.duration.textLabel).toBe("42:58") expect(episode.duration.accessibilityLabel).toBe( - 'demoPodcastListScreen:accessibility.durationLabel', + "demoPodcastListScreen:accessibility.durationLabel", ) })