Skip to content

fix(i18n): remove hardcoded strings; enforce mandatory pre-PR review - #232

Merged
yuga-hashimoto merged 5 commits into
mainfrom
fix-i18n-hardcoded-ja
Aug 9, 2026
Merged

fix(i18n): remove hardcoded strings; enforce mandatory pre-PR review#232
yuga-hashimoto merged 5 commits into
mainfrom
fix-i18n-hardcoded-ja

Conversation

@yuga-hashimoto

@yuga-hashimoto yuga-hashimoto commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Summary

Two related changes:

1. Remove hardcoded strings and locale-specific behavior (fix(i18n))

  • Speech recognition no longer defaults to ja-JP; callers must pass the user's locale tag, so dictation can never silently fall back to the wrong language. Both call sites already pass the device locale.
  • The session-handoff prompt and its role labels were hardcoded Japanese sent to the model; they are now English, consistent with the codebase and every user locale.
  • Slash-command descriptions were hardcoded English even though slash_desc_* resources already existed in all eight locale files; the registry now carries @StringRes ids and the composer resolves them.
  • Localized the remaining hardcoded user-visible messages — "Config saved", "Clone failed (exit code …)", MCP "Authentication was not removed" / "Authentication failed: …", and "Provider disconnect was not accepted" — adding five new string keys translated in all eight locale files (en, ar, es, fr, ja, pt-rBR, ru, zh-rCN).
  • The diagnostics sheet showed placeholder values ("0.3.0", "connected", "ready"); it now reports BuildConfig.VERSION_NAME and the real selected-runtime state.
  • Deleted the unused ThemePickerDialog (dead code full of hardcoded strings) and switched the drawer preview's sample data to English.

