Skip to content

feat(settings): adopt contract language catalogs - #154

Merged
Quick104 merged 1 commit into
mainfrom
codex/settings-language-catalog
Jul 30, 2026
Merged

feat(settings): adopt contract language catalogs#154
Quick104 merged 1 commit into
mainfrom
codex/settings-language-catalog

Conversation

@Quick104

@Quick104 Quick104 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Problem

Android phone and TV used a small hand-maintained language table, so metadata and subtitle pickers exposed fewer choices than the web app. The duplicated lists also made future drift between phone, TV, Apple, and web likely.

Approach

  • vendor and pin Silo Server settings manifest revision 2 from 025083159f9624269483479cc05de02a822c6cd2
  • consume generated, definition-specific audio, subtitle, and metadata option sets
  • build each picker from the contract floor, server-provided suggested_values, and the exact current wire value
  • localize language labels with Android locale/ICU data while persisting BCP 47 tags
  • deduplicate true ISO aliases without collapsing explicit regions or scripts
  • propagate runtime suggestions through the shared profile settings controller to both phone and TV
  • add manifest/binding drift coverage and preserve unknown current tags as selectable rows

Canonical Server contract: Silo-Server/silo-server#521

Companion PRs

Verification

  • :shared:testDebugUnitTest — passed
  • :androidApp:testDebugUnitTest — passed
  • :androidApp:assembleDebug — passed
  • :androidTvApp:testDebugUnitTest — passed
  • :androidTvApp:assembleDebug — passed
  • combined phone/TV verification — BUILD SUCCESSFUL in 3m 22s, 152 actionable tasks
  • generated Kotlin metadata and vendored manifest comparison — byte-identical to Server commit 02508315
  • vendored manifest/conformance fixtures — byte-identical to Server commit 02508315

Risks and follow-up

  • Merge/deploy the Server contract first; revision-aware clients intentionally report an upgrade requirement against an older revision.
  • Runtime suggestions are advisory. The generated floor remains available if the server omits them, and valid unknown current tags remain visible.
  • Existing Gradle/Kotlin warnings are unchanged by this PR.

AI Disclosure

  • Tool(s): Codex in T3 Code; Claude Code CLI
  • Model(s): gpt-5.6-sol; claude-fable-5
  • Involvement: fully AI-generated
  • Adversarial review: Reviewed generated metadata drift, alias versus region identity, legacy label migration, phone/TV propagation, current-value retention, and old-server behavior. No Android-specific defect remained after the full unit-test and assembly matrix.

Summary by CodeRabbit

  • New Features

    • Language settings now provide context-specific options for audio, subtitles, and metadata.
    • Language pickers include server-provided suggestions and preserve the current selection when it is not in the standard list.
    • Language names are displayed more clearly across Android mobile and TV settings.
    • Improved support for language-code aliases and regional variants.
  • Bug Fixes

    • Settings now stay synchronized with the latest profile configuration and available language suggestions.
    • Improved handling of default and unset language selections.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Language settings now use revisioned catalog metadata, platform-localized labels, canonical language identities, and server-provided suggestions. Profile state propagates these suggestions to Android and TV pickers, which use keyed option and wire-value mappings.

Changes

Language settings presentation and suggestions

