Skip to content

Commit 0577561

Browse files
Add multi language support for labels (#1071)
![Screenshot 2025-02-11 at 3 09 23 PM](https://github.com/user-attachments/assets/5a65588a-41a6-4fef-90ea-e1b5c32911cb) Add multi language support for labels by: - Create a Translations Helper that: - Read language of the browser - Set language for the browser or set english as default - Add string languages files for: - English (Default) - Spanish - Portuguese - French --------- Co-authored-by: Wesley Luyten <[email protected]>
1 parent ba3c9fe commit 0577561

32 files changed

+430
-182
lines changed

docs/src/pages/landing/shared/data.tsx

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
} from "media-chrome/react";
1818
import type { MediaChromeListItem } from "../../../types";
1919
// @ts-ignore
20-
import { labels, timeUtils } from "media-chrome";
20+
import { timeUtils, t } from "media-chrome";
2121

2222
const { formatAsTimePhrase } = timeUtils;
2323

@@ -29,7 +29,7 @@ const mediaChromeListItems: MediaChromeListItem[] = [
2929
component: MediaController,
3030
a11y: {
3131
role: "region",
32-
"aria-label": labels.VIDEO_PLAYER(),
32+
"aria-label": t("video player"),
3333
},
3434
},
3535
{
@@ -43,7 +43,7 @@ const mediaChromeListItems: MediaChromeListItem[] = [
4343
component: MediaPlayButton,
4444
a11y: {
4545
role: "button",
46-
"aria-label": labels.PLAY(),
46+
"aria-label": t("play"),
4747
},
4848
},
4949
{
@@ -52,7 +52,7 @@ const mediaChromeListItems: MediaChromeListItem[] = [
5252
component: MediaSeekForwardButton,
5353
a11y: {
5454
role: "button",
55-
"aria-label": labels.SEEK_FORWARD_N_SECS({ seekOffset: 30 }),
55+
"aria-label": t("seek forward {seekOffset} seconds", { seekOffset: 30 }),
5656
},
5757
},
5858
{
@@ -61,7 +61,7 @@ const mediaChromeListItems: MediaChromeListItem[] = [
6161
component: MediaSeekBackwardButton,
6262
a11y: {
6363
role: "button",
64-
"aria-label": labels.SEEK_BACK_N_SECS({ seekOffset: 30 }),
64+
"aria-label": t("seek back {seekOffset} seconds", { seekOffset: 30 }),
6565
},
6666
},
6767
{
@@ -70,7 +70,7 @@ const mediaChromeListItems: MediaChromeListItem[] = [
7070
component: MediaMuteButton,
7171
a11y: {
7272
role: "button",
73-
"aria-label": labels.MUTE(),
73+
"aria-label": t("mute"),
7474
},
7575
},
7676
{
@@ -79,7 +79,7 @@ const mediaChromeListItems: MediaChromeListItem[] = [
7979
component: MediaVolumeRange,
8080
a11y: {
8181
role: "slider",
82-
"aria-label": labels.VOLUME(),
82+
"aria-label": t("volume"),
8383
"aria-valuetext": `${65}%`,
8484
},
8585
},
@@ -90,7 +90,7 @@ const mediaChromeListItems: MediaChromeListItem[] = [
9090
component: MediaTimeRange,
9191
a11y: {
9292
role: "slider",
93-
"aria-label": labels.SEEK(),
93+
"aria-label": t("seek"),
9494
"aria-valuetext": `${formatAsTimePhrase(-234)} of ${formatAsTimePhrase(
9595
360
9696
)}`,
@@ -109,7 +109,7 @@ const mediaChromeListItems: MediaChromeListItem[] = [
109109
a11y: {
110110
role: "slider",
111111
// NOTE: Should add a label for media-time-display in media-chrome!
112-
"aria-label": labels.PLAYBACK_TIME(),
112+
"aria-label": t("playback time"),
113113
"aria-valuetext": `${formatAsTimePhrase(-234)} of ${formatAsTimePhrase(
114114
360
115115
)}`,
@@ -122,7 +122,7 @@ const mediaChromeListItems: MediaChromeListItem[] = [
122122
component: MediaCaptionsButton,
123123
a11y: {
124124
role: "switch",
125-
"aria-label": labels.CLOSED_CAPTIONS(),
125+
"aria-label": t("closed captions"),
126126
},
127127
},
128128
{
@@ -131,7 +131,7 @@ const mediaChromeListItems: MediaChromeListItem[] = [
131131
component: MediaPlaybackRateButton,
132132
a11y: {
133133
role: "button",
134-
"aria-label": labels.PLAYBACK_RATE({ playbackRate: 1 }),
134+
"aria-label": t("Playback rate {playbackRate}", { playbackRate: 1 }),
135135
},
136136
},
137137
{
@@ -141,7 +141,7 @@ const mediaChromeListItems: MediaChromeListItem[] = [
141141
component: MediaPipButton,
142142
a11y: {
143143
role: "button",
144-
"aria-label": labels.ENTER_PIP(),
144+
"aria-label": t("Enter picture in picture mode"),
145145
},
146146
},
147147
{
@@ -150,7 +150,7 @@ const mediaChromeListItems: MediaChromeListItem[] = [
150150
component: MediaFullscreenButton,
151151
a11y: {
152152
role: "button",
153-
"aria-label": labels.ENTER_FULLSCREEN(),
153+
"aria-label": t("enter fullscreen mode"),
154154
},
155155
},
156156
];

docs/src/types/index.d.ts

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,5 @@
1-
// declare module 'media-chrome/dist/labels/labels' {
2-
// export interface nouns {
3-
// [k: string]: function({ [k: string]: any }): string;
4-
// };
5-
// export interface verbs {
6-
// [k: string]: function({ [k: string]: any }): string;
7-
// };
8-
// }
1+
92
declare module "media-chrome" {
10-
type labels = {
11-
nouns: {
12-
[k: string]: (
13-
x?: Partial<{ seekOffset: number; playbackRate: number }>
14-
) => string;
15-
};
16-
verbs: {
17-
[k: string]: (
18-
x?: Partial<{ seekOffset: number; playbackRate: number }>
19-
) => string;
20-
};
21-
};
223
}
234

245
export type ARIARole =

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,12 @@
8585
"import": "./dist/*.js",
8686
"require": "./dist/cjs/*.js",
8787
"default": "./dist/*.js"
88+
},
89+
"./lang/*": {
90+
"types": "./dist/lang/*.d.ts",
91+
"import": "./dist/lang/*.js",
92+
"require": "./dist/lang/*.js",
93+
"default": "./dist/lang/*.js"
8894
}
8995
},
9096
"customElements": "dist/custom-elements.json",

src/js/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export * as constants from './constants.js';
2-
export { default as labels } from './labels/labels.js';
32
export * as timeUtils from './utils/time.js';
3+
export { t } from './utils/i18n.js';
44

55
// Import media-controller first to ensure it's available for other components
66
// when calling `associateElement(this)` in connectedCallback.

src/js/labels/labels.ts

Lines changed: 14 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export type LabelOptions = { seekOffset?: number; playbackRate?: number };
1+
import { t } from '../utils/i18n.js';
22

33
export type MediaErrorLike = {
44
code: number;
@@ -7,89 +7,28 @@ export type MediaErrorLike = {
77
};
88

99
const defaultErrorTitles = {
10-
2: 'Network Error',
11-
3: 'Decode Error',
12-
4: 'Source Not Supported',
13-
5: 'Encryption Error',
10+
2: t('Network Error'),
11+
3: t('Decode Error'),
12+
4: t('Source Not Supported'),
13+
5: t('Encryption Error'),
1414
};
1515

1616
const defaultErrorMessages = {
17-
2: 'A network error caused the media download to fail.',
18-
3: 'A media error caused playback to be aborted. The media could be corrupt or your browser does not support this format.',
19-
4: 'An unsupported error occurred. The server or network failed, or your browser does not support this format.',
20-
5: 'The media is encrypted and there are no keys to decrypt it.',
17+
2: t('A network error caused the media download to fail.'),
18+
3: t(
19+
'A media error caused playback to be aborted. The media could be corrupt or your browser does not support this format.'
20+
),
21+
4: t(
22+
'An unsupported error occurred. The server or network failed, or your browser does not support this format.'
23+
),
24+
5: t('The media is encrypted and there are no keys to decrypt it.'),
2125
};
2226

2327
// Returning null makes the error not show up in the UI.
2428
export const formatError = (error: MediaErrorLike) => {
2529
if (error.code === 1) return null;
2630
return {
2731
title: defaultErrorTitles[error.code] ?? `Error ${error.code}`,
28-
message: defaultErrorMessages[error.code] ?? error.message
32+
message: defaultErrorMessages[error.code] ?? error.message,
2933
};
30-
}
31-
32-
export const tooltipLabels = {
33-
ENTER_AIRPLAY: 'Start airplay',
34-
EXIT_AIRPLAY: 'Stop airplay',
35-
AUDIO_TRACK_MENU: 'Audio',
36-
CAPTIONS: 'Captions',
37-
ENABLE_CAPTIONS: 'Enable captions',
38-
DISABLE_CAPTIONS: 'Disable captions',
39-
START_CAST: 'Start casting',
40-
STOP_CAST: 'Stop casting',
41-
ENTER_FULLSCREEN: 'Enter fullscreen mode',
42-
EXIT_FULLSCREEN: 'Exit fullscreen mode',
43-
MUTE: 'Mute',
44-
UNMUTE: 'Unmute',
45-
ENTER_PIP: 'Enter picture in picture mode',
46-
EXIT_PIP: 'Enter picture in picture mode',
47-
PLAY: 'Play',
48-
PAUSE: 'Pause',
49-
PLAYBACK_RATE: 'Playback rate',
50-
RENDITIONS: 'Quality',
51-
SEEK_BACKWARD: 'Seek backward',
52-
SEEK_FORWARD: 'Seek forward',
53-
SETTINGS: 'Settings',
54-
};
55-
56-
export const nouns: Record<string, (options?: LabelOptions) => string> = {
57-
AUDIO_PLAYER: () => 'audio player',
58-
VIDEO_PLAYER: () => 'video player',
59-
VOLUME: () => 'volume',
60-
SEEK: () => 'seek',
61-
CLOSED_CAPTIONS: () => 'closed captions',
62-
PLAYBACK_RATE: ({ playbackRate = 1 } = {}) =>
63-
`current playback rate ${playbackRate}`,
64-
PLAYBACK_TIME: () => `playback time`,
65-
MEDIA_LOADING: () => `media loading`,
66-
SETTINGS: () => `settings`,
67-
AUDIO_TRACKS: () => `audio tracks`,
68-
QUALITY: () => `quality`,
69-
};
70-
71-
export const verbs: Record<string, (options?: LabelOptions) => string> = {
72-
PLAY: () => 'play',
73-
PAUSE: () => 'pause',
74-
MUTE: () => 'mute',
75-
UNMUTE: () => 'unmute',
76-
ENTER_AIRPLAY: () => 'start airplay',
77-
EXIT_AIRPLAY: () => 'stop airplay',
78-
ENTER_CAST: () => 'start casting',
79-
EXIT_CAST: () => 'stop casting',
80-
ENTER_FULLSCREEN: () => 'enter fullscreen mode',
81-
EXIT_FULLSCREEN: () => 'exit fullscreen mode',
82-
ENTER_PIP: () => 'enter picture in picture mode',
83-
EXIT_PIP: () => 'exit picture in picture mode',
84-
SEEK_FORWARD_N_SECS: ({ seekOffset = 30 } = {}) =>
85-
`seek forward ${seekOffset} seconds`,
86-
SEEK_BACK_N_SECS: ({ seekOffset = 30 } = {}) =>
87-
`seek back ${seekOffset} seconds`,
88-
SEEK_LIVE: () => 'seek to live',
89-
PLAYING_LIVE: () => 'playing live',
90-
};
91-
92-
export default {
93-
...nouns,
94-
...verbs,
9534
};

src/js/lang/en.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
export const En = {
2+
'Start airplay': 'Start airplay',
3+
'Stop airplay': 'Stop airplay',
4+
Audio: 'Audio',
5+
Captions: 'Captions',
6+
'Enable captions': 'Enable captions',
7+
'Disable captions': 'Disable captions',
8+
'Start casting': 'Start casting',
9+
'Stop casting': 'Stop casting',
10+
'Enter fullscreen mode': 'Enter fullscreen mode',
11+
'Exit fullscreen mode': 'Exit fullscreen mode',
12+
Mute: 'Mute',
13+
Unmute: 'Unmute',
14+
'Enter picture in picture mode': 'Enter picture in picture mode',
15+
'Exit picture in picture mode': 'Exit picture in picture mode',
16+
Play: 'Play',
17+
Pause: 'Pause',
18+
'Playback rate': 'Playback rate',
19+
'Playback rate {playbackRate}': 'Playback rate {playbackRate}',
20+
Quality: 'Quality',
21+
'Seek backward': 'Seek backward',
22+
'Seek forward': 'Seek forward',
23+
Settings: 'Settings',
24+
'audio player': 'audio player',
25+
'video player': 'video player',
26+
volume: 'volume',
27+
seek: 'seek',
28+
'closed captions': 'closed captions',
29+
'current playback rate': 'current playback rate',
30+
'playback time': 'playback time',
31+
'media loading': 'media loading',
32+
33+
settings: 'settings',
34+
'audio tracks': 'audio tracks',
35+
quality: 'quality',
36+
play: 'play',
37+
pause: 'pause',
38+
mute: 'mute',
39+
unmute: 'unmute',
40+
live: 'live',
41+
'start airplay': 'start airplay',
42+
'stop airplay': 'stop airplay',
43+
'start casting': 'start casting',
44+
'stop casting': 'stop casting',
45+
'enter fullscreen mode': 'enter fullscreen mode',
46+
'exit fullscreen mode': 'exit fullscreen mode',
47+
'enter picture in picture mode': 'enter picture in picture mode',
48+
'exit picture in picture mode': 'exit picture in picture mode',
49+
'seek to live': 'seek to live',
50+
'playing live': 'playing live',
51+
'seek back {seekOffset} seconds': 'seek back {seekOffset} seconds',
52+
'seek forward {seekOffset} seconds': 'seek forward {seekOffset} seconds',
53+
54+
'Network Error': 'Network Error',
55+
'Decode Error': 'Decode Error',
56+
'Source Not Supported': 'Source Not Supported',
57+
'Encryption Error': 'Encryption Error',
58+
'A network error caused the media download to fail.':
59+
'A network error caused the media download to fail.',
60+
'A media error caused playback to be aborted. The media could be corrupt or your browser does not support this format.':
61+
'A media error caused playback to be aborted. The media could be corrupt or your browser does not support this format.',
62+
'An unsupported error occurred. The server or network failed, or your browser does not support this format.':
63+
'An unsupported error occurred. The server or network failed, or your browser does not support this format.',
64+
'The media is encrypted and there are no keys to decrypt it.':
65+
'The media is encrypted and there are no keys to decrypt it.',
66+
} as const;
67+
68+
export type TranslateKeys = keyof typeof En;
69+
70+
export type TranslateDictionary = {
71+
[key in TranslateKeys]: string;
72+
};

0 commit comments

Comments
 (0)