refactor: load what's-new content from per-version JSON files#497
Conversation
β¦nager Compose Resources doesn't extend qualifier-folder resolution to its raw 'files' directory, so localized variants now live as plain subfolders (files/whatsnew/<locale>/<vc>.json) and the loader walks the candidates itself: full BCP-47 code, primary language code, then the English file at files/whatsnew/<vc>.json. The current locale comes from LocalizationManager so the in-app language override picks the right translation, not just the OS locale.
Adds files/whatsnew/<locale>/{15,16}.json for ar, bn, es, fr, hi, it,
ja, ko, pl, ru, tr, zh-CN. Brand and platform names (GitHub, APK,
Shizuku, OAuth, Linux, macOS, Compose Multiplatform, deb, rpm,
AppImage, Personal Access Token, SHA-256) are preserved untranslated.
Native speakers are welcome to refine any locale via follow-up PRs;
English remains the fallback for any locale that has not translated a
given release.
WalkthroughThis PR refactors the "What's New" feature from hardcoded data to a resource-driven system. It replaces a static ChangesWhat's New: Resource-Driven Architecture
Estimated code review effortπ― 3 (Moderate) | β±οΈ ~25 minutes Possibly related PRs
Poem
π₯ Pre-merge checks | β 4 | β 1β Failed checks (1 warning)
β Passed checks (4 passed)
βοΈ Tip: You can configure your own custom pre-merge checks in the settings. β¨ Finishing Touchesπ Generate docstrings
π§ͺ Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Review rate limit: 7/8 reviews remaining, refill in 7 minutes and 30 seconds.Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
π§Ή Nitpick comments (1)
composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/whatsnew/WhatsNewViewModel.kt (1)
79-91: β‘ Quick win
forceShowLatest()redundantly re-reads all JSON files whenhistoryEntriesis already cached.
_historyEntries.valueis populated duringinitfrom the sameloadAll()call. WhenforceShowLatest()is invoked from the UI,_historyEntrieswill almost certainly be populated, making the fallbackloadAll()call unnecessary file I/O. A small guard handles the rare early-call race window:β»οΈ Proposed refactor
- ?: whatsNewLoader.loadAll().firstOrNull() + ?: _historyEntries.value.firstOrNull() + ?: whatsNewLoader.loadAll().firstOrNull()π€ Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/whatsnew/WhatsNewViewModel.kt` around lines 79 - 91, forceShowLatest() currently calls whatsNewLoader.loadAll() even though the cache _historyEntries.value is populated in init; change the logic in the viewModelScope.launch block to first consult _historyEntries.value (or its backed property) and use whatsNewLoader.forVersionCode(current) against that cached list, falling back to calling whatsNewLoader.loadAll() only if the cached _historyEntries is null or empty to avoid redundant file I/O; update references in the try block around whatsNewLoader.forVersionCode(current), _historyEntries, and _pendingEntry to use the cached entries first and keep the existing catch behavior.
π€ Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/WhatsNewEntryMapper.kt`:
- Around line 18-22: The current WhatsNewSectionDto.toDomain() uses
WhatsNewSectionType.valueOf(...) which throws on unknown names and can cause
entire entries to be dropped; change WhatsNewSectionDto.toDomain() to return a
nullable WhatsNewSection? and perform a safe lookup like
WhatsNewSectionType.entries.find { it.name.equals(type.trim(), ignoreCase =
true) } (or similar) to resolve the enum; if the lookup returns null, return
null from toDomain() so callers can filter out only the malformed section
instead of the whole entry; update any call sites of
WhatsNewSectionDto.toDomain() to filterNulls()/remove nulls accordingly when
building WhatsNewSection lists.
---
Nitpick comments:
In
`@composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/whatsnew/WhatsNewViewModel.kt`:
- Around line 79-91: forceShowLatest() currently calls whatsNewLoader.loadAll()
even though the cache _historyEntries.value is populated in init; change the
logic in the viewModelScope.launch block to first consult _historyEntries.value
(or its backed property) and use whatsNewLoader.forVersionCode(current) against
that cached list, falling back to calling whatsNewLoader.loadAll() only if the
cached _historyEntries is null or empty to avoid redundant file I/O; update
references in the try block around whatsNewLoader.forVersionCode(current),
_historyEntries, and _pendingEntry to use the cached entries first and keep the
existing catch behavior.
πͺ Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
βΉοΈ Review info
βοΈ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: d77e59ea-9693-48de-8e9b-4d7cc6ba599e
π Files selected for processing (39)
composeApp/build.gradle.ktscomposeApp/src/commonMain/kotlin/zed/rainxch/githubstore/Main.ktcomposeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/WhatsNewModule.ktcomposeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/di/initKoin.ktcomposeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.ktcomposeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/whatsnew/WhatsNewLoaderImpl.ktcomposeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/whatsnew/WhatsNewViewModel.ktcore/data/src/commonMain/kotlin/zed/rainxch/core/data/dto/WhatsNewEntryDto.ktcore/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/WhatsNewEntryMapper.ktcore/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/WhatsNewEntries.ktcore/domain/src/commonMain/kotlin/zed/rainxch/core/domain/repository/WhatsNewLoader.ktcore/presentation/src/commonMain/composeResources/files/whatsnew/15.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/README.mdcore/presentation/src/commonMain/composeResources/files/whatsnew/ar/15.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/ar/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/bn/15.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/bn/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/es/15.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/es/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/fr/15.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/fr/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/hi/15.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/hi/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/it/15.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/it/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/ja/15.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/ja/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/ko/15.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/ko/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/pl/15.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/pl/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/ru/15.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/ru/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/tr/15.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/tr/16.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/zh-CN/15.jsoncore/presentation/src/commonMain/composeResources/files/whatsnew/zh-CN/16.jsoncore/presentation/src/commonMain/kotlin/zed/rainxch/core/presentation/components/whatsnew/WhatsNewHistoryScreen.kt
π€ Files with no reviewable changes (1)
- core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/WhatsNewEntries.kt
| fun WhatsNewSectionDto.toDomain(): WhatsNewSection = | ||
| WhatsNewSection( | ||
| type = WhatsNewSectionType.valueOf(type.trim().uppercase()), | ||
| bullets = bullets, | ||
| ) |
There was a problem hiding this comment.
π§© Analysis chain
π Script executed:
# First, let's verify the file exists and check its package declaration
find . -name "WhatsNewEntryMapper.kt" -type fRepository: OpenHub-Store/GitHub-Store
Length of output: 157
π Script executed:
# Also look for the WhatsNewSectionType enum
find . -name "*.kt" -type f | xargs grep -l "enum.*WhatsNewSectionType"Repository: OpenHub-Store/GitHub-Store
Length of output: 153
π Script executed:
# Check the directory structure to verify source set organization
find . -path "*/core/data/src/*" -type d | head -20Repository: OpenHub-Store/GitHub-Store
Length of output: 1155
π Script executed:
cat -n ./core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/WhatsNewEntryMapper.ktRepository: OpenHub-Store/GitHub-Store
Length of output: 969
π Script executed:
cat -n ./core/domain/src/commonMain/kotlin/zed/rainxch/core/domain/model/WhatsNewEntry.ktRepository: OpenHub-Store/GitHub-Store
Length of output: 627
π Script executed:
# Search for all calls to toDomain() involving WhatsNewSection
rg "toDomain\(\)" -B2 -A2 core/data/src/Repository: OpenHub-Store/GitHub-Store
Length of output: 15884
π Script executed:
# Search for WhatsNewLoaderImpl and loadOrNull
rg -l "WhatsNewLoaderImpl|loadOrNull" --type ktRepository: OpenHub-Store/GitHub-Store
Length of output: 97
π Script executed:
# Find JSON files related to WhatsNew
find . -name "*.json" -type f | xargs grep -l "WhatsNew\|whatsnew" 2>/dev/null | head -10Repository: OpenHub-Store/GitHub-Store
Length of output: 52
π Script executed:
# Search for test files related to WhatsNew
find . -path "*/test*" -name "*WhatsNew*.kt" -type fRepository: OpenHub-Store/GitHub-Store
Length of output: 52
π Script executed:
# Search for WhatsNewLoaderImpl without type filter
rg "WhatsNewLoaderImpl|WhatsNewLoader" -lRepository: OpenHub-Store/GitHub-Store
Length of output: 497
π Script executed:
# Find where DTOs are loaded/parsed
rg "WhatsNewSectionDto|WhatsNewEntryDto" -B3 -A3 | head -80Repository: OpenHub-Store/GitHub-Store
Length of output: 6029
π Script executed:
# Look for exception handling around valueOf or enum parsing
rg "catch|try.*valueOf" core/data/src/Repository: OpenHub-Store/GitHub-Store
Length of output: 19237
π Script executed:
# Read the WhatsNew README
cat ./core/presentation/src/commonMain/composeResources/files/whatsnew/README.mdRepository: OpenHub-Store/GitHub-Store
Length of output: 2821
π Script executed:
# Look at WhatsNewLoaderImpl more thoroughly
cat -n ./composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/whatsnew/WhatsNewLoaderImpl.ktRepository: OpenHub-Store/GitHub-Store
Length of output: 2810
π Script executed:
# Check if there are actual JSON files for whatsnew
find . -path "*/whatsnew/*" -name "*.json" -type fRepository: OpenHub-Store/GitHub-Store
Length of output: 2098
π Script executed:
# Quick verification of the exact flow - confirm that a bad section type at line 20
# would propagate and be caught by WhatsNewLoaderImpl
cat -n ./composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/whatsnew/WhatsNewLoaderImpl.kt | grep -A5 "readEntry"Repository: OpenHub-Store/GitHub-Store
Length of output: 566
valueOf() throws on any unrecognized section type, silently dropping entire entries.
valueOf() throws an IllegalArgumentException on JVM if the enum constant is not found; on JavaScript and Kotlin/Native, the exception type may differ (IllegalStateException or plain Exception), making a broad catch at the loader level unreliable across Compose Multiplatform targets.
Since JSON files are community-translated (12+ locales documented in the README), any typo or mistake in a type field will cause readEntry() to catch the exception and return null, losing the entire release entry instead of just the malformed section.
Replace valueOf with a safe lookup using WhatsNewSectionType.entries.find, and make toDomain() nullable so callers can filter unknown sections without losing the whole entry:
π‘οΈ Proposed fix
-fun WhatsNewSectionDto.toDomain(): WhatsNewSection =
+fun WhatsNewSectionDto.toDomain(): WhatsNewSection? {
+ val sectionType = WhatsNewSectionType.entries
+ .find { it.name == type.trim().uppercase() }
+ ?: return null
WhatsNewSection(
- type = WhatsNewSectionType.valueOf(type.trim().uppercase()),
+ type = sectionType,
bullets = bullets,
)
+}Update the entry mapper to filter out unknown sections:
sections = sections.map { it.toDomain() },
+sections = sections.mapNotNull { it.toDomain() },π€ Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@core/data/src/commonMain/kotlin/zed/rainxch/core/data/mappers/WhatsNewEntryMapper.kt`
around lines 18 - 22, The current WhatsNewSectionDto.toDomain() uses
WhatsNewSectionType.valueOf(...) which throws on unknown names and can cause
entire entries to be dropped; change WhatsNewSectionDto.toDomain() to return a
nullable WhatsNewSection? and perform a safe lookup like
WhatsNewSectionType.entries.find { it.name.equals(type.trim(), ignoreCase =
true) } (or similar) to resolve the enum; if the lookup returns null, return
null from toDomain() so callers can filter out only the malformed section
instead of the whole entry; update any call sites of
WhatsNewSectionDto.toDomain() to filterNulls()/remove nulls accordingly when
building WhatsNewSection lists.
Follow-up to #496.
Why
The hardcoded
WhatsNewEntriesobject grew with every release and offered no path for community translators to contribute. With one entry per release, the file would balloon over time; localization required either Kotlin churn from translators or shipping English-only forever.This PR moves the content out of code and into per-version JSON files under Compose Resources, with locale-qualified directories that let translators contribute incrementally without ever touching Kotlin.
What changed
WhatsNewLoaderinterface incore/domain+WhatsNewLoaderImplincomposeApp/.../whatsnew/readsfiles/whatsnew/<versionCode>.jsonviaRes.readBytes. Compose Resources resolves the locale-qualified variant first (files-zh-rCN/whatsnew/<vc>.json) and falls back to the default English file when a translation is missing.WhatsNewEntryDto+toDomain()mapper incore/datakeep the domain model framework-free.KnownWhatsNewVersionCodes.ALLinWhatsNewLoaderImpl.ktenumerates the known versions β one line touched per release. Picked over anindex.jsonfor type safety + zero extra parsing.core/presentation/.../composeResources/files/whatsnew/15.jsonand16.json.WhatsNewViewModelnow consumes the loader; exposeshistoryEntries: StateFlow<List<WhatsNewEntry>>andhasHistory: StateFlow<Boolean>so consumers can observe load state without blocking on suspend calls.WhatsNewHistoryScreentakesentries: List<WhatsNewEntry>as a parameter β the AppNavigation route collects from the activity-scopedWhatsNewViewModeland passes them in, with the existing empty-state covering the brief loading window.WhatsNewEntriesobject is deleted.core/presentation/.../composeResources/files/whatsnew/README.md.Per-release author workflow (after this PR)
core/presentation/src/commonMain/composeResources/files/whatsnew/<versionCode>.json.versionCodetoKnownWhatsNewVersionCodes.ALL.Translator workflow
files/whatsnew/<versionCode>.jsontofiles-<locale>/whatsnew/<versionCode>.json.versionCode,versionName,releaseDate,showAsSheet, sectiontype) untouched.Test plan
16.json, evaluator hides sheet on fresh install (existing behavior preserved).16.json.whatsnew/<vc>.jsonβ loader logs the parse failure and silently skips that version, app does not crash.Summary by CodeRabbit
Release Notes