2. Make pre-PR review mandatory (ci)

  • New pre-pr-review skill (.opencode/skills/pre-pr-review/SKILL.md): run local gates, have the repo-reviewer subagent review the full diff against origin/main, fix blocking findings until APPROVE, and record the report in the PR description.
  • New repo-reviewer subagent (.opencode/agents/repo-reviewer.md): read-only reviewer whose prompt embeds this repository's architecture map and hard rules (i18n key parity across all eight locale files, Koin/ViewModelFactory DI wiring, CI gates), so it reviews like a maintainer who knows the repo.
  • New PR Review Gate workflow: fails any PR (except Weblate's automated translation PRs) whose description lacks the <!-- pre-pr-review: approved --> marker, so the gate is enforced by CI, not just convention.
  • AGENTS.md now requires the review before any PR is opened.

Verification

  • ./gradlew detekt spotlessCheck passes locally.
  • i18n key parity verified with the same logic as i18n-check.yml: all keys present in all eight locale files, no extras.
  • Full compile/tests run in CI (x86_64 aapt2 cannot run on this arm64 device).
  • This PR itself went through the new review flow; the report is below.

Pre-PR review

Reviewer: repo-reviewer subagent (invoked via the pre-pr-review skill workflow). Verdict below is for the i18n + review-mechanism changes; the final commit applies the reviewer's own two non-blocking suggestions (Connecting state on the diagnostics connection row; KDoc on SlashSuggestion.App.description).

判定: APPROVE

## ブロッカー
- なし

## 提案(非ブロッキング)
- app/src/main/java/com/yugahashimoto/andcode/ui/AndCodeApp.kt:1211 — connectionStatus は RuntimeState.Connecting 中でも "Disconnected" 表示になる(runtimeStatus 側のみ "Starting…")。接続状態ラベルとしても runtime_status_starting を使う方が実態に合う。 → 適用済み(9d4bf5e)
- app/src/main/java/com/yugahashimoto/andcode/feature/chat/SlashCommandRegistry.kt:36 — SlashSuggestion.App.description = "" は、将来別のUIが description を直接読むと空文字になる潜在リスク。 → KDoc で明記済み(9d4bf5e)

## チェック済み項目
- フルdiff(2コミット・25ファイル)を全hunk精読。
- startListening(language) の必須引数化: 呼び出し元は SpeechRecognizerManager を使う2箇所のみで、AndCodeApp.kt:336(speechLocaleTag)と AndCodeVoiceSession.kt:319(speechLanguageTag)とも明示的に言語を渡すことを確認。QuickInputActivity:228 はプラットフォームの SpeechRecognizer.startListening(intent)、WakeWordService は同名の別メソッドで影響なし。
- SessionHandoff.kt の英語化と SessionHandoffTest の期待値一致を確認(ヘッダー・User/Assistant役割・空リスト時のヘッダー返却)。
- SlashCommandRegistry: slash_desc_* 6キーが全8ロケールに存在すること、SlashSuggestion の利用箇所が ChatHomeScreen のみで stringResource 解決が@Composableスコープ内であり import も存在すること、テスト(ChatViewModelTest:261)は Backend サジェストのみ使用で影響なしを確認。
- ViewModelメッセージ注入パターンの両構築箇所の同期確認: SettingsViewModel は di/ViewModelModule.kt:35 と AndCodeApp.kt:226 の両方で providerDisconnectRejectedMessage を渡す。McpViewModel は McpScreen.kt:67 のみ(テストはデフォルト値で構築、プライマリ/セカンダリコンストラクタともデフォルト持ちでコンパイル可)。
- DiagnosticsSheet 呼び出し: RuntimeTarget.state: StateFlow<RuntimeState> に対し when が null + 5サブタイプ全てを網羅(exhaustive)。connected_label/disconnected_label/runtime_status_starting/connected_version が全8ロケールに存在し、connected_version は全ロケールで %s 一致。BuildConfig は app/build.gradle.kts:174 で有効。必要な import は全て AndCodeApp.kt に存在。
- GithubCloneDialog: workspace_clone_failed が全8ロケールで %1$d を持ち、format(result.exitCode)(Int)と適合。
- i18nキー整合: CI(i18n-check.yml)と同一ロジックをローカルで実行し、必須キーが全7ロケール+ソースで欠落・余剰なし。新規5キーのフォーマット引数も全ロケール一致。
- AppearanceSettings.kt 削除: ThemePickerDialog/ThemeColorPreview の参照がリポジトリ全体にゼロであることを確認。
- pr-review-gate.yml: YAMLとして妥当、edited で再実行、weblate除外、マーカー検出ロジックが SKILL.md/AGENTS.md の指示と一致。
- Kotlin静的検証: 型・import・exhaustive when・composable内 stringResource のいずれもコンパイルエラー要因なし(実機でコンパイル不可のため静的確認)。

Incremental re-reviews (post-approval fixups)

Two fixup commits landed after the main review; each was re-reviewed by the same reviewer workflow:

  1. 8787e27 (collect runtime state instead of StateFlow.value in composition, fixing the lintDebug StateFlowValueCalledInComposition error) — re-review verdict REQUEST_CHANGES: by collectAsState() delegation blocks smart casts in the when branches (confirmed by CI compile errors on lines 1224–1226).
  2. 09aae4e — fixes that blocker by binding collectAsState().value to a local val and dropping the dead null branch. Re-review verdict APPROVE, no blockers. Full CI green on this HEAD.

- speech recognition no longer defaults to ja-JP; callers must pass a locale
- session handoff prompt and role labels switched from Japanese to English
- slash command descriptions now use the existing slash_desc_* resources
- localize snackbar/error messages (config saved, clone failed, MCP auth,
  provider disconnect) with translations in all eight locale files
- diagnostics sheet shows real version/runtime state instead of placeholders
- delete unused ThemePickerDialog with hardcoded strings
- English sample data in drawer preview
- add the pre-pr-review skill: local gates, repo-reviewer verdict loop,
  review report recorded in the PR description
- add the repo-reviewer subagent with an embedded map of this repository
  and its hard rules (i18n key parity, DI wiring, CI gates)
- add the PR Review Gate workflow that fails PRs without an approved
  review report (Weblate bot PRs exempt)
- AGENTS.md now requires the review before opening any PR
- show Starting… for the connection row while the runtime is connecting
- document that SlashSuggestion.App.description is empty by design
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

🔍 OpenCodeReview found 1 issue(s) in this PR.

  • ✅ Successfully posted inline: 1 comment(s)

pull_request:
# "edited" re-runs the gate when the PR description changes, so the review report can be
# added after the PR exists and the check turns green without a new push.
types: [opened, edited, synchronize, reopened]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[other · low]
synchronizeedited トリガーにより連続 push で同じジョブが重複実行されます。このジョブは PR ごとに結果が決まるゲートなので、concurrency によるグループ化と cancel-in-progress: true を設定し、直前の実行をキャンセルして冗長な実行を防ぐことを推奨します。

Suggestion:

Suggested change
types: [opened, edited, synchronize, reopened]
concurrency:
group: pr-review-gate-${{ github.event.pull_request.number }}
cancel-in-progress: true
on:
pull_request:
# "edited" re-runs the gate when the PR description changes, so the review report can be
# added after the PR exists and the check turns green without a new push.
types: [opened, edited, synchronize, reopened]


jobs:
review-gate:
runs-on: ubuntu-latest

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[other · low]
このジョブには timeout-minutes が設定されていません。grep のみの軽量な処理とはいえ、ランナー上でステップがハングすると無期限に実行され、ランナーリソースを消費し続けます。timeout-minutes: 5 程度を明示することを推奨します。

Suggestion:

Suggested change
runs-on: ubuntu-latest
runs-on: ubuntu-latest
timeout-minutes: 5

Comment on lines +70 to +71
authNotRemovedMessage = context.getString(R.string.mcp_auth_not_removed),
authFailedTemplate = context.getString(R.string.mcp_auth_failed_status),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[maintainability · low]
viewModel(key = ...) は Key 単位でインスタンスをキャッシュするため、この Factory は ViewModel が初回生成されるときに一度だけ実行され、context.getString(...) で解決した文字列はそのまま ViewModel に保持され続けます。システム言語を変更して Activity が再生成されても ViewModel は破棄されず、以後のエラーメッセージが以前の言語のまま表示される可能性があります(特にナビゲーションで画面を離れて戻った場合もキャッシュが再利用されます)。実行時のロケール切替を考慮する場合は、文字列を ViewModel 生成時に固定するのではなく、表示時に再解決する(例: エラー表示側で resource ID を基に解決する)設計を検討してください。現状のままでも機能的には問題ないため、対応は任意です。

…n composition

lintDebug flags StateFlowValueCalledInComposition for the diagnostics
sheet; collect the selected runtime's state flow instead.
Comment on lines +3 to +7
on:
pull_request:
# "edited" re-runs the gate when the PR description changes, so the review report can be
# added after the PR exists and the check turns green without a new push.
types: [opened, edited, synchronize, reopened]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[performance · medium]
PRイベント(opened/edited/synchronize/reopened)で起動するこのジョブにはconcurrency設定がありません。連続プッシュや説明文編集のたびに同一のゲートチェックが並列実行され、リソースと実行時間を無駄に消費します。concurrencyグループとcancel-in-progress: trueを追加し、同じPRの古い実行をキャンセルすることを推奨します。

Suggestion:

Suggested change
on:
pull_request:
# "edited" re-runs the gate when the PR description changes, so the review report can be
# added after the PR exists and the check turns green without a new push.
types: [opened, edited, synchronize, reopened]
concurrency:
group: pr-review-gate-${{ github.event.pull_request.number }}
cancel-in-progress: true
on:
pull_request:
# "edited" re-runs the gate when the PR description changes, so the review report can be
# added after the PR exists and the check turns green without a new push.
types: [opened, edited, synchronize, reopened]

Comment on lines +12 to +14
jobs:
review-gate:
runs-on: ubuntu-latest

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[other · low]
ジョブにtimeout-minutesが設定されていません。万が一run:スクリプトがハングした場合、ジョブが無期限に実行されランナーリソースを占有します。単純なゲートチェックなので、数分のタイムアウトを設定して上限を明示することを推奨します。

Suggestion:

Suggested change
jobs:
review-gate:
runs-on: ubuntu-latest
jobs:
review-gate:
runs-on: ubuntu-latest
timeout-minutes: 5

Comment on lines 36 to 40
data class App(val command: SlashCommand) : SlashSuggestion {
override val name: String = command.name
override val description: String = command.description
override val description: String = ""
override val descriptionRes: Int = command.descriptionRes
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[maintainability · low]
Appdescription を常に空文字でオーバーライドしていますが、インターフェース SlashSuggestiondescription: String を必須の抽象プロパティとして公開し続けています。descriptionRes の解決は UI 層でしか行われないため、description を直接参照する将来のコード(テスト・ログ・他機能など)はアプリコマンドで黙って空文字を表示してしまいます。descriptionString? にしデフォルトで null(App はオーバーライド不要)とし、Backend のみリテラル文字列を持つ設計にすると、このダミー空文字を排除でき、安全かつ簡潔になります。

Suggestion:

Suggested change
data class App(val command: SlashCommand) : SlashSuggestion {
override val name: String = command.name
override val description: String = command.description
override val description: String = ""
override val descriptionRes: Int = command.descriptionRes
}
data class App(val command: SlashCommand) : SlashSuggestion {
override val name: String = command.name
override val descriptionRes: Int = command.descriptionRes
}

Comment on lines +57 to +59
authNotRemovedMessage: String = "Authentication was not removed",
authFailedTemplate: String = "Authentication failed: %1\$s",
) : this(registry::targetFor, agent, authNotRemovedMessage, authFailedTemplate)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[maintainability · low]
プライマリコンストラクタとセカンダリコンストラクタの両方に同じデフォルトメッセージ文字列が重複して定義されています。今後デフォルト文言を変更する際に両方の修正が必要になり、片方だけ変更すると挙動が乖離するリスクがあります。companion object の const val に共通定義し、両コンストラクタで参照する形にすると重複を回避できます。

if (showDiagnostics) {
val runtimeStateFlow =
remember(selectedRuntime) {
selectedRuntime?.state ?: MutableStateFlow(RuntimeState.Disconnected)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[maintainability · low]
selectedRuntime が null の場合に作成される MutableStateFlow(RuntimeState.Disconnected) は、以後どこからも更新されることがない実質的な定数フローです。flowOf(RuntimeState.Disconnected) を使うほうが意図が明確になり、このためだけに追加した MutableStateFlow import も不要になります。

Suggestion:

Suggested change
selectedRuntime?.state ?: MutableStateFlow(RuntimeState.Disconnected)
selectedRuntime?.state ?: flowOf(RuntimeState.Disconnected)

},
runtimeStatus =
when (runtimeState) {
null, RuntimeState.Disconnected -> stringResource(R.string.disconnected_label)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[maintainability · low]
runtimeStateStateFlow<RuntimeState> に対して collectAsState() を呼んだ結果なので常に非 null です。したがって whennull 分岐は到達不能なデッドコードです。null, を削除して RuntimeState.Disconnected のみにしてください。

Suggestion:

Suggested change
null, RuntimeState.Disconnected -> stringResource(R.string.disconnected_label)
RuntimeState.Disconnected -> stringResource(R.string.disconnected_label)

by-delegation from collectAsState() blocks smart casting in the when
branches; bind State.value to a local val instead and drop the now-dead
null branch.
Comment on lines +12 to +16
jobs:
review-gate:
runs-on: ubuntu-latest
# Weblate opens automated translation PRs; they cannot run the agent-side review skill.
if: github.event.pull_request.user.login != 'weblate'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[performance · low]
このワークフローは pull_requestsynchronize(プッシュ毎)と edited(説明文の編集毎)で再実行されます。開発中にコミットを連続でプッシュすると、内容のほぼ同一なジョブが並列にキューされ、ランナーリソースを無駄に消費します。プルリクエスト単位の concurrency グループと cancel-in-progress: true を追加し、最新のジョブだけを実行することを推奨します。

Suggestion:

Suggested change
jobs:
review-gate:
runs-on: ubuntu-latest
# Weblate opens automated translation PRs; they cannot run the agent-side review skill.
if: github.event.pull_request.user.login != 'weblate'
concurrency:
group: pr-review-gate-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
review-gate:
runs-on: ubuntu-latest
# Weblate opens automated translation PRs; they cannot run the agent-side review skill.
if: github.event.pull_request.user.login != 'weblate'

@yuga-hashimoto
yuga-hashimoto merged commit ea1e8cc into main Aug 9, 2026
8 checks passed
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