diff --git a/apps/desktop/changelog/next.md b/apps/desktop/changelog/next.md index 8f5eac449..c92e47a2a 100644 --- a/apps/desktop/changelog/next.md +++ b/apps/desktop/changelog/next.md @@ -2,10 +2,12 @@ ## Shiny new things +- Supported French localization. + ## Improvements ## No longer broken ## Thanks -Special thanks to volunteer contributors @ for their valuable contributions +Special thanks to volunteer contributors @AnthonyMahe for their valuable contributions diff --git a/apps/desktop/layer/renderer/src/@types/constants.ts b/apps/desktop/layer/renderer/src/@types/constants.ts index 2d5659421..a1ee08167 100644 --- a/apps/desktop/layer/renderer/src/@types/constants.ts +++ b/apps/desktop/layer/renderer/src/@types/constants.ts @@ -1,5 +1,5 @@ // DONT EDIT THIS FILE MANUALLY -const langs = ["en", "zh-CN", "zh-TW", "ja"] as const +const langs = ["en", "zh-CN", "zh-TW", "ja", "fr-FR"] as const export const currentSupportedLanguages = langs as readonly string[] export type RendererSupportedLanguages = (typeof langs)[number] @@ -8,6 +8,7 @@ export const dayjsLocaleImportMap = { ["zh-CN"]: ["zh-cn", () => import("dayjs/locale/zh-cn")], ["ja"]: ["ja", () => import("dayjs/locale/ja")], ["zh-TW"]: ["zh-tw", () => import("dayjs/locale/zh-tw")], + ["fr-FR"]: ["fr", () => import("dayjs/locale/fr")], } export const ns = ["common", "lang", "errors", "app", "settings", "shortcuts", "ai"] as const export const defaultNS = "app" as const diff --git a/apps/desktop/layer/renderer/src/@types/default-resource.electron.ts b/apps/desktop/layer/renderer/src/@types/default-resource.electron.ts index 11c07ff52..723e4c1ce 100644 --- a/apps/desktop/layer/renderer/src/@types/default-resource.electron.ts +++ b/apps/desktop/layer/renderer/src/@types/default-resource.electron.ts @@ -1,27 +1,34 @@ // DONT EDIT THIS FILE MANUALLY import ai_en from "@locales/ai/en.json" +import ai_frFR from "@locales/ai/fr-FR.json" import ai_ja from "@locales/ai/ja.json" import en from "@locales/app/en.json" +import app_frFR from "@locales/app/fr-FR.json" import app_ja from "@locales/app/ja.json" import app_zhCN from "@locales/app/zh-CN.json" import app_zhTW from "@locales/app/zh-TW.json" import common_en from "@locales/common/en.json" +import common_frFR from "@locales/common/fr-FR.json" import common_ja from "@locales/common/ja.json" import common_zhCN from "@locales/common/zh-CN.json" import common_zhTW from "@locales/common/zh-TW.json" import errors_en from "@locales/errors/en.json" +import errors_frFR from "@locales/errors/fr-FR.json" import errors_ja from "@locales/errors/ja.json" import errors_zhCN from "@locales/errors/zh-CN.json" import errors_zhTW from "@locales/errors/zh-TW.json" import lang_en from "@locales/lang/en.json" +import lang_frFR from "@locales/lang/fr-FR.json" import lang_ja from "@locales/lang/ja.json" import lang_zhCN from "@locales/lang/zh-CN.json" import lang_zhTW from "@locales/lang/zh-TW.json" import settings_en from "@locales/settings/en.json" +import settings_frFR from "@locales/settings/fr-FR.json" import settings_ja from "@locales/settings/ja.json" import settings_zhCN from "@locales/settings/zh-CN.json" import settings_zhTW from "@locales/settings/zh-TW.json" import shortcuts_en from "@locales/shortcuts/en.json" +import shortcuts_frFR from "@locales/shortcuts/fr-FR.json" import shortcuts_ja from "@locales/shortcuts/ja.json" import shortcuts_zhCN from "@locales/shortcuts/zh-CN.json" import shortcuts_zhTW from "@locales/shortcuts/zh-TW.json" @@ -70,6 +77,15 @@ export const defaultResources = { errors: errors_zhTW, ai: ai_en, // Fallback to English until Traditional Chinese translation is available }, + "fr-FR": { + app: app_frFR, + lang: lang_frFR, + common: common_frFR, + settings: settings_frFR, + shortcuts: shortcuts_frFR, + errors: errors_frFR, + ai: ai_frFR, + }, } satisfies Record< RendererSupportedLanguages, Partial>> diff --git a/apps/desktop/layer/renderer/src/@types/default-resource.ts b/apps/desktop/layer/renderer/src/@types/default-resource.ts index 946e97cdc..193d31b72 100644 --- a/apps/desktop/layer/renderer/src/@types/default-resource.ts +++ b/apps/desktop/layer/renderer/src/@types/default-resource.ts @@ -2,11 +2,13 @@ import ai_en from "@locales/ai/en.json" import en from "@locales/app/en.json" import common_en from "@locales/common/en.json" +import common_frFR from "@locales/common/fr-FR.json" import common_ja from "@locales/common/ja.json" import common_zhCN from "@locales/common/zh-CN.json" import common_zhTW from "@locales/common/zh-TW.json" import errors_en from "@locales/errors/en.json" import lang_en from "@locales/lang/en.json" +import lang_frFR from "@locales/lang/fr-FR.json" import lang_ja from "@locales/lang/ja.json" import lang_zhCN from "@locales/lang/zh-CN.json" import lang_zhTW from "@locales/lang/zh-TW.json" @@ -42,6 +44,7 @@ export const defaultResources = { common: common_ja, }, "zh-TW": { lang: lang_zhTW, common: common_zhTW }, + "fr-FR": { lang: lang_frFR, common: common_frFR }, } satisfies Record< RendererSupportedLanguages, Partial>> diff --git a/apps/desktop/layer/renderer/src/modules/discover/DiscoveryContent.tsx b/apps/desktop/layer/renderer/src/modules/discover/DiscoveryContent.tsx index d4d44783a..c8fc85498 100644 --- a/apps/desktop/layer/renderer/src/modules/discover/DiscoveryContent.tsx +++ b/apps/desktop/layer/renderer/src/modules/discover/DiscoveryContent.tsx @@ -18,13 +18,17 @@ const LanguageOptions = [ label: "words.english", value: "eng", }, + { + label: "words.french", + value: "fra", + }, { label: "words.chinese", value: "cmn", }, ] satisfies ResponsiveSelectItem[] -type Language = "all" | "eng" | "cmn" +type Language = "all" | "eng" | "cmn" | "fra" type DiscoveryView = "trending" | "categories" export function DiscoveryContent() { diff --git a/apps/desktop/layer/renderer/src/modules/trending/index.tsx b/apps/desktop/layer/renderer/src/modules/trending/index.tsx index 9fccb5668..231be9804 100644 --- a/apps/desktop/layer/renderer/src/modules/trending/index.tsx +++ b/apps/desktop/layer/renderer/src/modules/trending/index.tsx @@ -25,9 +25,13 @@ const LanguageOptions = [ label: "words.chinese", value: "cmn", }, + { + label: "words.french", + value: "fra", + }, ] -type Language = "all" | "eng" | "cmn" +type Language = "all" | "eng" | "cmn" | "fra" type View = "all" | string @@ -64,6 +68,7 @@ export function Trending({ const { t: tCommon } = useTranslation("common") const lang = useUISettingKey("discoverLanguage") const { onUpdateMaxScroll } = useScrollElementUpdate() + const trendingLanguage = lang === "fra" ? "eng" : lang const [selectedView, setSelectedView] = useState("all") const viewOptions = useMemo(() => buildViewOptions(), []) @@ -72,7 +77,7 @@ export function Trending({ queryKey: ["trending", lang, selectedView], queryFn: async () => { return await followClient.api.trending.getFeeds({ - language: lang === "all" ? undefined : lang, + language: trendingLanguage === "all" ? undefined : trendingLanguage, view: selectedView === "all" ? undefined : Number(selectedView), limit, }) diff --git a/apps/desktop/layer/renderer/src/pages/(main)/(layer)/(subview)/discover/category/[category].tsx b/apps/desktop/layer/renderer/src/pages/(main)/(layer)/(subview)/discover/category/[category].tsx index 6df48f2d7..2cb995238 100644 --- a/apps/desktop/layer/renderer/src/pages/(main)/(layer)/(subview)/discover/category/[category].tsx +++ b/apps/desktop/layer/renderer/src/pages/(main)/(layer)/(subview)/discover/category/[category].tsx @@ -26,6 +26,7 @@ const LanguageMap = { all: "all", eng: "en", cmn: "zh-CN", + fra: "fr-FR", } as const export const Component = () => { @@ -134,7 +135,7 @@ export const Component = () => { ) : items.length > 0 ? (
{ @@ -162,12 +163,10 @@ export const Component = () => {

- {t("common.noContent", { defaultValue: "No content found in this category" })} + {t("discover.empty.no_content", { ns: "common" })}

- {t("discover.tryAnotherCategory", { - defaultValue: "Try selecting another category or language", - })} + {t("discover.empty.try_another_category_or_language", { ns: "common" })}

)} diff --git a/apps/mobile/changelog/next.md b/apps/mobile/changelog/next.md index 000f858e3..255f6f190 100644 --- a/apps/mobile/changelog/next.md +++ b/apps/mobile/changelog/next.md @@ -2,10 +2,12 @@ ## Shiny new things +- Supported French localization. + ## Improvements ## No longer broken ## Thanks -Special thanks to volunteer contributors @ for their valuable contributions +Special thanks to volunteer contributors @AnthonyMahe for their valuable contributions diff --git a/apps/mobile/src/components/errors/GlobalErrorScreen.tsx b/apps/mobile/src/components/errors/GlobalErrorScreen.tsx index 332ec3433..c8f0ba87f 100644 --- a/apps/mobile/src/components/errors/GlobalErrorScreen.tsx +++ b/apps/mobile/src/components/errors/GlobalErrorScreen.tsx @@ -1,5 +1,6 @@ import { applicationName } from "expo-application" import * as React from "react" +import { useTranslation } from "react-i18next" import { TouchableOpacity, View } from "react-native" import { Text } from "@/src/components/ui/typography/Text" @@ -9,22 +10,23 @@ interface GlobalErrorScreenProps { resetError?: () => void } export const GlobalErrorScreen: React.FC = ({ error, resetError }) => { + const { t } = useTranslation("common") return ( 😕 - {applicationName} crashed! + {t("error_screen.crashed", { appName: applicationName ?? "App" })} - {error?.message || "An unexpected error occurred."} + {error?.message || t("error_screen.unexpected")} {resetError && ( - Try Again + {t("retry")} )} diff --git a/apps/mobile/src/components/errors/ListErrorView.tsx b/apps/mobile/src/components/errors/ListErrorView.tsx index c6613b81a..4f59cad03 100644 --- a/apps/mobile/src/components/errors/ListErrorView.tsx +++ b/apps/mobile/src/components/errors/ListErrorView.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from "react-i18next" import { TouchableOpacity, View } from "react-native" import { useColor } from "react-native-uikit-colors" @@ -7,6 +8,7 @@ import { AlertCuteFiIcon } from "@/src/icons/alert_cute_fi" import { MonoText } from "../ui/typography/MonoText" export const ListErrorView = ({ error, resetError }: { error: Error; resetError: () => void }) => { + const { t } = useTranslation("common") const red = useColor("red") return ( @@ -15,17 +17,17 @@ export const ListErrorView = ({ error, resetError }: { error: Error; resetError: - Unable to Load Content + {t("error_screen.list_unable_to_load")} - There was a problem loading the list. Please try again later. + {t("error_screen.list_try_later")} - {error.message || "Unknown Error"} + {error.message || t("error_screen.unknown")} - Try Again + {t("retry")}
diff --git a/apps/mobile/src/components/errors/ScreenErrorScreen.tsx b/apps/mobile/src/components/errors/ScreenErrorScreen.tsx index 6315ade76..f57b76c25 100644 --- a/apps/mobile/src/components/errors/ScreenErrorScreen.tsx +++ b/apps/mobile/src/components/errors/ScreenErrorScreen.tsx @@ -1,4 +1,5 @@ import * as React from "react" +import { useTranslation } from "react-i18next" import { TouchableOpacity, View } from "react-native" import { Text } from "@/src/components/ui/typography/Text" @@ -9,6 +10,7 @@ interface ScreenErrorScreenProps { resetError?: () => void } export const ScreenErrorScreen: React.FC = ({ error }) => { + const { t } = useTranslation("common") const navigation = useNavigation() const canGoBack = useCanBack() return ( @@ -16,10 +18,10 @@ export const ScreenErrorScreen: React.FC = ({ error }) = 😕 - This page went wrong, go back and try again. + {t("error_screen.page_failed")} - {error?.message || "An unexpected error occurred."} + {error?.message || t("error_screen.unexpected")} @@ -28,7 +30,9 @@ export const ScreenErrorScreen: React.FC = ({ error }) = className="min-w-[160px] rounded-xl bg-accent px-6 py-3" onPress={() => navigation.back()} > - Go Back + + {t("words.back")} + )} diff --git a/apps/mobile/src/modules/ai/summary.tsx b/apps/mobile/src/modules/ai/summary.tsx index 6881414f4..aef99f0e8 100644 --- a/apps/mobile/src/modules/ai/summary.tsx +++ b/apps/mobile/src/modules/ai/summary.tsx @@ -95,6 +95,7 @@ export const AISummary: FC<{ const upgradeTitle = t("ai.summary_upgrade_required_title") const upgradeDescription = t("ai.summary_upgrade_required_description") const upgradeCTA = t("ai.summary_upgrade_view_plans") + const summaryTitle = t("entry_actions.toggle_ai_summary") const handleUpgradePress = () => { void Haptics.selectionAsync() void navigateToPlanScreen() @@ -163,12 +164,12 @@ export const AISummary: FC<{ - AI Summary + {summaryTitle} } > - AI Summary + {summaryTitle} @@ -177,7 +178,7 @@ export const AISummary: FC<{ onPress={() => { Clipboard.setString(summaryTextForSheet) Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success) - toast.success("Copied to clipboard") + toast.success(t("app.copied_to_clipboard", { ns: "common" })) }} onLongPress={() => setSheetOpen(true)} className="rounded-full bg-quaternary-system-fill p-1.5 active:opacity-70" @@ -230,6 +231,7 @@ const ErrorContent = ({ message: string onRetry?: () => void }) => { + const { t } = useTranslation() return ( @@ -238,14 +240,18 @@ const ErrorContent = ({ {onRetry && (forMeasurement ? ( - Retry + + {t("retry", { ns: "common" })} + ) : ( - Retry + + {t("retry", { ns: "common" })} + ))} @@ -312,6 +318,7 @@ const SelectableTextSheet: FC<{ onClose: () => void text: string }> = ({ visible, onClose, text }) => { + const { t } = useTranslation() const insets = useSafeAreaInsets() const textColor = useColor("label") const handleCopyAll = () => { @@ -334,7 +341,9 @@ const SelectableTextSheet: FC<{ > - AI Summary + + {t("entry_actions.toggle_ai_summary")} + { + const { t } = useTranslation("common") const navigation = useNavigation() const label = useColor("label") return ( @@ -25,7 +26,9 @@ export const Category = () => { - Categories + + {t("words.categories")} + { ) } -const LanguageMap = { - all: "all", - eng: "en", - cmn: "zh-CN", -} as const export const RecommendationTab: TabComponent<{ contentContainerStyle?: StyleProp insets?: { @@ -151,11 +146,11 @@ export const RecommendationTab: TabComponent<{ insets: customInsets, ...rest }) => { + const { t } = useTranslation("common") const discoverLanguage = useUISettingKey("discoverLanguage") const { data, isLoading } = useQuery({ queryKey: ["rsshub-popular", tab.value, discoverLanguage], - queryFn: () => - fetchRsshubPopular(tab.value, LanguageMap[discoverLanguage]).then((res) => res.data), + queryFn: () => fetchRsshubPopular(tab.value, discoverLanguage).then((res) => res.data), staleTime: 1000 * 60 * 60 * 24, // 1 day meta: { @@ -164,7 +159,7 @@ export const RecommendationTab: TabComponent<{ }) const { data: analysisData, isLoading: isAnalysisLoading } = useQuery({ queryKey: ["rsshub-analysis", discoverLanguage], - queryFn: () => fetchRsshubAnalysis(LanguageMap[discoverLanguage]).then((res) => res.data), + queryFn: () => fetchRsshubAnalysis(discoverLanguage).then((res) => res.data), staleTime: 1000 * 60 * 60 * 24, // 1 day meta: { @@ -259,7 +254,7 @@ export const RecommendationTab: TabComponent<{ if (keys.length === 0) { return ( - This is a barren wasteland of knowledge. + {t("search.empty.no_results")} ) } diff --git a/apps/mobile/src/modules/discover/SearchTabBar.tsx b/apps/mobile/src/modules/discover/SearchTabBar.tsx index 1388e040d..37706e8aa 100644 --- a/apps/mobile/src/modules/discover/SearchTabBar.tsx +++ b/apps/mobile/src/modules/discover/SearchTabBar.tsx @@ -1,5 +1,7 @@ import { useAtom } from "jotai" import type { FC } from "react" +import { useMemo } from "react" +import { useTranslation } from "react-i18next" import type { Animated } from "react-native" import { TabBar } from "@/src/components/ui/tabview/TabBar" @@ -11,17 +13,26 @@ import { useSearchPageContext } from "./ctx" export const SearchTabBar: FC<{ animatedX: Animated.Value }> = ({ animatedX }) => { + const { t } = useTranslation("common") const { searchTypeAtom } = useSearchPageContext() const [searchType, setSearchType] = useAtom(searchTypeAtom) + const tabs = useMemo( + () => + SearchTabs.map((tab) => ({ + ...tab, + name: t(tab.name), + })), + [t], + ) return ( tab.value === searchType)} + tabs={tabs} + currentTab={tabs.findIndex((tab) => tab.value === searchType)} onTabItemPress={(index) => { - setSearchType(SearchTabs[index]!.value as SearchType) + setSearchType(tabs[index]!.value as SearchType) }} /> ) diff --git a/apps/mobile/src/modules/discover/Trending.tsx b/apps/mobile/src/modules/discover/Trending.tsx index 2f6e4a89e..1089e6da1 100644 --- a/apps/mobile/src/modules/discover/Trending.tsx +++ b/apps/mobile/src/modules/discover/Trending.tsx @@ -1,5 +1,6 @@ import { cn, formatNumber } from "@follow/utils" import { useQuery } from "@tanstack/react-query" +import { useTranslation } from "react-i18next" import { View } from "react-native" import { useColor } from "react-native-uikit-colors" @@ -24,13 +25,15 @@ export const Trending = ({ className?: string itemClassName?: string }) => { + const { t } = useTranslation("common") const label = useColor("label") const discoverLanguage = useUISettingKey("discoverLanguage") + const trendingLanguage = discoverLanguage === "fra" ? "eng" : discoverLanguage const { data, isLoading } = useQuery({ queryKey: ["trending", "feeds", discoverLanguage], queryFn: () => fetchFeedTrending({ - lang: discoverLanguage === "all" ? undefined : discoverLanguage, + lang: trendingLanguage === "all" ? undefined : trendingLanguage, limit: 20, }).then((res) => res.data), meta: { @@ -43,7 +46,9 @@ export const Trending = ({ - Trending + + {t("words.trending")} + { + const { t } = useTranslation("common") const { searchValueAtom } = useSearchPageContext() const searchValue = useAtomValue(searchValueAtom) const windowWidth = useWindowDimensions().width @@ -24,13 +26,18 @@ export const SearchFeed = () => { const skeleton = useDataSkeleton(isLoading, data) if (skeleton) return skeleton if (data === undefined) return null + const resultCount = data.data?.length ?? 0 + const resultLabel = + resultCount === 0 + ? t("discover.search.results_zero") + : t("discover.search.results_other", { count: resultCount }) return ( - Found {data.data?.length} feeds + {resultLabel} {data.data?.map((item) => ( diff --git a/apps/mobile/src/modules/discover/search-tabs/SearchFeedCard.tsx b/apps/mobile/src/modules/discover/search-tabs/SearchFeedCard.tsx index 7aece2de1..3bfe540a5 100644 --- a/apps/mobile/src/modules/discover/search-tabs/SearchFeedCard.tsx +++ b/apps/mobile/src/modules/discover/search-tabs/SearchFeedCard.tsx @@ -1,6 +1,7 @@ import { useSubscriptionByFeedId } from "@follow/store/subscription/hooks" import { formatNumber } from "@follow/utils" import type { DiscoveryItem, TrendingFeedItem } from "@follow-app/client-sdk" +import { useTranslation } from "react-i18next" import { View } from "react-native" import { RelativeDateTime } from "@/src/components/ui/datetime/RelativeDateTime" @@ -13,15 +14,17 @@ import { useColor } from "@/src/theme/colors" import { FeedSummary } from "../FeedSummary" export const SearchFeedCard = ({ item }: { item: TrendingFeedItem | DiscoveryItem }) => { + const { t } = useTranslation("common") const isSubscribed = useSubscriptionByFeedId(item.feed?.id ?? "") const iconColor = useColor("secondaryLabel") + const followerCount = item.analytics?.subscriptionCount || 0 return ( - {formatNumber(item.analytics?.subscriptionCount || 0)} followers + {formatNumber(followerCount)} {t("feed.follower_other")} @@ -29,13 +32,13 @@ export const SearchFeedCard = ({ item }: { item: TrendingFeedItem | DiscoveryIte <> - {item.analytics.updatesPerWeek} entries/week + {t("feed.entry_week_other", { count: item.analytics.updatesPerWeek })} ) : item.analytics?.latestEntryPublishedAt ? ( <> - Updated + {t("feed.updated_at")} {isSubscribed ? ( - Followed + + {t("feed.actions.followed")} + ) : ( - Follow + {t("feed.actions.follow")} )} diff --git a/apps/mobile/src/modules/discover/search-tabs/SearchList.tsx b/apps/mobile/src/modules/discover/search-tabs/SearchList.tsx index 5572019c3..2fffd12c4 100644 --- a/apps/mobile/src/modules/discover/search-tabs/SearchList.tsx +++ b/apps/mobile/src/modules/discover/search-tabs/SearchList.tsx @@ -3,6 +3,7 @@ import { formatNumber } from "@follow/utils" import { useQuery } from "@tanstack/react-query" import { useAtomValue } from "jotai" import { memo } from "react" +import { useTranslation } from "react-i18next" import { useWindowDimensions, View } from "react-native" import { FallbackIcon } from "@/src/components/ui/icon/fallback-icon" @@ -26,6 +27,7 @@ type SearchResultItem = Awaited< ReturnType >["data"][number] export const SearchList = () => { + const { t } = useTranslation("common") const { searchValueAtom } = useSearchPageContext() const searchValue = useAtomValue(searchValueAtom) const windowWidth = useWindowDimensions().width @@ -37,13 +39,18 @@ export const SearchList = () => { const skeleton = useDataSkeleton(isLoading, data) if (skeleton) return skeleton if (data === undefined) return null + const resultCount = data.data?.length ?? 0 + const resultLabel = + resultCount === 0 + ? t("discover.search.results_zero") + : t("discover.search.results_other", { count: resultCount }) return ( - Found {data.data?.length} lists + {resultLabel} {data.data?.map((item) => ( @@ -56,9 +63,11 @@ export const SearchList = () => { ) } const SearchListCard = memo(({ item }: { item: SearchResultItem }) => { + const { t } = useTranslation("common") const isSubscribed = useSubscriptionByListId(item.list?.id ?? "") const navigation = useNavigation() const iconColor = useColor("text") + const followerCount = item.analytics?.subscriptionCount || 0 return ( { {isSubscribed ? ( - Followed + {t("feed.actions.followed")} ) : ( - Follow + {t("feed.actions.follow")} )} @@ -126,7 +135,7 @@ const SearchListCard = memo(({ item }: { item: SearchResultItem }) => { - {formatNumber(item.analytics?.subscriptionCount || 0)} followers + {formatNumber(followerCount)} {t("feed.follower_other")} diff --git a/apps/mobile/src/modules/discover/search-tabs/hooks.tsx b/apps/mobile/src/modules/discover/search-tabs/hooks.tsx index 0a9c94a87..36153c110 100644 --- a/apps/mobile/src/modules/discover/search-tabs/hooks.tsx +++ b/apps/mobile/src/modules/discover/search-tabs/hooks.tsx @@ -1,5 +1,6 @@ import { withOpacity } from "@follow/utils" import { useMemo } from "react" +import { useTranslation } from "react-i18next" import { PlatformActivityIndicator } from "@/src/components/ui/loading/PlatformActivityIndicator" import { Text } from "@/src/components/ui/typography/Text" @@ -9,6 +10,7 @@ import { useColor } from "@/src/theme/colors" import { BaseSearchPageRootView } from "./__base" export const useDataSkeleton = (isLoading: boolean, data: any) => { + const { t } = useTranslation("common") const textColor = useColor("text") return useMemo(() => { if (isLoading) { @@ -22,10 +24,10 @@ export const useDataSkeleton = (isLoading: boolean, data: any) => { return ( - No results found + {t("search.empty.no_results")} ) } return null - }, [isLoading, data, textColor]) + }, [isLoading, data, t, textColor]) } diff --git a/apps/mobile/src/modules/entry-list/EntryListEmpty.tsx b/apps/mobile/src/modules/entry-list/EntryListEmpty.tsx index 3713be8ea..23bfbd57e 100644 --- a/apps/mobile/src/modules/entry-list/EntryListEmpty.tsx +++ b/apps/mobile/src/modules/entry-list/EntryListEmpty.tsx @@ -1,3 +1,4 @@ +import { useTranslation } from "react-i18next" import { View } from "react-native" import { useGeneralSettingKey } from "@/src/atoms/settings/general" @@ -6,6 +7,7 @@ import { CelebrateCuteReIcon } from "@/src/icons/celebrate_cute_re" import { useColor } from "@/src/theme/colors" export const EntryListEmpty = () => { + const { t } = useTranslation() const unreadOnly = useGeneralSettingKey("unreadOnly") const color = useColor("secondaryLabel") return ( @@ -13,10 +15,14 @@ export const EntryListEmpty = () => { {unreadOnly ? ( <> - Zero Unread + + {t("entry_list.zero_unread")} + ) : ( - No entries + + {t("search.empty.no_results", { ns: "common" })} + )} ) diff --git a/apps/mobile/src/modules/entry-list/templates/EntryPictureItem.tsx b/apps/mobile/src/modules/entry-list/templates/EntryPictureItem.tsx index 6ef497814..2c60a44c9 100644 --- a/apps/mobile/src/modules/entry-list/templates/EntryPictureItem.tsx +++ b/apps/mobile/src/modules/entry-list/templates/EntryPictureItem.tsx @@ -8,6 +8,7 @@ import { uniqBy } from "es-toolkit/compat" import type { ImageSource } from "expo-image" import type { Ref } from "react" import { useMemo } from "react" +import { useTranslation } from "react-i18next" import { View } from "react-native" import { measure, runOnJS, runOnUI, useAnimatedRef } from "react-native-reanimated" @@ -16,6 +17,7 @@ import { useLightboxControls } from "@/src/components/ui/lightbox/lightboxState" import { Text } from "@/src/components/ui/typography/Text" export function EntryPictureItem({ id }: { id: string }) { + const { t } = useTranslation() const { openLightbox } = useLightboxControls() const aviRef = useAnimatedRef() const item = useEntry(id, (state) => ({ @@ -36,7 +38,7 @@ export function EntryPictureItem({ id }: { id: string }) { aspectRatio: 16 / 9, }} > - No media available + {t("entry_content.no_content")} ) } diff --git a/apps/mobile/src/modules/entry-list/templates/EntryVideoItem.tsx b/apps/mobile/src/modules/entry-list/templates/EntryVideoItem.tsx index 9690ebacf..727daeb2f 100644 --- a/apps/mobile/src/modules/entry-list/templates/EntryVideoItem.tsx +++ b/apps/mobile/src/modules/entry-list/templates/EntryVideoItem.tsx @@ -4,6 +4,7 @@ import { unreadSyncService } from "@follow/store/unread/store" import { tracker } from "@follow/tracker" import { formatDuration, transformVideoUrl } from "@follow/utils" import { memo, useMemo } from "react" +import { useTranslation } from "react-i18next" import { Linking, View } from "react-native" import { getGeneralSettings } from "@/src/atoms/settings/general" @@ -18,6 +19,7 @@ import { VideoContextMenu } from "../../context-menu/video" import { EntryGridFooter } from "../../entry-content/EntryGridFooter" export const EntryVideoItem = memo(({ id }: { id: string }) => { + const { t } = useTranslation() const item = useEntry(id, (state) => ({ attachments: state.attachments, media: state.media, @@ -49,7 +51,7 @@ export const EntryVideoItem = memo(({ id }: { id: string }) => { entryId: id, }) if (!item.url) { - toast.error("No video URL found") + toast.error(t("entry_content.no_video_url")) return } openVideo(item.url) @@ -68,7 +70,7 @@ export const EntryVideoItem = memo(({ id }: { id: string }) => { }} /> ) : ( - + )} {!!duration && ( @@ -83,14 +85,14 @@ export const EntryVideoItem = memo(({ id }: { id: string }) => { ) }) EntryVideoItem.displayName = "EntryVideoItem" -const FallbackMedia = () => ( +const FallbackMedia = ({ text }: { text: string }) => ( - No media available + {text} ) const parseSchemeLink = (url: string) => { diff --git a/apps/mobile/src/modules/login/email.tsx b/apps/mobile/src/modules/login/email.tsx index fb24739b0..006e2d079 100644 --- a/apps/mobile/src/modules/login/email.tsx +++ b/apps/mobile/src/modules/login/email.tsx @@ -1,9 +1,11 @@ import { tracker } from "@follow/tracker" import { zodResolver } from "@hookform/resolvers/zod" import { useMutation } from "@tanstack/react-query" +import i18next from "i18next" import { useCallback, useRef } from "react" import type { Control } from "react-hook-form" import { useController, useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" import type { TextInputProps } from "react-native" import { Alert, TouchableOpacity, View } from "react-native" import { KeyboardController } from "react-native-keyboard-controller" @@ -30,7 +32,7 @@ async function onSubmit(values: FormValue) { const result = formSchema.safeParse(values) if (!result.success) { const issue = result.error.issues[0] - Alert.alert("Invalid email or password", issue?.message) + Alert.alert(i18next.t("login.invalid_email_or_password"), issue?.message) return } await signIn @@ -60,6 +62,7 @@ async function onSubmit(values: FormValue) { }) } export function EmailLogin() { + const { t } = useTranslation() const emailValueRef = useRef("") const passwordValueRef = useRef("") const submitMutation = useMutation({ @@ -86,7 +89,7 @@ export function EmailLogin() { autoCorrect={false} keyboardType="email-address" autoComplete="email" - placeholder="Email" + placeholder={t("login.email")} className="flex-1 text-text" returnKeyType="next" onSubmitEditing={() => { @@ -105,7 +108,7 @@ export function EmailLogin() { autoCapitalize="none" autoCorrect={false} autoComplete="current-password" - placeholder="Password" + placeholder={t("login.password")} className="flex-1 text-text" secureTextEntry returnKeyType="go" @@ -118,9 +121,13 @@ export function EmailLogin() { className="mx-auto my-5" onPress={() => navigation.presentControllerView(ForgetPasswordScreen)} > - Forgot password? + {t("login.forget_password.note")} - + ) } @@ -134,7 +141,7 @@ const signupFormSchema = z confirmPassword: z.string(), }) .refine((data) => data.password === data.confirmPassword, { - message: "Passwords don't match", + message: i18next.t("login.passwords_do_not_match"), path: ["confirmPassword"], }) type SignupFormValue = z.infer @@ -160,6 +167,7 @@ function SignupInput({ ) } export function EmailSignUp() { + const { t } = useTranslation() const { control, handleSubmit, formState } = useForm({ resolver: zodResolver(signupFormSchema), defaultValues: { @@ -185,7 +193,7 @@ export function EmailSignUp() { if (res.error?.message) { toast.error(res.error.message) } else { - toast.success("Sign up successful") + toast.success(i18next.t("login.sign_up_successful")) tracker.register({ type: "email", }) @@ -209,7 +217,7 @@ export function EmailSignUp() { autoComplete="email" control={control} name="email" - placeholder="Email" + placeholder={t("login.email")} className="flex-1 text-text" returnKeyType="next" onSubmitEditing={() => { @@ -226,7 +234,7 @@ export function EmailSignUp() { autoComplete="password-new" control={control} name="password" - placeholder="Password" + placeholder={t("login.password")} className="flex-1 text-text" secureTextEntry returnKeyType="next" @@ -241,7 +249,7 @@ export function EmailSignUp() { autoComplete="password-new" control={control} name="confirmPassword" - placeholder="Confirm Password" + placeholder={t("login.confirm_password.label")} className="flex-1 text-text" secureTextEntry returnKeyType="go" @@ -255,7 +263,7 @@ export function EmailSignUp() { disabled={submitMutation.isPending || !formState.isValid} isLoading={submitMutation.isPending} onPress={signup} - title="Continue" + title={t("login.submit")} className="mt-8" /> diff --git a/apps/mobile/src/modules/login/index.tsx b/apps/mobile/src/modules/login/index.tsx index 38a23e3fa..a5b3b30fe 100644 --- a/apps/mobile/src/modules/login/index.tsx +++ b/apps/mobile/src/modules/login/index.tsx @@ -114,24 +114,23 @@ const TermsCheckBox = () => { ) } const TermsText = () => { + const { t } = useTranslation() return ( - - By continuing, you agree to our{" "} - + {t("login.agree_to")} Linking.openURL("https://folo.is/terms-of-service")} className="text-secondary-label" > - Terms of Service + {t("login.terms")}  &  Linking.openURL("https://folo.is/privacy-policy")} className="text-secondary-label" > - Privacy Policy + {t("login.privacy")} diff --git a/apps/mobile/src/modules/settings/routes/General.tsx b/apps/mobile/src/modules/settings/routes/General.tsx index 89affac9f..ece48755c 100644 --- a/apps/mobile/src/modules/settings/routes/General.tsx +++ b/apps/mobile/src/modules/settings/routes/General.tsx @@ -28,7 +28,9 @@ export function LanguageSelect({ settingKey }: { settingKey: "language" | "actio const languageKeys = settingKey === "language" ? (currentSupportedLanguages as MobileSupportedLanguages[]) - : ACTION_LANGUAGE_KEYS.sort( + : ACTION_LANGUAGE_KEYS.filter( + (key): key is MobileSupportedLanguages => key in defaultResources, + ).sort( (a, b) => currentSupportedLanguages.indexOf(a) - currentSupportedLanguages.indexOf(b), ) diff --git a/apps/mobile/src/screens/(modal)/DiscoverSettingsScreen.tsx b/apps/mobile/src/screens/(modal)/DiscoverSettingsScreen.tsx index 88391cccf..505729489 100644 --- a/apps/mobile/src/screens/(modal)/DiscoverSettingsScreen.tsx +++ b/apps/mobile/src/screens/(modal)/DiscoverSettingsScreen.tsx @@ -15,6 +15,7 @@ import { export const DiscoverSettingsScreen = () => { const { t } = useTranslation("settings") + const { t: tCommon } = useTranslation("common") const discoverLanguage = useUISettingKey("discoverLanguage") return ( @@ -25,16 +26,17 @@ export const DiscoverSettingsScreen = () => { - +