From 22d0c2584ce0e17ade6d0928be55aacfc13677ef Mon Sep 17 00:00:00 2001 From: camrun01 Date: Tue, 26 May 2026 14:07:12 -0500 Subject: [PATCH 1/3] chore: extract strings --- .gitignore | 1 + .../src/components/YouVersionAuthButton.tsx | 19 ++- .../src/components/bible-app-logo-lockup.tsx | 144 +++++++++--------- packages/ui/src/components/bible-card.tsx | 10 +- .../src/components/bible-chapter-picker.tsx | 17 ++- packages/ui/src/components/bible-reader.tsx | 50 +++--- .../src/components/bible-version-picker.tsx | 40 +++-- packages/ui/src/components/icons/loader.tsx | 8 +- packages/ui/src/components/icons/votd.tsx | 42 ++--- .../ui/src/components/verse-of-the-day.tsx | 20 ++- packages/ui/src/components/verse.tsx | 18 ++- packages/ui/src/i18n/locales/en.json | 57 ++++++- packages/ui/src/lib/bible-text-error.ts | 32 ++-- 13 files changed, 286 insertions(+), 172 deletions(-) diff --git a/.gitignore b/.gitignore index b4f26e1c..3903eb67 100644 --- a/.gitignore +++ b/.gitignore @@ -65,6 +65,7 @@ storybook-static # Yarn (legacy) .pnp.* +.pnpm-store .yarn/* !.yarn/patches !.yarn/plugins diff --git a/packages/ui/src/components/YouVersionAuthButton.tsx b/packages/ui/src/components/YouVersionAuthButton.tsx index 69688e03..5e11a6db 100644 --- a/packages/ui/src/components/YouVersionAuthButton.tsx +++ b/packages/ui/src/components/YouVersionAuthButton.tsx @@ -1,4 +1,6 @@ import React, { useMemo } from 'react'; +import { useTranslation, Trans } from 'react-i18next'; +import i18n from '@/i18n'; import { LoaderIcon } from './icons/loader'; import { type AuthenticationScopes } from '@youversion/platform-core'; import { useYVAuth, useTheme } from '@youversion/platform-react-hooks'; @@ -126,6 +128,7 @@ export const YouVersionAuthButton = React.forwardRef): Promise => { e.preventDefault(); @@ -158,19 +161,27 @@ export const YouVersionAuthButton = React.forwardRef - Sign out of YouVersion + }} + /> ) : (
- Sign in with YouVersion + }} + />
); - }, [mode, auth.isAuthenticated, size, text]); + }, [mode, auth.isAuthenticated, size, text, t]); const loadingSpinner = ( diff --git a/packages/ui/src/components/bible-app-logo-lockup.tsx b/packages/ui/src/components/bible-app-logo-lockup.tsx index 25d06056..195b1dc9 100644 --- a/packages/ui/src/components/bible-app-logo-lockup.tsx +++ b/packages/ui/src/components/bible-app-logo-lockup.tsx @@ -1,78 +1,82 @@ import type { SVGProps } from 'react'; +import { useTranslation } from 'react-i18next'; +import i18n from '@/i18n'; import { cn } from '@/lib/utils'; -const SvgComponent = (props: SVGProps): React.ReactElement => ( - - Bible App - - ): React.ReactElement { + const { t } = useTranslation(undefined, { i18n }); + + return ( + + {t('bibleAppTitle')} - - - - - - - - - - - - - - - - - - -); - -export { SvgComponent as BibleAppLogoLockup }; + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/packages/ui/src/components/bible-card.tsx b/packages/ui/src/components/bible-card.tsx index a2a52f50..738a8764 100644 --- a/packages/ui/src/components/bible-card.tsx +++ b/packages/ui/src/components/bible-card.tsx @@ -1,5 +1,7 @@ import { usePassage, useVersion, useTheme } from '@youversion/platform-react-hooks'; import { DEFAULT_LICENSE_FREE_BIBLE_VERSION } from '@youversion/platform-core'; +import { useTranslation } from 'react-i18next'; +import i18n from '@/i18n'; import { BibleTextView, type FootnoteData } from './verse'; import { BibleAppLogoLockup } from './bible-app-logo-lockup'; import { BibleVersionPicker, type BibleVersionPickerPressData } from './bible-version-picker'; @@ -41,10 +43,11 @@ export type BibleCardProps = { }; function BibleCardHeaderError(): React.ReactNode { + const { t } = useTranslation(undefined, { i18n }); return (

- Error + {t('errorHeading')}

); @@ -75,6 +78,7 @@ function BibleCardVersionPicker({ theme: 'light' | 'dark'; onVersionPickerPress?: (data: BibleVersionPickerPressData) => void; }): React.ReactNode { + const { t } = useTranslation(undefined, { i18n }); return ( - + {({ version, loading }) => ( )} diff --git a/packages/ui/src/components/bible-chapter-picker.tsx b/packages/ui/src/components/bible-chapter-picker.tsx index 4a66bfff..8bb7b901 100644 --- a/packages/ui/src/components/bible-chapter-picker.tsx +++ b/packages/ui/src/components/bible-chapter-picker.tsx @@ -1,5 +1,7 @@ 'use client'; +import { useTranslation } from 'react-i18next'; +import i18n from '@/i18n'; import { cloneElement, createContext, @@ -89,6 +91,7 @@ function Root({ onChapterPickerPress, children, }: RootProps) { + const { t } = useTranslation(undefined, { i18n }); const [book, setBook] = useControllableState({ prop: controlledBook, defaultProp: defaultBook, @@ -203,7 +206,7 @@ function Root({ {children} {/* data-yv-sdk for styles is needed because the popover gets rendered outside of the providers scope **/} - + setIsPopoverOpen(false)} /> @@ -226,6 +229,7 @@ export type TriggerProps = Omit, 'ch }; function Trigger({ asChild = true, children, ...props }: TriggerProps) { + const { t } = useTranslation(undefined, { i18n }); const { book, chapter, background, versionId, scrollToCurrentBook, onChapterPickerPress } = useBibleChapterPickerContext(); const { books, loading } = useBooks(versionId); @@ -239,8 +243,8 @@ function Trigger({ asChild = true, children, ...props }: TriggerProps) { chapterLabel = currentBook.intro.title; } const buttonText = loading - ? 'Loading...' - : `${currentBook?.title || 'Select a chapter'}${chapterLabel ? ` ${chapterLabel}` : ''}`; + ? t('loadingEllipsis') + : `${currentBook?.title || t('selectChapter')}${chapterLabel ? ` ${chapterLabel}` : ''}`; const content = typeof children === 'function' @@ -291,6 +295,7 @@ function Trigger({ asChild = true, children, ...props }: TriggerProps) { } function Content({ onRequestClose, onSelect }: BibleChapterPickerContentProps) { + const { t } = useTranslation(undefined, { i18n }); const { book, defaultBook, @@ -372,7 +377,7 @@ function Content({ onRequestClose, onSelect }: BibleChapterPickerContentProps) { ) : (
- No chapters available + {t('noChaptersAvailable')}
)} @@ -380,7 +385,7 @@ function Content({ onRequestClose, onSelect }: BibleChapterPickerContentProps) { )) ) : (
- We're sorry, there are no Bible results for this search. + {t('noBibleSearchResults')}
)} @@ -390,7 +395,7 @@ function Content({ onRequestClose, onSelect }: BibleChapterPickerContentProps) { setSearchQuery(e.target.value)} /> diff --git a/packages/ui/src/components/bible-reader.tsx b/packages/ui/src/components/bible-reader.tsx index eaed2fb6..e16ea1c6 100644 --- a/packages/ui/src/components/bible-reader.tsx +++ b/packages/ui/src/components/bible-reader.tsx @@ -1,5 +1,7 @@ 'use client'; +import { useTranslation } from 'react-i18next'; +import i18n from '@/i18n'; import { useControllableState } from '@radix-ui/react-use-controllable-state'; import { useBooks, @@ -310,6 +312,7 @@ function Root({ } function Content() { + const { t } = useTranslation(undefined, { i18n }); const { background, book, @@ -361,10 +364,8 @@ function Content() { {chapterUnavailable ? ( - // This copy was taken from bible.com (e.g. https://www.bible.com/bible/4253/ACT.INTRO1.AFV)

- This chapter is not available in this version. Please choose a different chapter or - version. + {t('chapterUnavailable')}

) : ( - Learn More + {t('learnMore')} ) : null} @@ -404,6 +405,7 @@ function Content() { } function UserMenu() { + const { t } = useTranslation(undefined, { i18n }); const { auth, signIn, signOut, userInfo } = useYVAuth(); const yvContext = useContext(YouVersionContext); @@ -414,7 +416,7 @@ function UserMenu() { @@ -434,7 +436,7 @@ function UserMenu() { {auth.isAuthenticated ? ( ) : ( )} @@ -459,6 +461,7 @@ export function BibleThemeSettingsContent({ onFontIncreased, onFontDecreased, }: BibleThemeSettingsContentProps): ReactElement { + const { t } = useTranslation(undefined, { i18n }); return (
@@ -506,9 +509,9 @@ export function BibleThemeSettingsContent({ : '', )} > - Font + {t('fontLabel')} - Inter + {t('interFontName')}
@@ -546,6 +551,7 @@ export type BibleReaderToolbarProps = { }; function Toolbar({ border = 'top', onOpenBibleThemeSettings }: BibleReaderToolbarProps) { + const { t } = useTranslation(undefined, { i18n }); const { book, chapter, @@ -658,7 +664,7 @@ function Toolbar({ border = 'top', onOpenBibleThemeSettings }: BibleReaderToolba size="icon" variant="ghost" disabled={!canNavigatePrevious} - aria-label="Previous chapter" + aria-label={t('previousChapterAriaLabel')} onClick={(e) => { e.stopPropagation(); if (prevResult) { @@ -675,14 +681,14 @@ function Toolbar({ border = 'top', onOpenBibleThemeSettings }: BibleReaderToolba variant="secondary" className="yv:px-0 yv:font-bold yv:text-foreground yv:min-w-[5ch]" disabled={loading} - aria-label="Change Bible book and chapter" + aria-label={t('changeBibleBookAndChapterAriaLabel')} > {loading ? ( ) : ( <> - {currentBook?.title || 'Select'} + {currentBook?.title || t('select')} {chapterLabel || ''} @@ -703,7 +709,7 @@ function Toolbar({ border = 'top', onOpenBibleThemeSettings }: BibleReaderToolba size="icon" variant="ghost" disabled={!canNavigateNext} - aria-label="Next chapter" + aria-label={t('nextChapterAriaLabel')} > @@ -718,14 +724,16 @@ function Toolbar({ border = 'top', onOpenBibleThemeSettings }: BibleReaderToolba background={background} onVersionPickerPress={onVersionPickerPress} > - + {({ version, loading }) => ( ) : ( - + - + - {version?.localized_abbreviation || 'Select'} + {version?.localized_abbreviation || t('select')} ); @@ -450,13 +453,14 @@ function Trigger({ asChild = true, children, ...props }: BibleVersionPickerTrigg * callbacks through as React event handlers. */ export function BibleVersionPickerLanguageTrigger({ - 'aria-label': ariaLabel = 'Select language', + 'aria-label': ariaLabel, className, onClick, size = 'sm', variant = 'secondary', ...props }: BibleVersionPickerLanguageTriggerProps): React.ReactElement { + const { t } = useTranslation(undefined, { i18n }); const { filteredVersions, filteredRecentVersions, @@ -476,7 +480,7 @@ export function BibleVersionPickerLanguageTrigger({ return ( ) : null } @@ -612,7 +617,9 @@ function Content({ open, onRequestClose }: BibleVersionPickerContentProps = {}) {/* Recent Versions */} {filteredRecentVersions.length > 0 && ( <> -

Recently Used Versions

+

+ {t('recentlyUsedVersionsHeading')} +

{filteredRecentVersions.map((version) => ( 0 ? ( -

All Versions

+

{t('allVersionsHeading')}

{filteredVersions.map((version: BibleVersion) => ( ) : !filteredRecentVersions.length ? (
- No versions found + {t('noVersionsFound')}
) : null} @@ -701,7 +708,7 @@ function Content({ open, onRequestClose }: BibleVersionPickerContentProps = {}) setSearchQuery(e.target.value)} > @@ -735,6 +742,7 @@ export function BibleLanguagePickerContent({ open, onRequestClose, }: BibleLanguagePickerContentProps = {}): React.ReactElement { + const { t } = useTranslation(undefined, { i18n }); const { totalLanguages, selectedLanguageId, @@ -762,17 +770,19 @@ export function BibleLanguagePickerContent({ > - Suggested + {t('suggestedTab')} - All ({totalLanguages}) + {t('allLanguagesTab', { count: totalLanguages })} {suggestedLanguages.length > 0 ? ( <> -

Regional

+

+ {t('regionalHeading')} +

{suggestedLanguages.map((suggestedLanguage) => ( ) : (

- No regional languages available + {t('noRegionalLanguagesAvailable')}

)}
-

All Languages

+

+ {t('allLanguagesHeading')} +

{languages.map((language) => ( ): ReactElement { +export function LoaderIcon({ + 'aria-label': ariaLabel, + ...props +}: ComponentProps<'svg'>): ReactElement { return ( ): React.ReactElement => ( - - Sun - - -); +export function Votd(props: SVGProps): React.ReactElement { + const { t } = useTranslation(undefined, { i18n }); -export { SvgComponent as Votd }; + return ( + + {t('sunIconTitle')} + + + ); +} diff --git a/packages/ui/src/components/verse-of-the-day.tsx b/packages/ui/src/components/verse-of-the-day.tsx index cdbf8e8e..24c42ad2 100644 --- a/packages/ui/src/components/verse-of-the-day.tsx +++ b/packages/ui/src/components/verse-of-the-day.tsx @@ -50,7 +50,17 @@ export type VerseOfTheDayProps = { size?: 'default' | 'lg'; }; -async function share({ title, text, url }: { title?: string; text: string; url?: string }) { +async function share({ + title, + text, + url, + errorMessage = i18n.t('unableToShare'), +}: { + title?: string; + text: string; + url?: string; + errorMessage?: string; +}) { if (navigator.share) { try { await navigator.share({ @@ -63,7 +73,7 @@ async function share({ title, text, url }: { title?: string; text: string; url?: } } else if (navigator.clipboard) { navigator.clipboard.writeText(text).catch(() => { - alert('Unable to share. Please try again.'); + alert(errorMessage); }); } } @@ -122,7 +132,7 @@ export function VerseOfTheDay({ const handleShareVerse = async () => { if (verseRef.current) { const text = verseRef.current.innerText + '\n\n' + referenceText; - await share({ text }); + await share({ text, errorMessage: t('unableToShare') }); } }; @@ -159,7 +169,7 @@ export function VerseOfTheDay({ className="yv:col-start-2 yv:row-span-2 yv:row-start-1 yv:self-start yv:justify-self-end" >
0; return ( @@ -156,10 +159,12 @@ const VerseFootnoteButton = memo(function VerseFootnoteButton({ theme: 'light' | 'dark'; onFootnotePress?: (data: FootnoteData) => void; }) { + const { t } = useTranslation(undefined, { i18n }); + if (onFootnotePress) { return (