refactor(subtitles): simplify state machine and improve loading display#981
refactor(subtitles): simplify state machine and improve loading display#981
Conversation
- Consolidate 6 subtitle states into 3 (idle/loading/error) - Move loading message to bottom of video, error stays centered - Add "Read Frog" prefix to loading text in all 8 locales - Add display-rules module for subtitle visibility logic - Show loading only when no renderable subtitle for current cue Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 148a3ac The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
PR Review: refactor(subtitles): simplify state machine and improve loading displayOverall AssessmentClean refactor. Reducing 6 states to 3 and extracting pure display rules is the right direction — it eliminates special cases and makes the state machine easier to reason about. Code QualityGood:
Suggestions:
Potential Issues
Performance
SecurityNo security concerns. Changes are purely UI/state management with no external input handling, network calls, or storage changes. Test Coverage
i18n
Summary
The redundant |
There was a problem hiding this comment.
6 issues found across 17 files
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/locales/en.yml">
<violation number="1" location="src/locales/en.yml:645">
P3: The new loading message is missing the "Read Frog |" prefix/format, so it doesn’t match the branded status string format and reads awkwardly in English.</violation>
</file>
<file name="src/entrypoints/subtitles.content/translation-coordinator.ts">
<violation number="1" location="src/entrypoints/subtitles.content/translation-coordinator.ts:87">
P2: The new per-tick updateLoadingStateAt call overwrites `error` with `idle`/`loading` on the next timeupdate, so error messages won't persist (the scheduler’s 5s auto-hide is bypassed). Consider skipping loading-state updates while an error is active or preserving the error state until it expires.</violation>
<violation number="2" location="src/entrypoints/subtitles.content/translation-coordinator.ts:87">
P3: This `updateLoadingStateAt` call is also redundant for the same reason - the caller already invoked it with identical parameters. Consider removing both redundant calls inside `translateNearby` and keeping only the post-translation call (which uses fresh `getCurrentVideoTimeMs` and `getFragments()` values after async work completes).</violation>
</file>
<file name="src/locales/zh-TW.yml">
<violation number="1" location="src/locales/zh-TW.yml:644">
P3: Prefix should match the required "Read Frog |" format for subtitle status messages.</violation>
</file>
<file name="src/locales/ru.yml">
<violation number="1" location="src/locales/ru.yml:643">
P3: Loading message misses the required `Read Frog |` prefix format. This breaks the branding consistency described in the PR.</violation>
</file>
<file name="src/locales/ja.yml">
<violation number="1" location="src/locales/ja.yml:641">
P3: Japanese loading message is missing the "Read Frog |" prefix separator; it should match the new branding format used for other locales.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| const currentTimeMs = video.currentTime * 1000 | ||
| const fragments = this.getFragments() | ||
|
|
||
| this.updateLoadingStateAt(currentTimeMs, fragments) |
There was a problem hiding this comment.
P2: The new per-tick updateLoadingStateAt call overwrites error with idle/loading on the next timeupdate, so error messages won't persist (the scheduler’s 5s auto-hide is bypassed). Consider skipping loading-state updates while an error is active or preserving the error state until it expires.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/entrypoints/subtitles.content/translation-coordinator.ts, line 87:
<comment>The new per-tick updateLoadingStateAt call overwrites `error` with `idle`/`loading` on the next timeupdate, so error messages won't persist (the scheduler’s 5s auto-hide is bypassed). Consider skipping loading-state updates while an error is active or preserving the error state until it expires.</comment>
<file context>
@@ -81,21 +81,19 @@ export class TranslationCoordinator {
+ const currentTimeMs = video.currentTime * 1000
+ const fragments = this.getFragments()
+
+ this.updateLoadingStateAt(currentTimeMs, fragments)
+
if (this.segmentationPipeline && !this.segmentationPipeline.isRunning
</file context>
- Remove redundant updateLoadingStateAt calls in translateNearby - Remove dead idle entry from STATE_CONFIG (unreachable code) - Track lastEmittedState to skip redundant onStateChange calls Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
PR Review: refactor(subtitles): simplify state machine and improve loading displayOverall Assessment🟢 Good taste. This is a well-motivated simplification — collapsing 6 states into 3 removes accidental complexity that didn't serve the user. The extracted Net -100 deletions across 17 files for equivalent (better) behavior. That's a good refactor. Positive Highlights
Issues & Suggestions1. Potential stale state after error recovery (Medium)
This means after an error:
The Suggestion: Either track error state in 2.
|
| Area | Verdict |
|---|---|
| Code quality | 🟢 Clean separation, good use of pure functions |
| Bugs | 🟡 Error-to-idle transition timing needs verification (see #1) |
| Performance | 🟢 lastEmittedState dedup is a net improvement |
| Security | 🟢 No concerns |
| Tests | 🟡 display-rules well tested; coordinator state transitions untested |
The PR is in good shape. The error state lifecycle (issue #1) is the main thing worth investigating before merge — the rest are minor polish items.
There was a problem hiding this comment.
2 issues found across 2 files (changes from recent commits).
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/entrypoints/subtitles.content/translation-coordinator.ts">
<violation number="1" location="src/entrypoints/subtitles.content/translation-coordinator.ts:170">
P2: This method may prematurely clear error states. The `nextState` calculation only considers 'loading' or 'idle', but if an external error state was set (e.g., from the catch block in `translateNearby`), calling `updateLoadingStateAt` on the next tick will overwrite it with 'idle'. Consider checking if the current state is 'error' and preserving it, or track error states in `lastEmittedState` when they're set.</violation>
<violation number="2" location="src/entrypoints/subtitles.content/translation-coordinator.ts:174">
P2: Error states can become sticky because lastEmittedState is not updated when onStateChange('error') is emitted, so updateLoadingStateAt may suppress transitions back to idle/loading if the nextState matches the pre-error lastEmittedState.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| ? 'loading' | ||
| : 'idle' | ||
|
|
||
| if (nextState === this.lastEmittedState) |
There was a problem hiding this comment.
P2: Error states can become sticky because lastEmittedState is not updated when onStateChange('error') is emitted, so updateLoadingStateAt may suppress transitions back to idle/loading if the nextState matches the pre-error lastEmittedState.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/entrypoints/subtitles.content/translation-coordinator.ts, line 174:
<comment>Error states can become sticky because lastEmittedState is not updated when onStateChange('error') is emitted, so updateLoadingStateAt may suppress transitions back to idle/loading if the nextState matches the pre-error lastEmittedState.</comment>
<file context>
@@ -168,17 +167,15 @@ export class TranslationCoordinator {
- if (!this.translatedStarts.has(activeCue.start)) {
- this.onStateChange('loading')
+ if (nextState === this.lastEmittedState)
return
- }
</file context>
| private updateLoadingStateAt(timeMs: number, fragments: SubtitlesFragment[]) { | ||
| const activeCue = this.findActiveCue(timeMs, fragments) | ||
|
|
||
| const nextState: SubtitlesState = activeCue && !this.translatedStarts.has(activeCue.start) |
There was a problem hiding this comment.
P2: This method may prematurely clear error states. The nextState calculation only considers 'loading' or 'idle', but if an external error state was set (e.g., from the catch block in translateNearby), calling updateLoadingStateAt on the next tick will overwrite it with 'idle'. Consider checking if the current state is 'error' and preserving it, or track error states in lastEmittedState when they're set.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/entrypoints/subtitles.content/translation-coordinator.ts, line 170:
<comment>This method may prematurely clear error states. The `nextState` calculation only considers 'loading' or 'idle', but if an external error state was set (e.g., from the catch block in `translateNearby`), calling `updateLoadingStateAt` on the next tick will overwrite it with 'idle'. Consider checking if the current state is 'error' and preserving it, or track error states in `lastEmittedState` when they're set.</comment>
<file context>
@@ -168,17 +167,15 @@ export class TranslationCoordinator {
- this.onStateChange('idle')
- return
- }
+ const nextState: SubtitlesState = activeCue && !this.translatedStarts.has(activeCue.start)
+ ? 'loading'
+ : 'idle'
</file context>
Type of Changes
Description
idle,loading,active), removing redundant intermediate states (fetching-subtitles,waiting-for-cue,translating)display-rules.tsmodule with pure functions (shouldShowLoading,shouldShowSubtitles) to decouple display logic from state managementbottom-20instead of center, so it doesn't block video contentHow Has This Been Tested?
Unit tests for
display-rules.tscovering all state × translation combinations (8 test cases).All checks pass:
pnpm type-check✓pnpm test— 554 tests passedpnpm lint— clean (only pre-existing sidebar warning)Checklist
Summary by cubic
Simplifies the subtitle state to idle/loading/error and refactors display logic so loading shows only when the current cue has no renderable subtitle, with loading at the bottom and error centered.
Written for commit 148a3ac. Summary will update on new commits.