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/i18n/i18n.ts b/boilerplate/app/i18n/i18n.ts index 34603e8e3..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" @@ -13,56 +13,51 @@ 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 systemLocales = Localization.getLocales() -const systemLocale = Localization.getLocales()[0] -const systemLocaleTag = systemLocale?.languageTag ?? fallbackLocale +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) + i18n.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. */ 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/models/Episode.test.ts b/boilerplate/app/models/Episode.test.ts index c33436e0c..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 {"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", ) }) diff --git a/boilerplate/app/utils/formatDate.ts b/boilerplate/app/utils/formatDate.ts index be5b89b96..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 { i18n } from "@/i18n" +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) } 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",