From ea09a5220a8bf92a971e576be6abe7cab1f900cb Mon Sep 17 00:00:00 2001 From: camrun01 Date: Wed, 4 Nov 2026 13:37:23 -0600 Subject: [PATCH] refactor(ui): use shared localization keys --- .changeset/shared-localization-keys.md | 5 ++ docs/i18n-guidelines.md | 8 ++- docs/shared-localization-migration.md | 43 +++++++++++++ .../src/components/bible-app-logo-lockup.tsx | 2 +- .../ui/src/components/bible-card.stories.tsx | 5 +- packages/ui/src/components/bible-card.tsx | 2 +- .../src/components/bible-chapter-picker.tsx | 2 +- .../ui/src/components/bible-reader-search.tsx | 2 +- .../src/components/bible-reader.stories.tsx | 3 +- packages/ui/src/components/bible-reader.tsx | 4 +- .../bible-version-picker.stories.tsx | 10 +-- .../components/bible-version-picker.test.tsx | 5 +- .../components/bible-version-picker/index.tsx | 2 +- .../bible-version-picker/language-panel.tsx | 10 +-- .../bible-version-picker/version-list.tsx | 2 +- .../highlight-permission-dialog.tsx | 2 +- .../ui/src/components/missing-app-key.tsx | 2 +- packages/ui/src/components/ui/popover.tsx | 2 +- .../src/components/verse-of-the-day.test.tsx | 10 +-- .../ui/src/components/verse-of-the-day.tsx | 2 +- packages/ui/src/i18n/index.test.ts | 45 +++++++++---- packages/ui/src/lib/bible-text-error.ts | 2 +- scripts/migrate-shared-i18n-keys.mjs | 63 +++++++++++++++++++ 23 files changed, 185 insertions(+), 48 deletions(-) create mode 100644 .changeset/shared-localization-keys.md create mode 100644 docs/shared-localization-migration.md create mode 100644 scripts/migrate-shared-i18n-keys.mjs diff --git a/.changeset/shared-localization-keys.md b/.changeset/shared-localization-keys.md new file mode 100644 index 000000000..80a35ea98 --- /dev/null +++ b/.changeset/shared-localization-keys.md @@ -0,0 +1,5 @@ +--- +'@youversion/platform-react-ui': patch +--- + +Use shared localization keys for equivalent UI labels, actions, and passage errors. Korean and Czech use English for shared keys that await translations. diff --git a/docs/i18n-guidelines.md b/docs/i18n-guidelines.md index 90523060a..92632abbd 100644 --- a/docs/i18n-guidelines.md +++ b/docs/i18n-guidelines.md @@ -47,7 +47,7 @@ Use `useTranslation` + `t()` for simple strings and `Trans` with `i18nKey` for r ```tsx const { t } = useTranslation(undefined, { i18n }); - + ): React.ReactE fill="none" className={cn('yv:text-foreground yv:dark:text-(--yv-yellow-10)', props.className)} > - {t('bibleAppTitle')} + {t('bibleApp')} - {t('errorHeading')} + {t('error')} ); } diff --git a/packages/ui/src/components/bible-chapter-picker.tsx b/packages/ui/src/components/bible-chapter-picker.tsx index 2ebfca273..5c4a5c351 100644 --- a/packages/ui/src/components/bible-chapter-picker.tsx +++ b/packages/ui/src/components/bible-chapter-picker.tsx @@ -432,7 +432,7 @@ function Content({ onRequestClose, onSelect }: BibleChapterPickerContentProps) { search.setQuery(event.target.value)} onKeyDown={(event) => { diff --git a/packages/ui/src/components/bible-reader.stories.tsx b/packages/ui/src/components/bible-reader.stories.tsx index 335ee9821..838c98ac6 100644 --- a/packages/ui/src/components/bible-reader.stories.tsx +++ b/packages/ui/src/components/bible-reader.stories.tsx @@ -1,3 +1,4 @@ +import i18n from '@/i18n'; import { expandPassageId } from '@/lib/highlight-projection'; import { INTER_FONT, SOURCE_SERIF_FONT, UNTITLED_SERIF_FONT } from '@/lib/verse-html-utils'; import type { Meta, StoryObj } from '@storybook/react-vite'; @@ -420,7 +421,7 @@ export const ForcedRtlChromeGeometry: Story = { ); await expect(firstVersion.querySelector('bdi')).toHaveAttribute('dir', 'auto'); await expect( - versionCanvas.getByRole('textbox', { name: 'Search Bible versions' }), + versionCanvas.getByRole('textbox', { name: i18n.t('searchVersions') }), ).toHaveAttribute('dir', 'auto'); await userEvent.click(versionCanvas.getByRole('button', { name: 'Close' })); diff --git a/packages/ui/src/components/bible-reader.tsx b/packages/ui/src/components/bible-reader.tsx index fa9a000a7..4fd167a9c 100644 --- a/packages/ui/src/components/bible-reader.tsx +++ b/packages/ui/src/components/bible-reader.tsx @@ -1523,7 +1523,7 @@ export function BibleThemeSettingsContent({ : '', )} > - {t('fontLabel')} + {t('font')} {INTER_FONT_NAME} @@ -1547,7 +1547,7 @@ export function BibleThemeSettingsContent({ : '', )} > - {t('fontLabel')} + {t('font')} {UNTITLED_SERIF_FONT_NAME} diff --git a/packages/ui/src/components/bible-version-picker.stories.tsx b/packages/ui/src/components/bible-version-picker.stories.tsx index 5c8218a46..e57a14330 100644 --- a/packages/ui/src/components/bible-version-picker.stories.tsx +++ b/packages/ui/src/components/bible-version-picker.stories.tsx @@ -174,7 +174,7 @@ const tabletJourney: Story = { await userEvent.click(screen.getByRole('button', { name: /^back$/i })); await expect(screen.getByRole('tab', { name: /suggested/i })).toBeVisible(); await userEvent.click(screen.getByRole('button', { name: /back to bible versions/i })); - await expect(screen.getByRole('textbox', { name: /search bible versions/i })).toBeVisible(); + await expect(screen.getByRole('textbox', { name: i18n.t('searchVersions') })).toBeVisible(); await userEvent.click(screen.getByRole('button', { name: /^Language\b/ })); await userEvent.click(screen.getByRole('tab', { name: /^All\b/ })); await userEvent.click(await screen.findByRole('listitem', { name: /Spanish/ })); @@ -510,7 +510,7 @@ export const InteractiveVersionSearch: Story = { await userEvent.click(trigger); // Type in search - const searchInput = screen.getByRole('textbox', { name: /search bible versions/i }); + const searchInput = screen.getByRole('textbox', { name: i18n.t('searchVersions') }); await userEvent.type(searchInput, 'NIV', { delay: 50 }); // Verify search is working (versions should be filtered) @@ -613,7 +613,7 @@ export const SearchResetsAfterSelection: Story = { await userEvent.click(trigger); // Type in search - const searchInput = screen.getByRole('textbox', { name: /search bible versions/i }); + const searchInput = screen.getByRole('textbox', { name: i18n.t('searchVersions') }); await userEvent.type(searchInput, 'Amplified', { delay: 50 }); await expect(searchInput).toHaveValue('Amplified'); @@ -626,7 +626,7 @@ export const SearchResetsAfterSelection: Story = { await userEvent.click(updatedTrigger); // Verify search input is cleared - const resetSearchInput = screen.getByRole('textbox', { name: /search bible versions/i }); + const resetSearchInput = screen.getByRole('textbox', { name: i18n.t('searchVersions') }); await expect(resetSearchInput).toHaveValue(''); }, }; @@ -662,7 +662,7 @@ export const RecentVersionsSearchFilter: Story = { ).toBeInTheDocument(); // Search for "ASV" - const searchInput = screen.getByRole('textbox', { name: /search bible versions/i }); + const searchInput = screen.getByRole('textbox', { name: i18n.t('searchVersions') }); await userEvent.type(searchInput, 'ASV', { delay: 50 }); // Verify only ASV appears in recent versions after filtering diff --git a/packages/ui/src/components/bible-version-picker.test.tsx b/packages/ui/src/components/bible-version-picker.test.tsx index 5f7a14e54..537a599aa 100644 --- a/packages/ui/src/components/bible-version-picker.test.tsx +++ b/packages/ui/src/components/bible-version-picker.test.tsx @@ -1,6 +1,7 @@ /** * @vitest-environment jsdom */ +import i18n from '@/i18n'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import { render, screen, waitFor, within } from '@testing-library/react'; import type { ReactElement } from 'react'; @@ -193,7 +194,7 @@ function getLanguageSearchInput() { } function getVersionSearchInput() { - return screen.getByRole('textbox', { name: /search bible versions/i }); + return screen.getByRole('textbox', { name: i18n.t('searchVersions') }); } /** Ready-to-run setup for new/edited picker workflows (see docs/testing.md). */ @@ -728,7 +729,7 @@ describe('BibleVersionPicker', () => { expect(screen.queryByRole('button', { name: /select a language/i })).not.toBeInTheDocument(); expect(screen.getByRole('button', { name: /^language /i })).toBeInTheDocument(); - expect(screen.getAllByRole('textbox', { name: /search bible versions/i })).toHaveLength(1); + expect(screen.getAllByRole('textbox', { name: i18n.t('searchVersions') })).toHaveLength(1); }); it('mounts one language view, then a search view, and returns to versions', async () => { diff --git a/packages/ui/src/components/bible-version-picker/index.tsx b/packages/ui/src/components/bible-version-picker/index.tsx index 3042bf7e2..4e0bd4139 100644 --- a/packages/ui/src/components/bible-version-picker/index.tsx +++ b/packages/ui/src/components/bible-version-picker/index.tsx @@ -539,7 +539,7 @@ export function BibleVersionPickerLanguageTrigger({ return ( ) : null} diff --git a/packages/ui/src/components/verse-of-the-day.test.tsx b/packages/ui/src/components/verse-of-the-day.test.tsx index 8c6f6897e..a93a290df 100644 --- a/packages/ui/src/components/verse-of-the-day.test.tsx +++ b/packages/ui/src/components/verse-of-the-day.test.tsx @@ -128,7 +128,7 @@ describe('VerseOfTheDay - share', () => { renderVotd(); - await user.click(screen.getByRole('button', { name: en.shareAriaLabel })); + await user.click(screen.getByRole('button', { name: en.share })); expect(onShare).toHaveBeenCalledTimes(1); expect(onShare).toHaveBeenCalledWith(expectedShareData()); @@ -145,7 +145,7 @@ describe('VerseOfTheDay - share', () => { try { renderVotd(); - await user.click(screen.getByRole('button', { name: en.shareAriaLabel })); + await user.click(screen.getByRole('button', { name: en.share })); await new Promise((resolve) => setTimeout(resolve, 0)); expect(onShare).toHaveBeenCalledTimes(1); @@ -161,7 +161,7 @@ describe('VerseOfTheDay - share', () => { renderVotd(); - await user.click(screen.getByRole('button', { name: en.shareAriaLabel })); + await user.click(screen.getByRole('button', { name: en.share })); expect(onShare).toHaveBeenCalledTimes(1); expect(shareSpy).not.toHaveBeenCalled(); @@ -173,7 +173,7 @@ describe('VerseOfTheDay - share', () => { renderVotd(); - expect(screen.queryByRole('button', { name: en.shareAriaLabel })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: en.share })).not.toBeInTheDocument(); expect(onShare).not.toHaveBeenCalled(); }); @@ -186,7 +186,7 @@ describe('VerseOfTheDay - share', () => { stubOverrides({ passageError: new Error('Passage failed') }), ); - const shareButton = screen.getByRole('button', { name: en.shareAriaLabel }); + const shareButton = screen.getByRole('button', { name: en.share }); expect(shareButton).toBeDisabled(); await user.click(shareButton); diff --git a/packages/ui/src/components/verse-of-the-day.tsx b/packages/ui/src/components/verse-of-the-day.tsx index 906237ad4..c6f9dd06d 100644 --- a/packages/ui/src/components/verse-of-the-day.tsx +++ b/packages/ui/src/components/verse-of-the-day.tsx @@ -271,7 +271,7 @@ export function VerseOfTheDay({ className="yv:col-start-2 yv:row-span-2 yv:row-start-1 yv:self-start yv:justify-self-end" >