Layer / File(s) Summary
Presentation metadata and effective values
shared/src/commonMain/kotlin/org/siloserver/silo/model/settings/SettingKeys.kt, shared/src/commonMain/kotlin/org/siloserver/silo/model/settings/SettingValueModels.kt, shared/src/commonTest/resources/settings/v1/*, shared/src/commonTest/kotlin/org/siloserver/silo/model/settings/SettingsManifest.kt, shared/src/androidUnitTest/kotlin/org/siloserver/silo/model/settings/SettingsConformanceTest.kt, shared/src/commonTest/kotlin/org/siloserver/silo/network/api/SettingsApiValuesTest.kt
Settings revision 2 adds language option sets, per-setting presentation metadata, unset labels, and suggested_values effective-value data, with manifest and parsing conformance updates.
Keyed language option generation
shared/src/commonMain/kotlin/org/siloserver/silo/model/settings/LanguageOptions.kt, shared/src/androidMain/kotlin/org/siloserver/silo/model/settings/LanguagePresentation.android.kt, shared/src/commonTest/kotlin/org/siloserver/silo/model/settings/LanguageOptionsTest.kt
Language options combine catalog, runtime, and current values; platform labels and canonical identities deduplicate aliases; keyed labels, wire mapping, and legacy migration are tested.
Profile suggestion state propagation
shared/src/commonMain/kotlin/org/siloserver/silo/domain/settings/ProfileSettingsController.kt, shared/src/commonTest/kotlin/org/siloserver/silo/domain/settings/ProfileSettingsControllerTest.kt, androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/settings/SettingsViewModel.kt, androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/settings/TvSettingsViewModel.kt
Profile snapshots and Android/TV UI state now load and reconcile audio, subtitle, and metadata language suggestions.
Android and TV picker integration
androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/settings/{PlaybackSettings.kt,SubtitleSettings.kt,SettingsScreen.kt}, androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/settings/TvSettingsScreen.kt
Android and TV pickers use setting keys, current values, runtime suggestions, localized labels, and rendered-option wire-value resolution.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant EffectiveSettingsAPI
  participant ProfileSettingsController
  participant SettingsViewModel
  participant LanguageOptions
  participant SettingsPicker
  EffectiveSettingsAPI->>ProfileSettingsController: Return effective values and suggested_values
  ProfileSettingsController->>SettingsViewModel: Provide language suggestions in snapshot
  SettingsViewModel->>SettingsPicker: Pass current values and suggestions
  SettingsPicker->>LanguageOptions: Build keyed options and labels
  LanguageOptions-->>SettingsPicker: Return localized labels and wire mappings
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adopting contract-driven language catalogs for settings.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/settings-language-catalog

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
shared/src/androidUnitTest/kotlin/org/siloserver/silo/model/settings/SettingsConformanceTest.kt (1)

190-207: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider also asserting the reverse direction.

The loop only walks generated DEFINITIONS, so a manifest definition that carries suggested_options/unset_label but is absent from the generated bindings passes silently — the same vendoring drift theVendoredManifestCoversTheGeneratedBindings exists to catch.

♻️ Suggested addition
             )
         }
+
+        val manifestPresentationKeys = manifest.definitions
+            .filter { it.suggestedOptions != null || it.unsetLabel != null }
+            .map { it.key }
+            .toSet()
+        assertEquals(
+            manifestPresentationKeys,
+            SettingPresentationMetadata.DEFINITIONS.keys,
+            "the manifest and the generated presentation metadata were vendored from " +
+                "different server commits",
+        )
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@shared/src/androidUnitTest/kotlin/org/siloserver/silo/model/settings/SettingsConformanceTest.kt`
around lines 190 - 207, Extend
generatedPresentationMetadataMatchesTheVendoredManifest to also iterate manifest
definitions and verify that every definition containing suggested_options or
unset_label has a corresponding entry in
SettingPresentationMetadata.DEFINITIONS, reusing the existing generated-binding
coverage assertion pattern where appropriate.
shared/src/commonMain/kotlin/org/siloserver/silo/model/settings/LanguageOptions.kt (2)

82-87: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Hoist the Regex out of isPreservableTag; it is recompiled on every candidate.

namedOptions calls this once per catalog entry, runtime value, and current value, so each picker build recompiles the pattern ~40 times. Also note the pattern accepts _ separators, so an underscore-spelled tag is kept verbatim in values and can be written back to the wire — consider normalizing to - in add.

♻️ Proposed change
-    private fun isPreservableTag(value: String): Boolean =
-        value.isNotBlank() &&
-            !value.equals("Off", ignoreCase = true) &&
-            !value.equals("Default", ignoreCase = true) &&
-            Regex("^[a-zA-Z]{2,3}([-_][a-zA-Z0-9]{1,8})*$").matches(value) &&
-            canonicalSubtitleLanguage(value) != null
+    private val TAG_SHAPE = Regex("^[a-zA-Z]{2,3}([-_][a-zA-Z0-9]{1,8})*$")
+
+    private fun isPreservableTag(value: String): Boolean =
+        value.isNotBlank() &&
+            !value.equals("Off", ignoreCase = true) &&
+            !value.equals("Default", ignoreCase = true) &&
+            TAG_SHAPE.matches(value) &&
+            canonicalSubtitleLanguage(value) != null
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@shared/src/commonMain/kotlin/org/siloserver/silo/model/settings/LanguageOptions.kt`
around lines 82 - 87, Hoist the language-tag Regex used by isPreservableTag into
a reusable class-level or file-level constant so picker construction does not
recompile it for each candidate. Preserve the existing validation behavior,
including support for both hyphen and underscore separators; do not change add
normalization unless required elsewhere.

64-66: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Display labels are used as the identity for wire resolution. Localized labels are CLDR-derived and locale-dependent, so they are not a reliable key: colliding labels resolve to the wrong tag and a label formatted under a different default locale falls back to UNSET.

  • shared/src/commonMain/kotlin/org/siloserver/silo/model/settings/LanguageOptions.kt#L64-L66: resolve the selection by index or wire value instead of matching it.second == label.
  • shared/src/commonTest/kotlin/org/siloserver/silo/model/settings/LanguageOptionsTest.kt#L64-L81: once the API is wire/index-keyed, assert the round trip on wire values so the test no longer depends on the test JVM's default locale.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@shared/src/commonMain/kotlin/org/siloserver/silo/model/settings/LanguageOptions.kt`
around lines 64 - 66, The wireValue function in
shared/src/commonMain/kotlin/org/siloserver/silo/model/settings/LanguageOptions.kt
lines 64-66 must resolve selections by stable wire value or index, not the
localized display label; update its callers/API as needed while preserving UNSET
for invalid selections. Update
shared/src/commonTest/kotlin/org/siloserver/silo/model/settings/LanguageOptionsTest.kt
lines 64-81 to verify round trips using wire values and remove dependence on the
default locale.
shared/src/androidMain/kotlin/org/siloserver/silo/model/settings/LanguagePresentation.android.kt (1)

11-28: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Memoize the ISO-3 → ISO-2 lookup; the dead isEmpty() branch can go.

Each 3-letter tag scans all ~188 Locale.getISOLanguages() entries and constructs a Locale per candidate inside runCatching, and this runs once per picker candidate. split never yields an empty list, so line 13 is unreachable.

♻️ Proposed change
+private val iso3ToIso2: Map<String, String> by lazy {
+    Locale.getISOLanguages().mapNotNull { two ->
+        runCatching { Locale(two).isO3Language }.getOrNull()
+            ?.takeIf { it.isNotEmpty() }
+            ?.lowercase(Locale.ROOT)
+            ?.let { it to two }
+    }.toMap()
+}
+
 internal actual fun canonicalLanguageIdentity(tag: String): String {
     val parts = tag.replace('_', '-').split('-').toMutableList()
-    if (parts.isEmpty()) return tag.lowercase(Locale.ROOT)
-
     val primary = parts.first().lowercase(Locale.ROOT)
-    val canonicalPrimary = if (primary.length == 3) {
-        Locale.getISOLanguages().firstOrNull { twoLetter ->
-            runCatching {
-                Locale.forLanguageTag(twoLetter).isO3Language.equals(primary, ignoreCase = true)
-            }
-                .getOrDefault(false)
-        } ?: primary
-    } else {
-        primary
-    }
-    parts[0] = canonicalPrimary
+    parts[0] = if (primary.length == 3) iso3ToIso2[primary] ?: primary else primary
     return parts.joinToString("-").lowercase(Locale.ROOT)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@shared/src/androidMain/kotlin/org/siloserver/silo/model/settings/LanguagePresentation.android.kt`
around lines 11 - 28, Update canonicalLanguageIdentity so the ISO-3-to-ISO-2
lookup is memoized and reused across calls instead of scanning
Locale.getISOLanguages() and constructing candidate Locales for every 3-letter
tag. Remove the unreachable parts.isEmpty() branch, while preserving the
existing fallback to the primary tag and canonical lowercase output.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@shared/src/androidMain/kotlin/org/siloserver/silo/model/settings/LanguagePresentation.android.kt`:
- Around line 11-28: Update canonicalLanguageIdentity so the ISO-3-to-ISO-2
lookup is memoized and reused across calls instead of scanning
Locale.getISOLanguages() and constructing candidate Locales for every 3-letter
tag. Remove the unreachable parts.isEmpty() branch, while preserving the
existing fallback to the primary tag and canonical lowercase output.

In
`@shared/src/androidUnitTest/kotlin/org/siloserver/silo/model/settings/SettingsConformanceTest.kt`:
- Around line 190-207: Extend
generatedPresentationMetadataMatchesTheVendoredManifest to also iterate manifest
definitions and verify that every definition containing suggested_options or
unset_label has a corresponding entry in
SettingPresentationMetadata.DEFINITIONS, reusing the existing generated-binding
coverage assertion pattern where appropriate.

In
`@shared/src/commonMain/kotlin/org/siloserver/silo/model/settings/LanguageOptions.kt`:
- Around line 82-87: Hoist the language-tag Regex used by isPreservableTag into
a reusable class-level or file-level constant so picker construction does not
recompile it for each candidate. Preserve the existing validation behavior,
including support for both hyphen and underscore separators; do not change add
normalization unless required elsewhere.
- Around line 64-66: The wireValue function in
shared/src/commonMain/kotlin/org/siloserver/silo/model/settings/LanguageOptions.kt
lines 64-66 must resolve selections by stable wire value or index, not the
localized display label; update its callers/API as needed while preserving UNSET
for invalid selections. Update
shared/src/commonTest/kotlin/org/siloserver/silo/model/settings/LanguageOptionsTest.kt
lines 64-81 to verify round trips using wire values and remove dependence on the
default locale.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c23b2936-9e7f-4d32-9ad1-65f5053df2ce

📥 Commits

Reviewing files that changed from the base of the PR and between 83e23da and 21d70af.

📒 Files selected for processing (19)
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/settings/PlaybackSettings.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/settings/SettingsScreen.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/settings/SettingsViewModel.kt
  • androidApp/src/androidMain/kotlin/org/siloserver/silo/android/ui/screens/settings/SubtitleSettings.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/settings/TvSettingsScreen.kt
  • androidTvApp/src/androidMain/kotlin/org/siloserver/silo/tv/ui/screens/settings/TvSettingsViewModel.kt
  • shared/src/androidMain/kotlin/org/siloserver/silo/model/settings/LanguagePresentation.android.kt
  • shared/src/androidUnitTest/kotlin/org/siloserver/silo/model/settings/SettingsConformanceTest.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/domain/settings/ProfileSettingsController.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/model/settings/LanguageOptions.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/model/settings/SettingKeys.kt
  • shared/src/commonMain/kotlin/org/siloserver/silo/model/settings/SettingValueModels.kt
  • shared/src/commonTest/kotlin/org/siloserver/silo/domain/settings/ProfileSettingsControllerTest.kt
  • shared/src/commonTest/kotlin/org/siloserver/silo/model/settings/LanguageOptionsTest.kt
  • shared/src/commonTest/kotlin/org/siloserver/silo/model/settings/SettingsManifest.kt
  • shared/src/commonTest/kotlin/org/siloserver/silo/network/api/SettingsApiValuesTest.kt
  • shared/src/commonTest/resources/settings/v1/SOURCE
  • shared/src/commonTest/resources/settings/v1/conformance.json
  • shared/src/commonTest/resources/settings/v1/manifest.json

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 21d70afada

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +22 to +24
} ?: primary
} else {
primary

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Canonicalize two-letter ISO aliases before labeling

When a current or runtime value uses a deprecated two-letter alias such as iw, this branch preserves iw as a different identity from the generated he option, even though Android's Locale normalizes both and displays both as “Hebrew.” Both phone and TV therefore render duplicate labels, and wireValue() resolves either label to the first row (he), so the exact current iw row cannot round-trip as intended. Normalize two-letter aliases through Locale.forLanguageTag(...).language before deduplication.

Useful? React with 👍 / 👎.

Comment on lines +86 to 87
Regex("^[a-zA-Z]{2,3}([-_][a-zA-Z0-9]{1,8})*$").matches(value) &&
canonicalSubtitleLanguage(value) != null

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve valid open-ended BCP 47 values

When the server or another client supplies und, which is a valid BCP 47 language tag, this predicate rejects it because canonicalSubtitleLanguage() deliberately treats und as null for track-selection semantics. The picker consequently omits the exact current value and displays the unset label even though the server still holds a nonempty setting, violating the open language_tag behavior this adapter is intended to preserve. Validate setting tags independently of subtitle track-selection normalization.

Useful? React with 👍 / 👎.

@Quick104
Quick104 merged commit ad3524b into main Jul 30, 2026
2 checks passed
@Quick104
Quick104 deleted the codex/settings-language-catalog branch July 30, 2026 22:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant