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 });
-
+
[swift `versionList.statisticsFormat`](https://github.com/youversion/platform-sdk-swift/blob/84b6b7b34965b2279eb474d6e73cea9ddce2a4e6/Sources/YouVersionPlatformUI/Views/VersionPicking/BibleVersionsViewModel+VersionsList.swift#L18) = %#@versions_string@ in %#@languages_string@ | Kotlin uses positional string placeholders. Swift uses plural variations for versions and languages. |
+| `selectVersion` | [reactnative-expo `selectVersion`](https://github.com/youversion/platform-sdk-reactnative-expo/blob/c971b078dfe6c4df9c94903686c25020dc0f6f1b/packages/ui/src/native/bible-card.tsx#L242) = Select version | Exact English value and used native call site. |
+| `applyHighlightAriaLabel` | [reactnative-expo `applyHighlightAriaLabel`](https://github.com/youversion/platform-sdk-reactnative-expo/blob/c971b078dfe6c4df9c94903686c25020dc0f6f1b/packages/ui/src/native/bible-verse-action-sheet.tsx#L201) = Apply highlight | Exact English value and used native call site. |
+| `clearHighlightAriaLabel` | [reactnative-expo `clearHighlightAriaLabel`](https://github.com/youversion/platform-sdk-reactnative-expo/blob/c971b078dfe6c4df9c94903686c25020dc0f6f1b/packages/ui/src/native/bible-verse-action-sheet.tsx#L200) = Clear highlight | Exact English value and used native call site. |
+| `signInParagraph` | [kotlin `sign_in_prompt_paragraph`](https://github.com/youversion/platform-sdk-kotlin/blob/ed02fd04de080af4752af58c613abe97068a4ca4/platform-ui/src/main/java/com/youversion/platform/ui/views/SignInWithYouVersionPromptSheet.kt#L118) = **%1$s** wants to connect to your YouVersion **Bible App** account. This will allow them to see and interact with your Bible App activity. Would you like to proceed? [swift `signIn.paragraph`](https://github.com/youversion/platform-sdk-swift/blob/84b6b7b34965b2279eb474d6e73cea9ddce2a4e6/Sources/YouVersionPlatformUI/Views/SignInWithYouVersionView.swift#L72) = %@ wants to connect to your YouVersion Bible App account. This will allow them to see and interact with your Bible App activity. Would you like to proceed? [reactnative-expo `signInParagraph`](https://github.com/youversion/platform-sdk-reactnative-expo/blob/c971b078dfe6c4df9c94903686c25020dc0f6f1b/packages/ui/src/native/sign-in-with-youversion-sheet.tsx#L59) = {{appName}} wants to connect to your YouVersion Bible App account. This will allow them to see and interact with your Bible App activity. Would you like to proceed? | Exact in Expo. Swift uses %@. Kotlin uses %1$s and bold Markdown around the app name and Bible App. |
+| `youVersionPlatformLogoAriaLabel` | [reactnative-expo `youVersionPlatformLogoAriaLabel`](https://github.com/youversion/platform-sdk-reactnative-expo/blob/c971b078dfe6c4df9c94903686c25020dc0f6f1b/packages/ui/src/native/sign-in-with-youversion-sheet.tsx#L53) = YouVersion Platform | Exact English value and used native call site. |
+
+## Close wording or related UI role
+
+| React key | Used sibling strings | Qualification |
+| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `loadingVerseAriaLabel` | [reactnative-expo `loading`](https://github.com/youversion/platform-sdk-reactnative-expo/blob/c971b078dfe6c4df9c94903686c25020dc0f6f1b/packages/ui/src/native/verse-of-the-day.tsx#L103) = Loading | Generic Loading is used for native verse loaders. It omits the verse-specific announcement. |
+| `loadingPassageAriaLabel` | [reactnative-expo `loading`](https://github.com/youversion/platform-sdk-reactnative-expo/blob/c971b078dfe6c4df9c94903686c25020dc0f6f1b/packages/ui/src/native/bible-reader-toolbar.tsx#L305) = Loading | Generic Loading labels the chapter control while its book title loads. It does not describe passage-content loading. |
+| `loadingEllipsis` | [reactnative-expo `loading`](https://github.com/youversion/platform-sdk-reactnative-expo/blob/c971b078dfe6c4df9c94903686c25020dc0f6f1b/packages/ui/src/components/bible/chapter-picker-content.tsx#L395) = Loading | Loading is used without an ellipsis. Kotlin and Swift also define generic loading keys, but no direct production lookup was found. |
+| `footnotesHeading` | [kotlin `footnote_header_label`](https://github.com/youversion/platform-sdk-kotlin/blob/ed02fd04de080af4752af58c613abe97068a4ca4/platform-reader/src/main/java/com/youversion/platform/reader/sheets/BibleReaderIntroFootnotesSheet.kt#L58) = Footnote [kotlin `footnote_content_desc`](https://github.com/youversion/platform-sdk-kotlin/blob/ed02fd04de080af4752af58c613abe97068a4ca4/platform-ui/src/main/java/com/youversion/platform/ui/views/BibleText.kt#L125) = Footnote [swift `reader.footnoteTitle`](https://github.com/youversion/platform-sdk-swift/blob/84b6b7b34965b2279eb474d6e73cea9ddce2a4e6/Sources/YouVersionPlatformReader/Sheets/BibleReaderIntroFootnoteView.swift#L10) = Footnote | Singular Footnote is used. React labels a plural list, so this is a different role. |
+| `settingsAriaLabel` | [kotlin `reader_menu_font_settings`](https://github.com/youversion/platform-sdk-kotlin/blob/ed02fd04de080af4752af58c613abe97068a4ca4/platform-reader/src/main/java/com/youversion/platform/reader/components/BibleReaderHeaderMenu.kt#L38) = Font & Settings [swift `menu.fontSettings`](https://github.com/youversion/platform-sdk-swift/blob/84b6b7b34965b2279eb474d6e73cea9ddce2a4e6/Sources/YouVersionPlatformReader/Components/ReaderMenuItems.swift#L11) = Fonts & Settings [reactnative-expo `fontAndSettings`](https://github.com/youversion/platform-sdk-reactnative-expo/blob/c971b078dfe6c4df9c94903686c25020dc0f6f1b/packages/ui/src/native/bible-reader-toolbar.tsx#L236) = Fonts & Settings | Menu/control labels are Font & Settings in Kotlin and Fonts & Settings in Swift/Expo. These include more detail than Settings. |
+| `readerSettingsHeading` | [kotlin `reader_menu_font_settings`](https://github.com/youversion/platform-sdk-kotlin/blob/ed02fd04de080af4752af58c613abe97068a4ca4/platform-reader/src/main/java/com/youversion/platform/reader/components/BibleReaderHeaderMenu.kt#L38) = Font & Settings [swift `menu.fontSettings`](https://github.com/youversion/platform-sdk-swift/blob/84b6b7b34965b2279eb474d6e73cea9ddce2a4e6/Sources/YouVersionPlatformReader/Components/ReaderMenuItems.swift#L11) = Fonts & Settings [reactnative-expo `fontAndSettings`](https://github.com/youversion/platform-sdk-reactnative-expo/blob/c971b078dfe6c4df9c94903686c25020dc0f6f1b/packages/ui/src/native/bible-reader-toolbar.tsx#L236) = Fonts & Settings | Related font/settings entry points use different titles. A menu label is not automatically a panel-heading replacement. |
+| `bibleLanguagesHeading` | [kotlin `select_language_title`](https://github.com/youversion/platform-sdk-kotlin/blob/ed02fd04de080af4752af58c613abe97068a4ca4/platform-ui/src/main/java/com/youversion/platform/ui/views/versions/LanguagesScreen.kt#L86) = Select a Language [swift `languageList.title`](https://github.com/youversion/platform-sdk-swift/blob/84b6b7b34965b2279eb474d6e73cea9ddce2a4e6/Sources/YouVersionPlatformUI/Views/VersionPicking/BibleVersionsLanguagesView.swift#L108) = Select a Language [reactnative-expo `allLanguagesHeading`](https://github.com/youversion/platform-sdk-reactnative-expo/blob/c971b078dfe6c4df9c94903686c25020dc0f6f1b/packages/ui/src/components/bible/version-picker-content.tsx#L369) = All Languages | The native SDKs use Select a Language or All Languages in related picker UI. These are related roles with different wording. |
+| `languageVersionsHeading` | [kotlin `active_language_versions_header`](https://github.com/youversion/platform-sdk-kotlin/blob/ed02fd04de080af4752af58c613abe97068a4ca4/platform-ui/src/main/java/com/youversion/platform/ui/views/versions/VersionsScreen.kt#L143) = %1$s Versions (%2$s) | Kotlin uses {language} Versions ({count}). It adds a count token absent from the React heading. Expo uses Bible Versions plus the language name without this template. |
+| `chapterUnavailable` | [kotlin `bible_card_unavailable_reference`](https://github.com/youversion/platform-sdk-kotlin/blob/ed02fd04de080af4752af58c613abe97068a4ca4/platform-ui/src/main/java/com/youversion/platform/ui/views/card/BibleCard.kt#L221) = This passage is unavailable in the selected Bible version. [swift `bibleCard.unavailableReference`](https://github.com/youversion/platform-sdk-swift/blob/84b6b7b34965b2279eb474d6e73cea9ddce2a4e6/Sources/YouVersionPlatformUI/Views/BibleCardView.swift#L116) = This passage is unavailable in the selected Bible version. | The card message is This passage is unavailable in the selected Bible version. It lacks React chapter-specific recovery instructions. |
+| `unreachableServerError` | [kotlin `versions_load_error`](https://github.com/youversion/platform-sdk-kotlin/blob/ed02fd04de080af4752af58c613abe97068a4ca4/platform-ui/src/main/java/com/youversion/platform/ui/views/versions/VersionsScreen.kt#L174) = Couldn\'t load versions. Check your connection and try again. | Kotlin version-load failure says Could not load versions, then Check your connection and try again. It is scoped to version-list loading rather than any Bible-server request. |
+| `genericPassageError` | [swift `reader.versionAccessErrorBody`](https://github.com/youversion/platform-sdk-swift/blob/84b6b7b34965b2279eb474d6e73cea9ddce2a4e6/Sources/YouVersionPlatformUI/Views/VersionPicking/BibleVersionsViewModel+VersionsList.swift#L125) = It was not possible to access this Bible version. Please try again later. | Swift says It was not possible to access this Bible version. Please try again later. It concerns version access rather than a passage fetch. |
+
+## Catalog-only exact match
+
+[reactnative-expo `userAvatarAlt`](https://github.com/youversion/platform-sdk-reactnative-expo/blob/c971b078dfe6c4df9c94903686c25020dc0f6f1b/packages/ui/src/i18n/locales/en.json) = User avatar. No production lookup of `userAvatarAlt` was found in Expo native source.
+
+## No clear dedicated counterpart found
+
+| React key | React English |
+| -------------------------- | ----------------------------------------------------------------------------------------------- |
+| `unableToShare` | Unable to share. Please try again. |
+| `signInAppNameFallback` | This app |
+| `select` | Select |
+| `selectChapter` | Select a chapter |
+| `verseLabel` | Verse {{verseNum}} |
+| `sunIconTitle` | Sun |
+| `invalidAppKeyError` | This Bible content couldn't be loaded because the app key is missing or invalid. |
+| `forbiddenError` | This app isn't allowed to access this Bible content. |
+| `rateLimitedError` | The Bible service is receiving too many requests right now. Please wait a moment and try again. |
+| `serverError` | The Bible service is having trouble right now. Please try again in a moment. |
+| `verseActionsAriaLabel` | Verse actions |
+| `highlightColorsAriaLabel` | Highlight colors |
+
+## Scope and verification limits
+
+- Parsed Android default-language XML, resolved SDK R.string aliases, read the Swift English xcstrings catalog including plural variants, and read Expo English JSON.
+- Verified matches in production sources at the pinned revisions. Tests, examples, and previews were not evidence of active SDK copy.
+- Fuzzy matching generated candidates only. The reported close matches were checked for wording and role differences. Similar error words alone did not establish equivalence.
+- Expo DOM wrappers reuse the React web SDK. Inherited React copy is not counted as a second native implementation.
+- No app builds, devices, or rendered UI runs were needed or performed for this source-and-catalog inspection.
+- No shared-key migration or upstream changes are proposed or applied by this report.
diff --git a/docs/shared-localization-migration.md b/docs/shared-localization-migration.md
new file mode 100644
index 000000000..f814e377c
--- /dev/null
+++ b/docs/shared-localization-migration.md
@@ -0,0 +1,45 @@
+# Shared localization migration
+
+The React UI uses existing shared translation keys for equivalent actions, labels, and messages.
+Locale JSON files remain owned by the `platform-localization` sync.
+
+## Missing Korean and Czech translations
+
+At the initial locale inventory revision, `f1c55f10e71a0cd3851908bce31e856f50cd2ad7`, both `ko.json` and `cs.json` lack the shared keys below.
+These keys fall back to English until approved translations arrive through the localization sync.
+The direct migration accepts this fallback rather than retaining React-specific keys.
+
+| Shared key | Previous React key | Affected UI |
+| --- | --- | --- |
+| `cancel` | `genericCancel` | Highlights permission dialog |
+| `close` | `closeAriaLabel` | Popover close control |
+| `error` | `errorHeading` | Bible card and missing app key headings |
+| `font` | `fontLabel` | Reader font controls |
+| `passageUnavailable` | `passageNotFoundError` | Passage unavailable error |
+| `search` | `searchPlaceholder` | Chapter picker and Bible search placeholders |
+| `searchLanguages` | `languageSearchAriaLabel` | Language search controls |
+| `searchVersions` | `versionSearchAriaLabel` | Version search field |
+| `selectLanguage` | `selectLanguageAriaLabel` | Language selector control |
+| `regional` | `regionalHeading` | Regional language headings |
+| `suggested` | `suggestedTab` | Suggested languages tab |
+| `bibleApp` | `bibleAppTitle` | Bible App logo title |
+
+The previous `versionSearchAriaLabel` was also missing in Korean and Czech, so version search already used English.
+The other eleven replacements newly fall back to English.
+This changes text for eleven keys in Korean and ten in Czech, where the previous Bible App title was already English.
+The shared `share` key is present in both locales and replaces `shareAriaLabel` without that gap.
+
+The shared wording also changes some translations in other locales.
+English language search labels use "Search Languages", and version search uses "Search Versions".
+Distinct messages such as "Reader Settings" retain their existing keys.
+
+## Investigation references
+
+The missing entries belong to the shared keys in `platform-localization/sources/common/en.json` and its Korean and Czech translations.
+Translation approval and distribution follow [the i18n workflow](./i18n-guidelines.md).
+This migration does not establish why those translations are missing upstream.
+
+Run `node scripts/migrate-shared-i18n-keys.mjs` to check for remaining legacy callers and print the current Korean and Czech gaps.
+The optional `--write` argument applies the key substitutions to production TypeScript files.
+
+Remaining React-specific strings and their matches in sibling SDKs are listed in the [cross-SDK comparison](./react-string-cross-sdk-comparison.md).
diff --git a/packages/ui/src/components/bible-app-logo-lockup.tsx b/packages/ui/src/components/bible-app-logo-lockup.tsx
index 195b1dc9c..b41edfeaf 100644
--- a/packages/ui/src/components/bible-app-logo-lockup.tsx
+++ b/packages/ui/src/components/bible-app-logo-lockup.tsx
@@ -16,7 +16,7 @@ export function BibleAppLogoLockup(props: SVGProps): 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 (