Skip to content

Commit 1856b0d

Browse files
sampottscursoragent
andcommitted
feat(packages): opaque translation keys in core ui with duration formatting
Use TranslationKeyOrString for control labels, centralize optional label resolution, and expose formatOptions for formatDuration-based copy across time controls and sliders; extend utils time helpers used by core and tests. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent ea4835a commit 1856b0d

48 files changed

Lines changed: 470 additions & 262 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/core/src/core/i18n/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ export type TranslationParams = {
5757
mediaErrorCustom: never;
5858
};
5959

60+
/**
61+
* Either a known translation id from {@link TranslationParams}, or any other string the platform may
62+
* use as copy (custom overlay key, literal text, etc.).
63+
*/
64+
export type TranslationKeyOrString = keyof TranslationParams | (string & {});
65+
6066
/** Placeholder shape for each key that accepts `t(key, params)`. Omitting a key here is a type error when defining strings. */
6167
type ParametricTranslations = {
6268
seekForwardSeconds: Contains<'{seconds}'>;

packages/core/src/core/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export * from './ui/popover/popover-data-attrs';
4646
export * from './ui/popover/popup-host-attr';
4747
export * from './ui/poster/poster-core';
4848
export * from './ui/poster/poster-data-attrs';
49+
export * from './ui/resolve-optional-control-label';
4950
export * from './ui/seek-button/seek-button-core';
5051
export * from './ui/seek-button/seek-button-data-attrs';
5152
export * from './ui/slider/slider-core';

packages/core/src/core/ui/captions-button/captions-button-core.ts

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { createState } from '@videojs/store';
22
import { defaults } from '@videojs/utils/object';
3-
import { isFunction } from '@videojs/utils/predicate';
43
import type { NonNullableObject } from '@videojs/utils/types';
54

65
import type { MediaTextTrackState } from '../../media/state';
7-
import type { ButtonState } from '../types';
6+
import { resolveOptionalControlLabel } from '../resolve-optional-control-label';
7+
import type { ButtonState, TranslationKeyOrString } from '../types';
88

99
export interface CaptionsButtonProps {
1010
/** Custom label for the button. */
11-
label?: string | ((state: CaptionsButtonState) => string) | undefined;
11+
label?: TranslationKeyOrString | ((state: CaptionsButtonState) => TranslationKeyOrString) | undefined;
1212
/** Whether the button is disabled. */
1313
disabled?: boolean | undefined;
1414
}
@@ -40,17 +40,11 @@ export class CaptionsButtonCore {
4040
this.#props = defaults(props, CaptionsButtonCore.defaultProps);
4141
}
4242

43-
getLabel(state: CaptionsButtonState): string {
44-
const { label } = this.#props;
43+
getLabel(state: CaptionsButtonState): TranslationKeyOrString {
44+
const custom = resolveOptionalControlLabel(this.#props.label, state);
45+
if (custom !== undefined) return custom;
4546

46-
if (isFunction(label)) {
47-
const customLabel = label(state);
48-
if (customLabel) return customLabel;
49-
} else if (label) {
50-
return label;
51-
}
52-
53-
return state.subtitlesShowing ? 'Disable captions' : 'Enable captions';
47+
return state.subtitlesShowing ? 'disableCaptions' : 'enableCaptions';
5448
}
5549

5650
getAttrs(state: CaptionsButtonState) {

packages/core/src/core/ui/captions-button/tests/captions-button-core.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,12 @@ describe('CaptionsButtonCore', () => {
6666
describe('getLabel', () => {
6767
it('returns Enable captions when captions are disabled', () => {
6868
const core = new CaptionsButtonCore();
69-
expect(core.getLabel(createState({ subtitlesShowing: false }))).toBe('Enable captions');
69+
expect(core.getLabel(createState({ subtitlesShowing: false }))).toBe('enableCaptions');
7070
});
7171

7272
it('returns Disable captions when captions are enabled', () => {
7373
const core = new CaptionsButtonCore();
74-
expect(core.getLabel(createState({ subtitlesShowing: true }))).toBe('Disable captions');
74+
expect(core.getLabel(createState({ subtitlesShowing: true }))).toBe('disableCaptions');
7575
});
7676

7777
it('returns custom string label', () => {
@@ -91,7 +91,7 @@ describe('CaptionsButtonCore', () => {
9191
it('returns aria-label', () => {
9292
const core = new CaptionsButtonCore();
9393
const attrs = core.getAttrs(createState({ subtitlesShowing: false }));
94-
expect(attrs['aria-label']).toBe('Enable captions');
94+
expect(attrs['aria-label']).toBe('enableCaptions');
9595
});
9696

9797
it('sets aria-disabled when disabled', () => {

packages/core/src/core/ui/cast-button/cast-button-core.ts

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { createState } from '@videojs/store';
22
import { defaults } from '@videojs/utils/object';
3-
import { isFunction } from '@videojs/utils/predicate';
43
import type { NonNullableObject } from '@videojs/utils/types';
54

65
import type { MediaRemotePlaybackState, RemotePlaybackConnectionState } from '../../media/state';
76
import type { MediaFeatureAvailability } from '../../media/types';
8-
import type { ButtonState } from '../types';
7+
import { resolveOptionalControlLabel } from '../resolve-optional-control-label';
8+
import type { ButtonState, TranslationKeyOrString } from '../types';
99

1010
export interface CastButtonProps {
1111
/** Custom label for the button. */
12-
label?: string | ((state: CastButtonState) => string) | undefined;
12+
label?: TranslationKeyOrString | ((state: CastButtonState) => TranslationKeyOrString) | undefined;
1313
/** Whether the button is disabled. */
1414
disabled?: boolean | undefined;
1515
}
@@ -42,19 +42,13 @@ export class CastButtonCore {
4242
this.#props = defaults(props, CastButtonCore.defaultProps);
4343
}
4444

45-
getLabel(state: CastButtonState): string {
46-
const { label } = this.#props;
45+
getLabel(state: CastButtonState): TranslationKeyOrString {
46+
const custom = resolveOptionalControlLabel(this.#props.label, state);
47+
if (custom !== undefined) return custom;
4748

48-
if (isFunction(label)) {
49-
const customLabel = label(state);
50-
if (customLabel) return customLabel;
51-
} else if (label) {
52-
return label;
53-
}
54-
55-
if (state.castState === 'connected') return 'Stop casting';
56-
if (state.castState === 'connecting') return 'Connecting';
57-
return 'Start casting';
49+
if (state.castState === 'connected') return 'stopCasting';
50+
if (state.castState === 'connecting') return 'connectingCast';
51+
return 'startCasting';
5852
}
5953

6054
getAttrs(state: CastButtonState) {

packages/core/src/core/ui/cast-button/tests/cast-button-core.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,17 +78,17 @@ describe('CastButtonCore', () => {
7878
describe('getLabel', () => {
7979
it('returns Start casting when disconnected', () => {
8080
const core = new CastButtonCore();
81-
expect(core.getLabel(createState({ castState: 'disconnected' }))).toBe('Start casting');
81+
expect(core.getLabel(createState({ castState: 'disconnected' }))).toBe('startCasting');
8282
});
8383

8484
it('returns Stop casting when connected', () => {
8585
const core = new CastButtonCore();
86-
expect(core.getLabel(createState({ castState: 'connected' }))).toBe('Stop casting');
86+
expect(core.getLabel(createState({ castState: 'connected' }))).toBe('stopCasting');
8787
});
8888

8989
it('returns Connecting when connecting', () => {
9090
const core = new CastButtonCore();
91-
expect(core.getLabel(createState({ castState: 'connecting' }))).toBe('Connecting');
91+
expect(core.getLabel(createState({ castState: 'connecting' }))).toBe('connectingCast');
9292
});
9393

9494
it('returns custom string label', () => {
@@ -108,7 +108,7 @@ describe('CastButtonCore', () => {
108108
it('returns aria-label', () => {
109109
const core = new CastButtonCore();
110110
const attrs = core.getAttrs(createState());
111-
expect(attrs['aria-label']).toBe('Start casting');
111+
expect(attrs['aria-label']).toBe('startCasting');
112112
});
113113

114114
it('sets aria-disabled when disabled', () => {

packages/core/src/core/ui/fullscreen-button/fullscreen-button-core.ts

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { createState } from '@videojs/store';
22
import { defaults } from '@videojs/utils/object';
3-
import { isFunction } from '@videojs/utils/predicate';
43
import type { NonNullableObject } from '@videojs/utils/types';
54

65
import type { MediaFullscreenState } from '../../media/state';
7-
import type { ButtonState } from '../types';
6+
import { resolveOptionalControlLabel } from '../resolve-optional-control-label';
7+
import type { ButtonState, TranslationKeyOrString } from '../types';
88

99
export interface FullscreenButtonProps {
1010
/** Custom label for the button. */
11-
label?: string | ((state: FullscreenButtonState) => string) | undefined;
11+
label?: TranslationKeyOrString | ((state: FullscreenButtonState) => TranslationKeyOrString) | undefined;
1212
/** Whether the button is disabled. */
1313
disabled?: boolean | undefined;
1414
}
@@ -41,17 +41,11 @@ export class FullscreenButtonCore {
4141
this.#props = defaults(props, FullscreenButtonCore.defaultProps);
4242
}
4343

44-
getLabel(state: FullscreenButtonState): string {
45-
const { label } = this.#props;
44+
getLabel(state: FullscreenButtonState): TranslationKeyOrString {
45+
const custom = resolveOptionalControlLabel(this.#props.label, state);
46+
if (custom !== undefined) return custom;
4647

47-
if (isFunction(label)) {
48-
const customLabel = label(state);
49-
if (customLabel) return customLabel;
50-
} else if (label) {
51-
return label;
52-
}
53-
54-
return state.fullscreen ? 'Exit fullscreen' : 'Enter fullscreen';
48+
return state.fullscreen ? 'exitFullscreen' : 'enterFullscreen';
5549
}
5650

5751
getAttrs(state: FullscreenButtonState) {

packages/core/src/core/ui/fullscreen-button/tests/fullscreen-button-core.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,12 @@ describe('FullscreenButtonCore', () => {
4848
describe('getLabel', () => {
4949
it('returns Enter fullscreen when not fullscreen', () => {
5050
const core = new FullscreenButtonCore();
51-
expect(core.getLabel(createState({ fullscreen: false }))).toBe('Enter fullscreen');
51+
expect(core.getLabel(createState({ fullscreen: false }))).toBe('enterFullscreen');
5252
});
5353

5454
it('returns Exit fullscreen when fullscreen', () => {
5555
const core = new FullscreenButtonCore();
56-
expect(core.getLabel(createState({ fullscreen: true }))).toBe('Exit fullscreen');
56+
expect(core.getLabel(createState({ fullscreen: true }))).toBe('exitFullscreen');
5757
});
5858

5959
it('returns custom string label', () => {
@@ -73,7 +73,7 @@ describe('FullscreenButtonCore', () => {
7373
it('returns aria-label', () => {
7474
const core = new FullscreenButtonCore();
7575
const attrs = core.getAttrs(createState());
76-
expect(attrs['aria-label']).toBe('Enter fullscreen');
76+
expect(attrs['aria-label']).toBe('enterFullscreen');
7777
});
7878

7979
it('sets aria-disabled when disabled', () => {

packages/core/src/core/ui/live-button/live-button-core.ts

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import { createState } from '@videojs/store';
22
import { defaults } from '@videojs/utils/object';
3-
import { isFunction } from '@videojs/utils/predicate';
43
import type { NonNullableObject } from '@videojs/utils/types';
54

65
import type { MediaBufferState, MediaLiveState, MediaTimeState } from '../../media/state';
7-
import type { ButtonState } from '../types';
6+
import { resolveOptionalControlLabel } from '../resolve-optional-control-label';
7+
import type { ButtonState, TranslationKeyOrString } from '../types';
88

99
export interface LiveButtonProps {
1010
/** Custom label for the button. */
11-
label?: string | ((state: LiveButtonState) => string) | undefined;
11+
label?: TranslationKeyOrString | ((state: LiveButtonState) => TranslationKeyOrString) | undefined;
1212
/** Whether the button is disabled. */
1313
disabled?: boolean | undefined;
1414
}
@@ -82,18 +82,12 @@ export class LiveButtonCore {
8282
this.#props = defaults(props, LiveButtonCore.defaultProps);
8383
}
8484

85-
getLabel(state: LiveButtonState): string {
86-
const { label } = this.#props;
85+
getLabel(state: LiveButtonState): TranslationKeyOrString {
86+
const custom = resolveOptionalControlLabel(this.#props.label, state);
87+
if (custom !== undefined) return custom;
8788

88-
if (isFunction(label)) {
89-
const customLabel = label(state);
90-
if (customLabel) return customLabel;
91-
} else if (label) {
92-
return label;
93-
}
94-
95-
if (state.liveEdge) return 'Playing live';
96-
return 'Seek to live edge';
89+
if (state.liveEdge) return 'playingLive';
90+
return 'seekToLiveEdge';
9791
}
9892

9993
getAttrs(state: LiveButtonState) {

packages/core/src/core/ui/live-button/tests/live-button-core.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,12 +148,12 @@ describe('LiveButtonCore', () => {
148148
describe('getLabel', () => {
149149
it('returns "Seek to live edge" when behind live', () => {
150150
const core = new LiveButtonCore();
151-
expect(core.getLabel(createState({ live: true, liveEdge: false }))).toBe('Seek to live edge');
151+
expect(core.getLabel(createState({ live: true, liveEdge: false }))).toBe('seekToLiveEdge');
152152
});
153153

154154
it('returns "Playing live" when at live edge', () => {
155155
const core = new LiveButtonCore();
156-
expect(core.getLabel(createState({ live: true, liveEdge: true }))).toBe('Playing live');
156+
expect(core.getLabel(createState({ live: true, liveEdge: true }))).toBe('playingLive');
157157
});
158158

159159
it('returns custom string label', () => {

0 commit comments

Comments
 (0)