From 9909876a61adce40d659cd9853a62dfe0267698a Mon Sep 17 00:00:00 2001 From: VampireChicken12 Date: Sun, 12 Nov 2023 09:44:03 -0500 Subject: [PATCH] feat: i18n support This update adds internationalization support --- .vscode/settings.json | 3 +- nodemon.json | 8 +- public/locales/de-DE.json | 218 ++++++ public/locales/en-US.json | 218 ++++++ public/locales/en-US.json.d.ts | 175 +++++ public/locales/es-ES.json | 238 +++++++ src/components/Settings/Settings.tsx | 668 +++++++++--------- src/components/SettingsWrapper/index.tsx | 62 +- src/features/featureMenu/index.ts | 4 +- src/features/featureMenu/utils.ts | 29 +- src/features/maximizePlayerButton/index.ts | 2 +- src/features/maximizePlayerButton/utils.ts | 2 +- src/features/playerQuality/index.ts | 4 +- src/features/playerSpeed/index.ts | 3 +- src/features/remainingTime/index.ts | 5 +- src/features/remainingTime/utils.ts | 8 +- src/features/rememberVolume/index.ts | 2 +- src/features/rememberVolume/utils.ts | 2 +- src/features/screenshotButton/index.ts | 5 +- .../scrollWheelVolumeControl/index.ts | 2 +- .../scrollWheelVolumeControl/utils.ts | 2 +- src/features/videoHistory/index.ts | 18 +- src/features/videoHistory/utils.ts | 2 +- src/features/volumeBoost/index.ts | 2 +- src/global.d.ts | 3 + src/i18n/i18n.d.ts | 9 + src/i18n/index.ts | 49 ++ src/manifest.ts | 7 +- src/pages/content/index.tsx | 41 +- src/pages/inject/index.tsx | 25 +- src/utils/constants.ts | 11 +- src/utils/utilities.ts | 2 +- tsconfig.json | 2 +- 33 files changed, 1447 insertions(+), 384 deletions(-) create mode 100644 public/locales/de-DE.json create mode 100644 public/locales/en-US.json create mode 100644 public/locales/en-US.json.d.ts create mode 100644 public/locales/es-ES.json create mode 100644 src/i18n/i18n.d.ts create mode 100644 src/i18n/index.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 63662bfd..06cabbc6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { - "typescript.tsdk": "node_modules\\typescript\\lib" + "typescript.tsdk": "node_modules\\typescript\\lib", + "i18n-ally.localesPaths": ["src/locales"] } diff --git a/nodemon.json b/nodemon.json index 6cd31744..6b2fdbbd 100644 --- a/nodemon.json +++ b/nodemon.json @@ -2,8 +2,8 @@ "env": { "__DEV__": "true" }, - "watch": ["src", "utils", "vite.config.ts", "public"], - "ext": "ts,tsx,css,html", - "ignore": ["src/**/*.spec.ts"], - "exec": "vite build" + "watch": ["src", "utils", "vite.config.ts", "public", "public/locales"], + "ext": "ts,tsx,css,html,json", + "ignore": ["src/**/*.spec.ts", "public/locales/en-US.json.d.ts"], + "exec": "concurrently \"vite build\" -- \"ts-json-as-const public/locales/en-US.json\"" } diff --git a/public/locales/de-DE.json b/public/locales/de-DE.json new file mode 100644 index 00000000..50d8f64a --- /dev/null +++ b/public/locales/de-DE.json @@ -0,0 +1,218 @@ +{ + "langCode": "de-DE", + "langName": "Deutsche", + "pages": { + "content": { + "features": { + "videoHistory": { + "resumePrompt": { "close": "Close" }, + "resumeButton": "Resume" + }, + "screenshotButton": { + "copiedToClipboard": "Screenshot copied to clipboard", + "label": "Screenshot" + }, + "loopButton": { + "label": "Loop" + }, + "maximizePlayerButton": { + "label": "Maximize" + }, + "featureMenu": { + "label": "Feature menu" + } + } + }, + "options": { + "notifications": { + "success": { + "saved": "Options saved." + }, + "error": { + "playerQuality": "You must select a player quality if you want to enable the automatic quality feature." + }, + "info": { + "reset": "All options have been reset to their default values.\nYou can now save the changes by clicking the \"Confirm\" button or discard them by closing this page or ignore this notification." + } + } + } + }, + "settings": { + "sections": { + "importExportSettings": { + "title": "Import/Export Settings", + "importButton": { + "error": { + "validation": "Error importing settings. Please check the file format.\n{{ERROR_MESSAGE}}", + "unknown": "Error importing settings. Please check the file format.\nAn unknown error occurred." + }, + "success": "Settings imported successfully", + "title": "Import settings from a JSON file", + "value": "Import Settings" + }, + "exportButton": { + "success": "Settings successfully exported", + "title": "Export settings to a JSON file", + "value": "Export Settings" + } + }, + "miscellaneous": { + "title": "Miscellaneous settings", + "features": { + "rememberLastVolume": { + "title": "Remembers the volume you were watching at and sets it as the volume when you open a new video", + "label": "Remember last volume" + }, + "maximizePlayerButton": { + "title": "Fills the video to the window size", + "label": "Enable maximize player button" + }, + "videoHistory": { + "title": "Keeps track of where you left off on videos you were watching and asks if you want to resume when that video loads again", + "label": "Enable video history" + }, + "remainingTime": { + "title": "Shows the remaining time of the video you're watching", + "label": "Enable remaining time" + }, + "loopButton": { + "title": "Adds a button to the player to loop the video you're watching", + "label": "Enable loop button" + }, + "hideScrollbar": { + "title": "Hides the pages scrollbar", + "label": "Enable hide scrollbar" + } + } + }, + "scrollWheelVolumeControl": { + "title": "Scroll wheel volume control settings", + "enable": { + "title": "Lets you use the scroll wheel to control the volume of the video you're watching", + "label": "Enable scroll wheel volume control" + }, + "osdColor": { "title": "The color of the On Screen Display", "label": "OSD color" }, + "osdType": { "title": "The type of On Screen Display", "label": "OSD type" }, + "osdPosition": { "title": "The position of the On Screen Display", "label": "OSD position" }, + "osdOpacity": { + "title": "The opacity of the On Screen Display", + "label": "OSD opacity" + }, + "osdVolumeAdjustmentSteps": { "title": "The amount to adjust volume per scroll", "label": "Amount to adjust" }, + "osdHide": { "title": "The amount of milliseconds to wait before hiding the OSD", "label": "Time to hide" }, + "osdPadding": { "title": "The amount of padding to add to the OSD (in pixels, only applies to corner OSD)", "label": "Padding" }, + "onScreenDisplay": { + "colors": { + "red": "Red", + "green": "Green", + "blue": "Blue", + "yellow": "Yellow", + "orange": "Orange", + "purple": "Purple", + "pink": "Pink", + "white": "White" + }, + "position": { + "top_left": "Top Left", + "top_right": "Top Right", + "bottom_left": "Bottom Left", + "bottom_right": "Bottom Right", + "center": "Center" + }, + "type": { + "no_display": "No display", + "text": "Text", + "line": "Line", + "round": "Round" + } + } + }, + "automaticQuality": { + "title": "Automatic quality settings", + "enable": { + "title": "Automatically adjusts the video quality to the selected level.", + "label": "Enable automatic quality adjustment" + }, + "select": { + "label": "Player quality", + "title": "The quality to set the video to" + } + }, + "playbackSpeed": { + "title": "Playback speed settings", + "enable": { + "title": "Sets the video speed to what you choose below", + "label": "Enable forced playback speed" + }, + "select": { + "label": "Player speed", + "title": "The speed to set the video to" + } + }, + "volumeBoost": { + "title": "Volume boost settings", + "enable": { + "title": "Boosts the volume of the video you're watching", + "label": "Enable volume boost" + }, + "number": { + "label": "Volume boost amount (dB)", + "title": "The amount to boost the volume by" + } + }, + "screenshotButton": { + "title": "Screenshot settings", + "enable": { + "title": "Adds a button to the player to take a screenshot of the video", + "label": "Enable screenshot button" + }, + "selectSaveAs": { + "label": "Screenshot save type", + "title": "The screenshot save type" + }, + "selectFormat": { + "label": "Screenshot format", + "title": "The format to save the screenshot in" + }, + "format": { + "png": "PNG", + "jpeg": "JPEG", + "webp": "WebP" + }, + "saveAs": { + "file": "File", + "clipboard": "Clipboard" + } + }, + "bottomButtons": { + "confirm": { + "title": "Confirm setting reset", + "value": "Confirm" + }, + "clear": { + "title": "Clears all data this extension has stored on your machine", + "value": "Clear Data" + }, + "reset": { + "title": "Resets all settings to their defaults, Click the confirm button to save the changes", + "value": "Reset" + } + }, + "language": { + "title": "Language", + "select": { + "label": "Language", + "title": "The language to use for the extension" + } + } + }, + "clearData": { + "confirmAlert": "This will delete all extension data related to options. Continue?", + "allDataDeleted": "All data has been deleted." + } + }, + "messages": { + "settingVolume": "Setting volume boost to {{VOLUME_BOOST_AMOUNT}}", + "resumingVideo": "Resuming video at {{VIDEO_TIME}}" + } +} diff --git a/public/locales/en-US.json b/public/locales/en-US.json new file mode 100644 index 00000000..07b7076d --- /dev/null +++ b/public/locales/en-US.json @@ -0,0 +1,218 @@ +{ + "langCode": "en-US", + "langName": "English (US)", + "pages": { + "content": { + "features": { + "videoHistory": { + "resumePrompt": { "close": "Close" }, + "resumeButton": "Resume" + }, + "screenshotButton": { + "copiedToClipboard": "Screenshot copied to clipboard", + "label": "Screenshot" + }, + "loopButton": { + "label": "Loop" + }, + "maximizePlayerButton": { + "label": "Maximize" + }, + "featureMenu": { + "label": "Feature menu" + } + } + }, + "options": { + "notifications": { + "success": { + "saved": "Options saved." + }, + "error": { + "playerQuality": "You must select a player quality if you want to enable the automatic quality feature." + }, + "info": { + "reset": "All options have been reset to their default values.\nYou can now save the changes by clicking the \"Confirm\" button or discard them by closing this page or ignore this notification." + } + } + } + }, + "settings": { + "sections": { + "importExportSettings": { + "title": "Import/Export Settings", + "importButton": { + "error": { + "validation": "Error importing settings. Please check the file format.\n{{ERROR_MESSAGE}}", + "unknown": "Error importing settings. Please check the file format.\nAn unknown error occurred." + }, + "success": "Settings imported successfully", + "title": "Import settings from a JSON file", + "value": "Import Settings" + }, + "exportButton": { + "success": "Settings successfully exported", + "title": "Export settings to a JSON file", + "value": "Export Settings" + } + }, + "miscellaneous": { + "title": "Miscellaneous settings", + "features": { + "rememberLastVolume": { + "title": "Remembers the volume you were watching at and sets it as the volume when you open a new video", + "label": "Remember last volume" + }, + "maximizePlayerButton": { + "title": "Fills the video to the window size", + "label": "Enable maximize player button" + }, + "videoHistory": { + "title": "Keeps track of where you left off on videos you were watching and asks if you want to resume when that video loads again", + "label": "Enable video history" + }, + "remainingTime": { + "title": "Shows the remaining time of the video you're watching", + "label": "Enable remaining time" + }, + "loopButton": { + "title": "Adds a button to the player to loop the video you're watching", + "label": "Enable loop button" + }, + "hideScrollbar": { + "title": "Hides the pages scrollbar", + "label": "Enable hide scrollbar" + } + } + }, + "scrollWheelVolumeControl": { + "title": "Scroll wheel volume control settings", + "enable": { + "title": "Lets you use the scroll wheel to control the volume of the video you're watching", + "label": "Enable scroll wheel volume control" + }, + "osdColor": { "title": "The color of the On Screen Display", "label": "OSD color" }, + "osdType": { "title": "The type of On Screen Display", "label": "OSD type" }, + "osdPosition": { "title": "The position of the On Screen Display", "label": "OSD position" }, + "osdOpacity": { + "title": "The opacity of the On Screen Display", + "label": "OSD opacity" + }, + "osdVolumeAdjustmentSteps": { "title": "The amount to adjust volume per scroll", "label": "Amount to adjust" }, + "osdHide": { "title": "The amount of milliseconds to wait before hiding the OSD", "label": "Time to hide" }, + "osdPadding": { "title": "The amount of padding to add to the OSD (in pixels, only applies to corner OSD)", "label": "Padding" }, + "onScreenDisplay": { + "colors": { + "red": "Red", + "green": "Green", + "blue": "Blue", + "yellow": "Yellow", + "orange": "Orange", + "purple": "Purple", + "pink": "Pink", + "white": "White" + }, + "position": { + "top_left": "Top Left", + "top_right": "Top Right", + "bottom_left": "Bottom Left", + "bottom_right": "Bottom Right", + "center": "Center" + }, + "type": { + "no_display": "No display", + "text": "Text", + "line": "Line", + "round": "Round" + } + } + }, + "automaticQuality": { + "title": "Automatic quality settings", + "enable": { + "title": "Automatically adjusts the video quality to the selected level.", + "label": "Enable automatic quality adjustment" + }, + "select": { + "label": "Player quality", + "title": "The quality to set the video to" + } + }, + "playbackSpeed": { + "title": "Playback speed settings", + "enable": { + "title": "Sets the video speed to what you choose below", + "label": "Enable forced playback speed" + }, + "select": { + "label": "Player speed", + "title": "The speed to set the video to" + } + }, + "volumeBoost": { + "title": "Volume boost settings", + "enable": { + "title": "Boosts the volume of the video you're watching", + "label": "Enable volume boost" + }, + "number": { + "label": "Volume boost amount (dB)", + "title": "The amount to boost the volume by" + } + }, + "screenshotButton": { + "title": "Screenshot settings", + "enable": { + "title": "Adds a button to the player to take a screenshot of the video", + "label": "Enable screenshot button" + }, + "selectSaveAs": { + "label": "Screenshot save type", + "title": "The screenshot save type" + }, + "selectFormat": { + "label": "Screenshot format", + "title": "The format to save the screenshot in" + }, + "format": { + "png": "PNG", + "jpeg": "JPEG", + "webp": "WebP" + }, + "saveAs": { + "file": "File", + "clipboard": "Clipboard" + } + }, + "bottomButtons": { + "confirm": { + "title": "Confirm setting reset", + "value": "Confirm" + }, + "clear": { + "title": "Clears all data this extension has stored on your machine", + "value": "Clear Data" + }, + "reset": { + "title": "Resets all settings to their defaults, Click the confirm button to save the changes", + "value": "Reset" + } + }, + "language": { + "title": "Language", + "select": { + "label": "Language", + "title": "The language to use for the extension" + } + } + }, + "clearData": { + "confirmAlert": "This will delete all extension data related to options. Continue?", + "allDataDeleted": "All data has been deleted." + } + }, + "messages": { + "settingVolume": "Setting volume boost to {{VOLUME_BOOST_AMOUNT}}", + "resumingVideo": "Resuming video at {{VIDEO_TIME}}" + } +} diff --git a/public/locales/en-US.json.d.ts b/public/locales/en-US.json.d.ts new file mode 100644 index 00000000..9f13a4a5 --- /dev/null +++ b/public/locales/en-US.json.d.ts @@ -0,0 +1,175 @@ +interface EnUS { + langCode: 'en-US', + langName: 'English (US)', + pages: { + content: { + features: { + videoHistory: {resumePrompt: {close: 'Close'}, resumeButton: 'Resume'}, + screenshotButton: {copiedToClipboard: 'Screenshot copied to clipboard', label: 'Screenshot'}, + loopButton: {label: 'Loop'}, + maximizePlayerButton: {label: 'Maximize'}, + featureMenu: {label: 'Feature menu'} + } + }, + options: { + notifications: { + success: {saved: 'Options saved.'}, + error: { + playerQuality: 'You must select a player quality if you want to enable the automatic quality feature.' + }, + info: { + reset: 'All options have been reset to their default values.\nYou can now save the changes by clicking the "Confirm" button or discard them by closing this page or ignore this notification.' + } + } + } + }, + settings: { + sections: { + importExportSettings: { + title: 'Import/Export Settings', + importButton: { + error: { + validation: 'Error importing settings. Please check the file format.\n{{ERROR_MESSAGE}}', + unknown: 'Error importing settings. Please check the file format.\nAn unknown error occurred.' + }, + success: 'Settings imported successfully', + title: 'Import settings from a JSON file', + value: 'Import Settings' + }, + exportButton: { + success: 'Settings successfully exported', + title: 'Export settings to a JSON file', + value: 'Export Settings' + } + }, + miscellaneous: { + title: 'Miscellaneous settings', + features: { + rememberLastVolume: { + title: 'Remembers the volume you were watching at and sets it as the volume when you open a new video', + label: 'Remember last volume' + }, + maximizePlayerButton: { + title: 'Fills the video to the window size', + label: 'Enable maximize player button' + }, + videoHistory: { + title: 'Keeps track of where you left off on videos you were watching and asks if you want to resume when that video loads again', + label: 'Enable video history' + }, + remainingTime: { + title: 'Shows the remaining time of the video you\'re watching', + label: 'Enable remaining time' + }, + loopButton: { + title: 'Adds a button to the player to loop the video you\'re watching', + label: 'Enable loop button' + }, + hideScrollbar: {title: 'Hides the pages scrollbar', label: 'Enable hide scrollbar'} + } + }, + scrollWheelVolumeControl: { + title: 'Scroll wheel volume control settings', + enable: { + title: 'Lets you use the scroll wheel to control the volume of the video you\'re watching', + label: 'Enable scroll wheel volume control' + }, + osdColor: {title: 'The color of the On Screen Display', label: 'OSD color'}, + osdType: {title: 'The type of On Screen Display', label: 'OSD type'}, + osdPosition: {title: 'The position of the On Screen Display', label: 'OSD position'}, + osdOpacity: {title: 'The opacity of the On Screen Display', label: 'OSD opacity'}, + osdVolumeAdjustmentSteps: {title: 'The amount to adjust volume per scroll', label: 'Amount to adjust'}, + osdHide: { + title: 'The amount of milliseconds to wait before hiding the OSD', + label: 'Time to hide' + }, + osdPadding: { + title: 'The amount of padding to add to the OSD (in pixels, only applies to corner OSD)', + label: 'Padding' + }, + onScreenDisplay: { + colors: { + red: 'Red', + green: 'Green', + blue: 'Blue', + yellow: 'Yellow', + orange: 'Orange', + purple: 'Purple', + pink: 'Pink', + white: 'White' + }, + position: { + top_left: 'Top Left', + top_right: 'Top Right', + bottom_left: 'Bottom Left', + bottom_right: 'Bottom Right', + center: 'Center' + }, + type: {no_display: 'No display', text: 'Text', line: 'Line', round: 'Round'} + } + }, + automaticQuality: { + title: 'Automatic quality settings', + enable: { + title: 'Automatically adjusts the video quality to the selected level.', + label: 'Enable automatic quality adjustment' + }, + select: {label: 'Player quality', title: 'The quality to set the video to'} + }, + playbackSpeed: { + title: 'Playback speed settings', + enable: { + title: 'Sets the video speed to what you choose below', + label: 'Enable forced playback speed' + }, + select: {label: 'Player speed', title: 'The speed to set the video to'} + }, + volumeBoost: { + title: 'Volume boost settings', + enable: { + title: 'Boosts the volume of the video you\'re watching', + label: 'Enable volume boost' + }, + number: {label: 'Volume boost amount (dB)', title: 'The amount to boost the volume by'} + }, + screenshotButton: { + title: 'Screenshot settings', + enable: { + title: 'Adds a button to the player to take a screenshot of the video', + label: 'Enable screenshot button' + }, + selectSaveAs: {label: 'Screenshot save type', title: 'The screenshot save type'}, + selectFormat: {label: 'Screenshot format', title: 'The format to save the screenshot in'}, + format: {png: 'PNG', jpeg: 'JPEG', webp: 'WebP'}, + saveAs: {file: 'File', clipboard: 'Clipboard'} + }, + bottomButtons: { + confirm: {title: 'Confirm setting reset', value: 'Confirm'}, + clear: { + title: 'Clears all data this extension has stored on your machine', + value: 'Clear Data' + }, + reset: { + title: 'Resets all settings to their defaults, Click the confirm button to save the changes', + value: 'Reset' + } + }, + language: { + title: 'Language', + select: {label: 'Language', title: 'The language to use for the extension'} + } + }, + clearData: { + confirmAlert: 'This will delete all extension data related to options. Continue?', + allDataDeleted: 'All data has been deleted.' + } + }, + messages: { + settingVolume: 'Setting volume boost to {{VOLUME_BOOST_AMOUNT}}', + resumingVideo: 'Resuming video at {{VIDEO_TIME}}' + } +} + +declare const EnUS: EnUS; + +export = EnUS; \ No newline at end of file diff --git a/public/locales/es-ES.json b/public/locales/es-ES.json new file mode 100644 index 00000000..02a203c8 --- /dev/null +++ b/public/locales/es-ES.json @@ -0,0 +1,238 @@ +{ + "langCode": "es-ES", + "langName": "Español", + "pages": { + "content": { + "features": { + "videoHistory": { + "resumePrompt": { + "close": "Cerca" + }, + "resumeButton": "Reanudar" + }, + "screenshotButton": { + "copiedToClipboard": "Captura de pantalla copiada al portapapeles", + "label": "Captura de pantalla" + }, + "loopButton": { + "label": "Bucle" + }, + "maximizePlayerButton": { + "label": "Maximizar" + }, + "featureMenu": { + "label": "Menú de funciones" + } + } + }, + "options": { + "notifications": { + "success": { + "saved": "Opciones salvadas." + }, + "error": { + "playerQuality": "Debe seleccionar una calidad de reproductor si desea habilitar la función de calidad automática." + }, + "info": { + "reset": "Todas las opciones se han restablecido a sus valores predeterminados.\n" + } + } + } + }, + "settings": { + "sections": { + "importExportSettings": { + "title": "Configuración de importación/exportación", + "importButton": { + "error": { + "validation": "Error al importar la configuración. \n{{ERROR_MESSAGE}}", + "unknown": "Error al importar la configuración. " + }, + "success": "Configuración importada correctamente", + "title": "Importar configuraciones desde un archivo JSON", + "value": "Importar ajustes" + }, + "exportButton": { + "success": "Configuración exportada correctamente", + "title": "Exportar configuración a un archivo JSON", + "value": "Configuración de exportación" + } + }, + "miscellaneous": { + "title": "Otras configuraciones", + "features": { + "rememberLastVolume": { + "title": "Recuerda el volumen que estabas viendo y lo establece como volumen cuando abres un nuevo video.", + "label": "Recuerda el último volumen." + }, + "maximizePlayerButton": { + "title": "Rellena el vídeo al tamaño de la ventana.", + "label": "Habilitar el botón maximizar jugador" + }, + "videoHistory": { + "title": "Realiza un seguimiento de dónde lo dejaste en los videos que estabas viendo y te pregunta si deseas continuar cuando ese video se carga nuevamente.", + "label": "Habilitar historial de video" + }, + "remainingTime": { + "title": "Muestra el tiempo restante del vídeo que estás viendo.", + "label": "Habilitar el tiempo restante" + }, + "loopButton": { + "title": "Agrega un botón al reproductor para reproducir el video que estás viendo", + "label": "Habilitar botón de bucle" + }, + "hideScrollbar": { + "title": "Oculta la barra de desplazamiento de las páginas.", + "label": "Habilitar ocultar barra de desplazamiento" + } + } + }, + "scrollWheelVolumeControl": { + "title": "Configuración de control de volumen de la rueda de desplazamiento", + "enable": { + "title": "Te permite usar la rueda de desplazamiento para controlar el volumen del vídeo que estás viendo", + "label": "Habilitar el control de volumen de la rueda de desplazamiento" + }, + "osdColor": { + "title": "El color de la visualización en pantalla.", + "label": "color en pantalla" + }, + "osdType": { + "title": "El tipo de visualización en pantalla", + "label": "tipo de OSD" + }, + "osdPosition": { + "title": "La posición de la visualización en pantalla", + "label": "Posición OSD" + }, + "osdOpacity": { + "title": "La opacidad de la visualización en pantalla.", + "label": "Opacidad OSD" + }, + "osdVolumeAdjustmentSteps": { + "title": "La cantidad para ajustar el volumen por desplazamiento", + "label": "Cantidad a ajustar" + }, + "osdHide": { + "title": "La cantidad de milisegundos que se deben esperar antes de ocultar el OSD", + "label": "Es hora de esconderse" + }, + "osdPadding": { + "title": "La cantidad de relleno que se agregará al OSD (en píxeles, solo se aplica al OSD de las esquinas)", + "label": "Relleno" + }, + "onScreenDisplay": { + "colors": { + "red": "Rojo", + "green": "Verde", + "blue": "Azul", + "yellow": "Amarillo", + "orange": "Naranja", + "purple": "Púrpura", + "pink": "Rosa", + "white": "Blanco" + }, + "position": { + "top_left": "Arriba a la izquierda", + "top_right": "Parte superior derecha", + "bottom_left": "Abajo a la izquierda", + "bottom_right": "Abajo a la derecha", + "center": "Centro" + }, + "type": { + "no_display": "Sin pantalla", + "text": "Texto", + "line": "Línea", + "round": "Redondo" + } + } + }, + "automaticQuality": { + "title": "Configuración de calidad automática", + "enable": { + "title": "Ajusta automáticamente la calidad del vídeo al nivel seleccionado.", + "label": "Habilitar el ajuste automático de calidad" + }, + "select": { + "label": "Calidad del jugador", + "title": "La calidad para configurar el video." + } + }, + "playbackSpeed": { + "title": "Configuración de velocidad de reproducción", + "enable": { + "title": "Establece la velocidad del vídeo según lo que elijas a continuación", + "label": "Habilitar la velocidad de reproducción forzada" + }, + "select": { + "label": "Velocidad del jugador", + "title": "La velocidad para configurar el vídeo" + } + }, + "volumeBoost": { + "title": "Configuración de aumento de volumen", + "enable": { + "title": "Aumenta el volumen del vídeo que estás viendo.", + "label": "Habilitar aumento de volumen" + }, + "number": { + "label": "Cantidad de aumento de volumen (dB)", + "title": "La cantidad para aumentar el volumen" + } + }, + "screenshotButton": { + "title": "Configuración de captura de pantalla", + "enable": { + "title": "Agrega un botón al reproductor para tomar una captura de pantalla del video.", + "label": "Habilitar botón de captura de pantalla" + }, + "selectSaveAs": { + "label": "Tipo de guardado de captura de pantalla", + "title": "El tipo de guardar captura de pantalla" + }, + "selectFormat": { + "label": "Formato de captura de pantalla", + "title": "El formato para guardar la captura de pantalla" + }, + "format": { + "png": "PNG", + "jpeg": "JPEG", + "webp": "WebP" + }, + "saveAs": { + "file": "Archivo", + "clipboard": "Portapapeles" + } + }, + "bottomButtons": { + "confirm": { + "title": "Confirmar reinicio de configuración", + "value": "Confirmar" + }, + "clear": { + "title": "Borra todos los datos que esta extensión ha almacenado en su máquina", + "value": "Borrar datos" + }, + "reset": { + "title": "Restablece todas las configuraciones a sus valores predeterminados. Haga clic en el botón confirmar para guardar los cambios.", + "value": "Reiniciar" + } + }, + "language": { + "title": "Idioma", + "select": { + "label": "Idioma", + "title": "El idioma a utilizar para la extensión." + } + } + }, + "clearData": { + "confirmAlert": "Esto eliminará todos los datos de extensión relacionados con las opciones. ", + "allDataDeleted": "Todos los datos han sido eliminados." + } + }, + "messages": { + "settingVolume": "Configurar el aumento de volumen en {{VOLUME_BOOST_AMOUNT}}", + "resumingVideo": "Reanudando vídeo en {{VIDEO_TIME}}" + } +} diff --git a/src/components/Settings/Settings.tsx b/src/components/Settings/Settings.tsx index 8738e0cd..ad55d9af 100644 --- a/src/components/Settings/Settings.tsx +++ b/src/components/Settings/Settings.tsx @@ -2,17 +2,66 @@ import "@/assets/styles/tailwind.css"; import "@/components/Settings/Settings.css"; import { useNotifications } from "@/hooks"; -import type { configuration, configurationKeys } from "@/src/types"; -import { youtubePlayerSpeedRate } from "@/src/types"; -import { useAutoAnimate } from "@formkit/auto-animate/react"; -import React, { useEffect, useState } from "react"; +import type { configuration, configurationKeys } from "@/src/@types"; +import { youtubePlayerSpeedRate } from "@/src/@types"; +import React, { useEffect, useState, Suspense } from "react"; import type { ChangeEvent, Dispatch, SetStateAction } from "react"; -import { Checkbox, NumberInput, Select, type SelectOption } from "../Inputs"; import { cn, settingsAreDefault } from "@/src/utils/utilities"; import { configurationImportSchema } from "@/src/utils/constants"; import { generateErrorMessage } from "zod-error"; import { formatDateForFileName } from "../../utils/utilities"; - +import SettingsNotifications from "./components/SettingNotifications"; +import SettingSection from "./components/SettingSection"; +import SettingTitle from "./components/SettingTitle"; +import Setting from "./components/Setting"; +import type { SelectOption } from "../Inputs"; +import { availableLocales, type i18nInstanceType } from "@/src/i18n"; +import type EnUS from "public/locales/en-US.json"; +import Loader from "../Loader"; +async function getLanguageOptions() { + const languageOptions: SelectOption[] = []; + for (const locale of availableLocales) { + const response = await fetch(`${chrome.runtime.getURL("")}locales/${locale}.json`).catch((err) => console.error(err)); + const localeData = (await response?.json()) as EnUS; + languageOptions.push({ + value: locale, + label: (localeData as typeof EnUS).langName + }); + } + return languageOptions; +} +function LanguageOptions({ + setValueOption, + t, + selectedLanguage, + setSelectedLanguage +}: { + t: i18nInstanceType["t"]; + setValueOption: (key: configurationKeys) => ({ currentTarget }: ChangeEvent) => void; + selectedLanguage: string | undefined; + setSelectedLanguage: Dispatch>; +}) { + const [languageOptions, setLanguageOptions] = useState([]); + useEffect(() => { + getLanguageOptions().then(setLanguageOptions); + }, []); + return ( + + + + + ); +} export default function Settings({ settings, setSettings, @@ -30,7 +79,10 @@ export default function Settings({ setSelectedPlayerQuality, selectedPlayerSpeed, setSelectedPlayerSpeed, - defaultSettings + selectedLanguage, + setSelectedLanguage, + defaultSettings, + i18nInstance }: { settings: configuration | undefined; setSettings: Dispatch>; @@ -48,11 +100,14 @@ export default function Settings({ setSelectedScreenshotSaveAs: Dispatch>; selectedScreenshotFormat: string | undefined; setSelectedScreenshotFormat: Dispatch>; + selectedLanguage: string | undefined; + setSelectedLanguage: Dispatch>; defaultSettings: configuration; + i18nInstance: i18nInstanceType; }) { const [firstLoad, setFirstLoad] = useState(true); - const [parentRef] = useAutoAnimate({ duration: 300 }); const { notifications, addNotification, removeNotification } = useNotifications(); + const { t } = i18nInstance; const setCheckboxOption = (key: configurationKeys) => ({ currentTarget: { checked } }: ChangeEvent) => { @@ -75,112 +130,123 @@ export default function Settings({ function saveOptions() { if (settings) { if (settings.enable_automatically_set_quality && !settings.player_quality) { - addNotification("error", "You must select a player quality if you want to enable the automatic quality feature."); + addNotification("error", t("pages.options.notifications.error.playerQuality")); return; } Object.assign(localStorage, settings); chrome.storage.local.set(settings); - addNotification("success", "Options saved"); + addNotification("success", t("pages.options.notifications.success.saved")); } } function resetOptions() { - addNotification( - "info", - 'All options have been reset to their default values.\nYou can now save the changes by clicking the "Confirm" button or discard them by closing this page or ignore this notification.', - "reset_settings" - ); + addNotification("info", t("pages.options.notifications.info.reset"), "reset_settings"); } function clearData() { - const userHasConfirmed = window.confirm("This will delete all extension data related to options. Continue?"); + const userHasConfirmed = window.confirm(t("settings.clearData.confirmAlert")); if (userHasConfirmed) { Object.assign(localStorage, defaultSettings); chrome.storage.local.set(defaultSettings); - addNotification("success", "All data has been deleted"); + addNotification("success", t("settings.clearData.allDataDeleted")); } } + const { + colors: { red, green, blue, orange, pink, white, purple, yellow }, + position: { bottom_left, bottom_right, top_left, top_right, center }, + type: { line, no_display, round, text } + } = t("settings.sections.scrollWheelVolumeControl.onScreenDisplay", { + returnObjects: true, + defaultValue: {} + }); + const { + format: { jpeg, png, webp }, + saveAs: { clipboard, file } + } = t("settings.sections.screenshotButton", { + returnObjects: true, + defaultValue: {} + }); const colorOptions: SelectOption[] = [ { value: "red", - label: "Red", + label: red, element:
}, { value: "green", - label: "Green", + label: green, element:
}, { value: "blue", - label: "Blue", + label: blue, element:
}, { value: "yellow", - label: "Yellow", + label: yellow, element:
}, { value: "orange", - label: "Orange", + label: orange, element:
}, { value: "purple", - label: "Purple", + label: purple, element:
}, { value: "pink", - label: "Pink", + label: pink, element:
}, { value: "white", - label: "White", + label: white, element:
} ]; const OSD_DisplayTypeOptions: SelectOption[] = [ { value: "no_display", - label: "No display" + label: no_display }, { value: "text", - label: "Text" + label: text }, { value: "line", - label: "Line" + label: line }, { value: "round", - label: "Round" + label: round } ]; const OSD_PositionOptions: SelectOption[] = [ { value: "top_left", - label: "Top left" + label: top_left }, { value: "top_right", - label: "Top right" + label: top_right }, { value: "bottom_left", - label: "Bottom left" + label: bottom_left }, { value: "bottom_right", - label: "Bottom right" + label: bottom_right }, { value: "center", - label: "Center" + label: center } ]; const YouTubePlayerQualityOptions: SelectOption[] = [ @@ -198,14 +264,15 @@ export default function Settings({ ].reverse(); const YouTubePlayerSpeedOptions: SelectOption[] = youtubePlayerSpeedRate.map((rate) => ({ label: rate.toString(), value: rate.toString() })); const ScreenshotFormatOptions: SelectOption[] = [ - { label: "PNG", value: "png" }, - { label: "JPEG", value: "jpeg" }, - { label: "WEBP", value: "webp" } + { label: png, value: "png" }, + { label: jpeg, value: "jpeg" }, + { label: webp, value: "webp" } ]; const ScreenshotSaveAsOptions: SelectOption[] = [ - { label: "File", value: "file" }, - { label: "Clipboard", value: "clipboard" } + { label: file, value: "file" }, + { label: clipboard, value: "clipboard" } ]; + // Import settings from a JSON file. const importSettings = () => { const input = document.createElement("input"); @@ -226,7 +293,11 @@ export default function Settings({ if (!result.success) { const { error } = result; const errorMessage = generateErrorMessage(error.errors); - window.alert(`Error importing settings. Please check the file format.\n${errorMessage}`); + window.alert( + t("settings.sections.importExportSettings.importButton.error.validation", { + ERROR_MESSAGE: errorMessage + }) + ); } else { const castSettings = importedSettings as configuration; // Set the imported settings in your state. @@ -234,11 +305,11 @@ export default function Settings({ Object.assign(localStorage, castSettings); chrome.storage.local.set(castSettings); // Show a success notification. - addNotification("success", "Settings imported successfully"); + addNotification("success", t("settings.sections.importExportSettings.importButton.success")); } } catch (error) { // Handle any import errors. - window.alert("Error importing settings. Please check the file format."); + window.alert(t("settings.sections.importExportSettings.importButton.error.unknown")); } } }); @@ -263,7 +334,7 @@ export default function Settings({ a.click(); // Show a success notification. - addNotification("success", "Settings exported successfully"); + addNotification("success", t("settings.sections.importExportSettings.exportButton.success")); } }; // TODO: add "default player mode" setting (theater, fullscreen, etc.) feature @@ -275,275 +346,278 @@ export default function Settings({ YouTube Enhancer v{chrome.runtime.getManifest().version} -
- Import/Export Settings + }> + + + +
-
-
- Miscellaneous settings -
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
- Scroll wheel volume control settings - + + + + + + + + + + + + -
- -
-
- -
-
-
- Playback speed settings - + + + + -
- -
-
- {notifications.filter((n) => n.action === "reset_settings").length > 0 ? ( @@ -552,8 +626,8 @@ export default function Settings({ id="confirm_button" className="p-2 danger dark:hover:bg-[rgba(24,26,27,0.5)] text-sm sm:text-base md:text-lg" style={{ marginLeft: "auto" }} - value="Confirm" - title="Confirm setting reset" + title={t("settings.sections.bottomButtons.confirm.title")} + value={t("settings.sections.bottomButtons.confirm.value")} onClick={() => { const notificationToRemove = notifications.find((n) => n.action === "reset_settings"); if (notificationToRemove) { @@ -562,7 +636,7 @@ export default function Settings({ Object.assign(localStorage, Object.assign(defaultSettings, { remembered_volumes: settings.remembered_volumes })); chrome.storage.local.set(Object.assign(defaultSettings, { remembered_volumes: settings.remembered_volumes })); - addNotification("success", "Options saved"); + addNotification("success", t("pages.options.notifications.success.saved")); }} /> ) : ( @@ -571,51 +645,13 @@ export default function Settings({ id="reset_button" className="p-2 warning dark:hover:bg-[rgba(24,26,27,0.5)] text-sm sm:text-base md:text-lg" style={{ marginLeft: "auto" }} - value="Reset" - title="Resets all settings to their defaults, Click the confirm button to save the changes" + title={t("settings.sections.bottomButtons.reset.title")} + value={t("settings.sections.bottomButtons.reset.value")} onClick={resetOptions} /> )}
-
- {notifications.map((notification, index) => ( -
- {notification.action ? ( - notification.action === "reset_settings" ? ( - <> - {notification.message.split("\n").map((line, index) => ( -

{line}

- ))} - - - ) : null - ) : ( - <> - {notification.message} - - - )} -
-
- ))} -
+ ) ); diff --git a/src/components/SettingsWrapper/index.tsx b/src/components/SettingsWrapper/index.tsx index ffdb4a59..0277b7e3 100644 --- a/src/components/SettingsWrapper/index.tsx +++ b/src/components/SettingsWrapper/index.tsx @@ -1,9 +1,11 @@ import Loader from "@/src/components/Loader"; import Settings from "@/src/components/Settings/Settings"; -import type { configuration } from "@/src/types"; +import { NotificationsProvider } from "@/src/hooks/useNotifications/provider"; +import type { configuration } from "@/src/@types"; import { defaultConfiguration } from "@/src/utils/constants"; import { parseStoredValue } from "@/src/utils/utilities"; import React, { useEffect, useState } from "react"; +import { i18nService, type i18nInstanceType, type AvailableLocales } from "@/src/i18n"; export default function SettingsWrapper(): JSX.Element { const [settings, setSettings] = useState(undefined); @@ -14,6 +16,8 @@ export default function SettingsWrapper(): JSX.Element { const [selectedPlayerSpeed, setSelectedPlayerSpeed] = useState(); const [selectedScreenshotSaveAs, setSelectedScreenshotSaveAs] = useState(); const [selectedScreenshotFormat, setSelectedScreenshotFormat] = useState(); + const [selectedLanguage, setSelectedLanguage] = useState(); + const [i18nInstance, setI18nInstance] = useState(null); useEffect(() => { const fetchSettings = () => { chrome.storage.local.get((settings) => { @@ -28,6 +32,7 @@ export default function SettingsWrapper(): JSX.Element { setSelectedPlayerSpeed(settings.player_speed); setSelectedScreenshotSaveAs(settings.screenshot_save_as); setSelectedScreenshotFormat(settings.screenshot_format); + setSelectedLanguage(settings.language); }); }; @@ -70,6 +75,9 @@ export default function SettingsWrapper(): JSX.Element { case "screenshot_format": setSelectedScreenshotFormat(newValue); break; + case "language": + setSelectedLanguage(newValue); + break; } setSettings((prevSettings) => { if (prevSettings) { @@ -88,30 +96,40 @@ export default function SettingsWrapper(): JSX.Element { chrome.storage.onChanged.removeListener(handleStorageChange); }; }, []); - + useEffect(() => { + (async () => { + const instance = await i18nService((selectedLanguage as AvailableLocales) ?? "en-US"); + setI18nInstance(instance); + })(); + }, [selectedLanguage]); const defaultOptions = defaultConfiguration; - if (!settings) { + if (!settings || !i18nInstance || (i18nInstance && i18nInstance.isInitialized === false)) { return ; } return ( - + + + ); } diff --git a/src/features/featureMenu/index.ts b/src/features/featureMenu/index.ts index f6a73f47..4ff62e51 100644 --- a/src/features/featureMenu/index.ts +++ b/src/features/featureMenu/index.ts @@ -1,4 +1,4 @@ -import type { YouTubePlayerDiv } from "@/src/types"; +import type { YouTubePlayerDiv } from "@/src/@types"; import eventManager from "@/src/utils/EventManager"; import { isWatchPage, isShortsPage, createTooltip } from "@/src/utils/utilities"; @@ -36,7 +36,7 @@ function createFeatureMenuButton() { const featureMenuButton = document.createElement("button"); featureMenuButton.classList.add("ytp-button"); featureMenuButton.id = "yte-feature-menu-button"; - featureMenuButton.dataset.title = "Feature menu"; + featureMenuButton.dataset.title = window.i18nextInstance.t("pages.content.features.featureMenu.label"); featureMenuButton.style.display = "none"; // Create the SVG icon for the button const featureButtonSVG = document.createElementNS("http://www.w3.org/2000/svg", "svg"); diff --git a/src/features/featureMenu/utils.ts b/src/features/featureMenu/utils.ts index 0b829a50..e1cfdfed 100644 --- a/src/features/featureMenu/utils.ts +++ b/src/features/featureMenu/utils.ts @@ -1,4 +1,4 @@ -import type { FeatureMenuItemIconId, FeatureMenuItemId, FeatureMenuItemLabelId, WithId } from "@/src/types"; +import type { FeatureMenuItemIconId, FeatureMenuItemId, FeatureMenuItemLabelId, WithId } from "@/src/@types"; import eventManager, { type FeatureName } from "@/src/utils/EventManager"; import { waitForAllElements } from "@/src/utils/utilities"; /** @@ -146,7 +146,32 @@ export async function removeFeatureItemFromMenu(featureName: FeatureName) { // Adjust the height and width of the feature menu panel featureMenu.style.height = `${40 * featureMenuPanel.childElementCount + 16}px`; } - +/** + * Updates the label for a feature item. + * @param featureName the name of the feature + * @param label the label to set + * @returns + */ +export async function updateFeatureMenuItemLabel(featureName: FeatureName, label: string) { + const featureMenuItemLabel = getFeatureMenuItemLabel(featureName); + if (!featureMenuItemLabel) return; + featureMenuItemLabel.textContent = label; +} +/** + * Updates the title for the feature menu button. + * @param title the title to set + * @returns + */ +export async function updateFeatureMenuTitle(title: string) { + const featureMenuButton = document.querySelector("#yte-feature-menu-button") as HTMLButtonElement | null; + if (!featureMenuButton) return; + featureMenuButton.dataset.title = title; +} +/** + * Gets the IDs for a feature item. + * @param featureName the name of the feature + * @returns { featureMenuItemIconId, featureMenuItemId, featureMenuItemLabelId} + */ export function getFeatureIds(featureName: FeatureName): { featureMenuItemIconId: FeatureMenuItemIconId; featureMenuItemId: FeatureMenuItemId; diff --git a/src/features/maximizePlayerButton/index.ts b/src/features/maximizePlayerButton/index.ts index efabcada..7c7f9e70 100644 --- a/src/features/maximizePlayerButton/index.ts +++ b/src/features/maximizePlayerButton/index.ts @@ -1,4 +1,4 @@ -import type { YouTubePlayerDiv } from "@/src/types"; +import type { YouTubePlayerDiv } from "@/src/@types"; import eventManager from "@/src/utils/EventManager"; import { waitForSpecificMessage } from "@/src/utils/utilities"; import { makeMaximizeSVG, updateProgressBarPositions, setupVideoPlayerTimeUpdate, maximizePlayer } from "./utils"; diff --git a/src/features/maximizePlayerButton/utils.ts b/src/features/maximizePlayerButton/utils.ts index 3c9b8af9..55a9c914 100644 --- a/src/features/maximizePlayerButton/utils.ts +++ b/src/features/maximizePlayerButton/utils.ts @@ -1,4 +1,4 @@ -import type { YouTubePlayerDiv } from "@/src/types"; +import type { YouTubePlayerDiv } from "@/src/@types"; import eventManager from "@/src/utils/EventManager"; let wasInTheatreMode = false; let setToTheatreMode = false; diff --git a/src/features/playerQuality/index.ts b/src/features/playerQuality/index.ts index 9cde4620..a1ba6632 100644 --- a/src/features/playerQuality/index.ts +++ b/src/features/playerQuality/index.ts @@ -1,5 +1,5 @@ -import { youtubePlayerQualityLevel, youtubePlayerQualityLabel } from "@/src/types"; -import type { YoutubePlayerQualityLabel, YoutubePlayerQualityLevel, YouTubePlayerDiv } from "@/src/types"; +import { youtubePlayerQualityLevel, youtubePlayerQualityLabel } from "@/src/@types"; +import type { YoutubePlayerQualityLabel, YoutubePlayerQualityLevel, YouTubePlayerDiv } from "@/src/@types"; import { waitForSpecificMessage, isWatchPage, isShortsPage, chooseClosetQuality, browserColorLog } from "@/src/utils/utilities"; /** diff --git a/src/features/playerSpeed/index.ts b/src/features/playerSpeed/index.ts index 6a7effa8..d78b48ee 100644 --- a/src/features/playerSpeed/index.ts +++ b/src/features/playerSpeed/index.ts @@ -1,4 +1,4 @@ -import type { YouTubePlayerDiv } from "@/src/types"; +import type { YouTubePlayerDiv } from "@/src/@types"; import eventManager from "@/src/utils/EventManager"; import { isWatchPage, isShortsPage, browserColorLog, waitForSpecificMessage } from "@/src/utils/utilities"; @@ -102,6 +102,7 @@ export function setupPlaybackSpeedChangeListener() { for (const mutation of mutationsList) { if (mutation.type === "childList") { const titleElement: HTMLSpanElement | null = document.querySelector("div.ytp-panel > div.ytp-panel-header > span.ytp-panel-title"); + // TODO: fix this it relies on the language being English if (titleElement && titleElement.textContent && titleElement.textContent.includes("Playback speed")) { const menuItems: NodeListOf = document.querySelectorAll("div.ytp-panel-menu div.ytp-menuitem"); menuItems.forEach((node: HTMLDivElement) => { diff --git a/src/features/remainingTime/index.ts b/src/features/remainingTime/index.ts index 7e8b7de8..14fecc22 100644 --- a/src/features/remainingTime/index.ts +++ b/src/features/remainingTime/index.ts @@ -1,5 +1,5 @@ import { isShortsPage, isWatchPage, waitForSpecificMessage } from "@/src/utils/utilities"; -import type { YouTubePlayerDiv } from "@/src/types"; +import type { YouTubePlayerDiv } from "@/src/@types"; import { calculateRemainingTime } from "./utils"; import eventManager from "@/src/utils/EventManager"; async function playerTimeUpdateListener() { @@ -49,6 +49,9 @@ export async function setupRemainingTime() { const videoElement = playerContainer.querySelector("video") as HTMLVideoElement | null; // If video element is not available, return if (!videoElement) return; + const playerVideoData = await playerContainer.getVideoData(); + // If the video is live return + if (playerVideoData.isLive) return; const remainingTime = await calculateRemainingTime({ videoElement, playerContainer }); const remainingTimeElementExists = document.querySelector("span#ytp-time-remaining") !== null; const remainingTimeElement = document.querySelector("span#ytp-time-remaining") ?? document.createElement("span"); diff --git a/src/features/remainingTime/utils.ts b/src/features/remainingTime/utils.ts index f7053b78..607db7c5 100644 --- a/src/features/remainingTime/utils.ts +++ b/src/features/remainingTime/utils.ts @@ -1,5 +1,5 @@ -import type { YouTubePlayerDiv } from "@/src/types"; -function formatTime(timeInSeconds: number) { +import type { YouTubePlayerDiv } from "@/src/@types"; +export function formatTime(timeInSeconds: number) { timeInSeconds = Math.round(timeInSeconds); const units: number[] = [ Math.floor(timeInSeconds / (3600 * 24)), @@ -19,7 +19,7 @@ function formatTime(timeInSeconds: number) { return acc; }, []); - return ` (-${formattedUnits.length > 0 ? formattedUnits.join(":") : "0"})`; + return `${formattedUnits.length > 0 ? formattedUnits.join(":") : "0"}`; } export async function calculateRemainingTime({ videoElement, @@ -39,5 +39,5 @@ export async function calculateRemainingTime({ const remainingTimeInSeconds = (duration - currentTime) / playbackRate; // Format the remaining time - return formatTime(remainingTimeInSeconds); + return ` (-${formatTime(remainingTimeInSeconds)})`; } diff --git a/src/features/rememberVolume/index.ts b/src/features/rememberVolume/index.ts index 30b21222..2799d573 100644 --- a/src/features/rememberVolume/index.ts +++ b/src/features/rememberVolume/index.ts @@ -2,7 +2,7 @@ import { isShortsPage, isWatchPage, waitForSpecificMessage } from "@/src/utils/u import { setRememberedVolume, setupVolumeChangeListener } from "./utils"; -import type { YouTubePlayerDiv } from "@/src/types"; +import type { YouTubePlayerDiv } from "@/src/@types"; /** * Sets the remembered volume based on the options received from a specific message. * It restores the last volume if the option is enabled. diff --git a/src/features/rememberVolume/utils.ts b/src/features/rememberVolume/utils.ts index dc63b4a5..27f1b91d 100644 --- a/src/features/rememberVolume/utils.ts +++ b/src/features/rememberVolume/utils.ts @@ -1,7 +1,7 @@ import eventManager from "@/src/utils/EventManager"; import { browserColorLog, isShortsPage, isWatchPage, sendContentOnlyMessage, waitForSpecificMessage } from "@/src/utils/utilities"; -import type { YouTubePlayerDiv, configuration } from "@/src/types"; +import type { YouTubePlayerDiv, configuration } from "@/src/@types"; export async function setupVolumeChangeListener() { // Wait for the "options" message from the content script const optionsData = await waitForSpecificMessage("options", "request_data", "content"); diff --git a/src/features/screenshotButton/index.ts b/src/features/screenshotButton/index.ts index c4219c34..b61566a4 100644 --- a/src/features/screenshotButton/index.ts +++ b/src/features/screenshotButton/index.ts @@ -39,7 +39,7 @@ async function takeScreenshot(videoElement: HTMLVideoElement) { const clipboardImage = new ClipboardItem({ "image/png": blob }); navigator.clipboard.write([clipboardImage]); navigator.clipboard.writeText(dataUrl); - screenshotTooltip.textContent = "Screenshot copied to clipboard"; + screenshotTooltip.textContent = window.i18nextInstance.t("pages.content.features.screenshotButton.copiedToClipboard"); } break; } @@ -66,7 +66,6 @@ export async function addScreenshotButton(): Promise { const { enable_screenshot_button: enableScreenshotButton } = options; // If the screenshot button option is disabled, return if (!enableScreenshotButton) return; - // Add a click event listener to the screenshot button async function screenshotButtonClickListener() { // Get the video element @@ -83,7 +82,7 @@ export async function addScreenshotButton(): Promise { addFeatureItemToMenu({ featureName: "screenshotButton", icon: makeScreenshotIcon(), - label: "Screenshot", + label: window.i18nextInstance.t("pages.content.features.screenshotButton.label"), listener: screenshotButtonClickListener }); } diff --git a/src/features/scrollWheelVolumeControl/index.ts b/src/features/scrollWheelVolumeControl/index.ts index 0621f78e..e6c60b59 100644 --- a/src/features/scrollWheelVolumeControl/index.ts +++ b/src/features/scrollWheelVolumeControl/index.ts @@ -1,4 +1,4 @@ -import type { YouTubePlayerDiv } from "@/src/types"; +import type { YouTubePlayerDiv } from "@/src/@types"; import { waitForAllElements, isWatchPage, isShortsPage, waitForSpecificMessage } from "@/src/utils/utilities"; import { adjustVolume, getScrollDirection, setupScrollListeners, drawVolumeDisplay } from "./utils"; diff --git a/src/features/scrollWheelVolumeControl/utils.ts b/src/features/scrollWheelVolumeControl/utils.ts index dabf8da6..1738e9a4 100644 --- a/src/features/scrollWheelVolumeControl/utils.ts +++ b/src/features/scrollWheelVolumeControl/utils.ts @@ -1,4 +1,4 @@ -import type { OnScreenDisplayColor, OnScreenDisplayPosition, OnScreenDisplayType, Selector, YouTubePlayerDiv } from "@/src/types"; +import type { OnScreenDisplayColor, OnScreenDisplayPosition, OnScreenDisplayType, Selector, YouTubePlayerDiv } from "@/src/@types"; import eventManager from "@/src/utils/EventManager"; import { isWatchPage, isShortsPage, clamp, toDivisible, browserColorLog, round } from "@/src/utils/utilities"; diff --git a/src/features/videoHistory/index.ts b/src/features/videoHistory/index.ts index a32e1427..da75c7d7 100644 --- a/src/features/videoHistory/index.ts +++ b/src/features/videoHistory/index.ts @@ -1,6 +1,7 @@ -import type { YouTubePlayerDiv } from "@/src/types"; +import type { YouTubePlayerDiv } from "@/src/@types"; import eventManager from "@/utils/EventManager"; import { browserColorLog, createTooltip, isShortsPage, isWatchPage, sendContentMessage, waitForSpecificMessage } from "@/utils/utilities"; +import { formatTime } from "../remainingTime/utils"; export async function setupVideoHistory() { // Wait for the "options" message from the content script @@ -34,6 +35,15 @@ export async function setupVideoHistory() { eventManager.addEventListener(videoElement, "timeupdate", videoPlayerTimeUpdateListener, "videoHistory"); } export async function promptUserToResumeVideo() { + // Wait for the "options" message from the content script + const optionsData = await waitForSpecificMessage("options", "request_data", "content"); + if (!optionsData) return; + const { + data: { options } + } = optionsData; + const { enable_video_history: enableVideoHistory } = options; + if (!enableVideoHistory) return; + // Get the player container element const playerContainer = isWatchPage() ? (document.querySelector("div#movie_player") as YouTubePlayerDiv | null) : isShortsPage() ? null : null; @@ -85,7 +95,7 @@ export async function promptUserToResumeVideo() { clearInterval(countdownInterval); prompt.style.display = "none"; overlay.style.display = "none"; - browserColorLog(`Resuming video`, "FgGreen"); + browserColorLog(window.i18nextInstance.t("messages.resumingVideo", { VIDEO_TIME: formatTime(video_history_entry.timestamp) }), "FgGreen"); playerContainer.seekTo(video_history_entry.timestamp, true); }; const overlay = document.getElementById("resume-prompt-overlay") ?? document.createElement("div"); @@ -118,7 +128,7 @@ export async function promptUserToResumeVideo() { closeButton.style.padding = "5px"; closeButton.style.cursor = "pointer"; closeButton.style.lineHeight = "1px"; - closeButton.dataset.title = "Close"; + closeButton.dataset.title = window.i18nextInstance.t("pages.content.features.videoHistory.resumePrompt.close"); const { listener: resumePromptCloseButtonMouseOverListener } = createTooltip({ element: closeButton, id: "yte-resume-prompt-close-button-tooltip", @@ -141,7 +151,7 @@ export async function promptUserToResumeVideo() { prompt.style.boxShadow = "0px 0px 10px rgba(0, 0, 0, 0.2)"; prompt.style.zIndex = "25000"; resumeButton.id = "resume-prompt-button"; - resumeButton.textContent = "Resume"; + resumeButton.textContent = window.i18nextInstance.t("pages.content.features.videoHistory.resumeButton"); resumeButton.style.backgroundColor = "hsl(213, 80%, 50%)"; resumeButton.style.border = "transparent"; resumeButton.style.color = "white"; diff --git a/src/features/videoHistory/utils.ts b/src/features/videoHistory/utils.ts index 8a1844a8..50a113ce 100644 --- a/src/features/videoHistory/utils.ts +++ b/src/features/videoHistory/utils.ts @@ -1,4 +1,4 @@ -import type { VideoHistoryStatus, VideoHistoryStorage } from "@/src/types"; +import type { VideoHistoryStatus, VideoHistoryStorage } from "@/src/@types"; export function getVideoHistory() { return JSON.parse(window.localStorage.getItem("videoHistory") ?? "{}") as VideoHistoryStorage; } diff --git a/src/features/volumeBoost/index.ts b/src/features/volumeBoost/index.ts index c11b8d70..861237df 100644 --- a/src/features/volumeBoost/index.ts +++ b/src/features/volumeBoost/index.ts @@ -1,4 +1,4 @@ -import type { YouTubePlayerDiv } from "@/src/types"; +import type { YouTubePlayerDiv } from "@/src/@types"; import { waitForSpecificMessage, browserColorLog, formatError } from "@/src/utils/utilities"; export default async function volumeBoost() { diff --git a/src/global.d.ts b/src/global.d.ts index 81398779..daf04128 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -1,3 +1,5 @@ +import type { i18nInstanceType } from "./i18n"; + declare module "*.svg" { import React = require("react"); export const ReactComponent: React.SFC>; @@ -50,6 +52,7 @@ declare global { audioCtx: AudioContext; webkitAudioContext: AudioContext; gainNode: GainNode; + i18nextInstance: i18nInstanceType; } } export {}; diff --git a/src/i18n/i18n.d.ts b/src/i18n/i18n.d.ts new file mode 100644 index 00000000..3e5bb1d8 --- /dev/null +++ b/src/i18n/i18n.d.ts @@ -0,0 +1,9 @@ +import "i18next"; +declare module "i18next" { + interface CustomTypeOptions { + defaultNS: "en-US"; + resources: { + "en-US": typeof import("../../public/locales/en-US.json"); + }; + } +} diff --git a/src/i18n/index.ts b/src/i18n/index.ts new file mode 100644 index 00000000..f58902eb --- /dev/null +++ b/src/i18n/index.ts @@ -0,0 +1,49 @@ +import i18next, { createInstance, type Resource } from "i18next"; +import { waitForSpecificMessage } from "../utils/utilities"; +export const availableLocales = ["en-US", "es-ES", "de-DE"] as const; +export type AvailableLocales = (typeof availableLocales)[number]; +export type i18nInstanceType = ReturnType; + +export async function i18nService(locale: AvailableLocales) { + let extensionURL; + const isYouTube = window.location.hostname === "www.youtube.com"; + if (isYouTube) { + const extensionURLResponse = await waitForSpecificMessage("extensionURL", "request_data", "content"); + if (!extensionURLResponse) throw new Error("Failed to get extension URL"); + ({ + data: { extensionURL } + } = extensionURLResponse); + } else { + extensionURL = chrome.runtime.getURL(""); + } + if (!availableLocales.includes(locale)) throw new Error(`The locale '${locale}' is not available`); + const response = await fetch(`${extensionURL}locales/${locale}.json`).catch((err) => console.error(err)); + const translations = (await response?.json()) as typeof import("../../public/locales/en-US.json"); + const i18nextInstance = await new Promise((resolve, reject) => { + const resources: { + [k in AvailableLocales]?: { + translation: typeof import("../../public/locales/en-US.json"); + }; + } = { + [locale]: { translation: translations } + }; + const instance = i18next.createInstance(); + instance.init( + { + fallbackLng: "en-US", + interpolation: { + escapeValue: false + }, + returnObjects: true, + lng: locale, + debug: true, + resources: resources as unknown as { [key: string]: Resource } + }, + (err) => { + if (err) reject(err); + else resolve(instance); + } + ); + }); + return i18nextInstance; +} diff --git a/src/manifest.ts b/src/manifest.ts index c8260292..e24d5acd 100755 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -1,5 +1,6 @@ import type { Manifest } from "webextension-polyfill"; import pkg from "../package.json"; +import { availableLocales } from "./i18n"; const manifestV3: Manifest.WebExtensionManifest = { manifest_version: 3, @@ -43,7 +44,8 @@ const manifestV3: Manifest.WebExtensionManifest = { "/icons/icon_48.png", "/icons/icon_16.png", "src/pages/content/index.js", - "src/pages/inject/index.js" + "src/pages/inject/index.js", + ...availableLocales.map((locale) => `/locales/${locale}.json`) ], matches: ["https://www.youtube.com/*"] } @@ -87,7 +89,8 @@ const manifestV2: Manifest.WebExtensionManifest = { "/icons/icon_48.png", "/icons/icon_16.png", "src/pages/content/index.js", - "src/pages/inject/index.js" + "src/pages/inject/index.js", + ...availableLocales.map((locale) => `/locales/${locale}.json`) ] }; diff --git a/src/pages/content/index.tsx b/src/pages/content/index.tsx index b6683d93..da643e2b 100644 --- a/src/pages/content/index.tsx +++ b/src/pages/content/index.tsx @@ -11,11 +11,13 @@ import adjustVolumeOnScrollWheel from "@/src/features/scrollWheelVolumeControl"; import { promptUserToResumeVideo, setupVideoHistory } from "@/src/features/videoHistory"; import volumeBoost from "@/src/features/volumeBoost"; import eventManager from "@/utils/EventManager"; -import { browserColorLog, formatError } from "@/utils/utilities"; +import { browserColorLog, formatError, waitForSpecificMessage } from "@/utils/utilities"; -import type { ExtensionSendOnlyMessageMappings, Messages, YouTubePlayerDiv } from "@/src/types"; +import type { ExtensionSendOnlyMessageMappings, Messages, YouTubePlayerDiv } from "@/src/@types"; import { enableHideScrollBar } from "@/src/features/hideScrollBar"; import { hideScrollBar, showScrollBar } from "@/src/features/hideScrollBar/utils"; +import { i18nService } from "@/src/i18n"; +import { updateFeatureMenuItemLabel, updateFeatureMenuTitle } from "@/src/features/featureMenu/utils"; // TODO: Add always show progressbar feature // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -66,9 +68,10 @@ element.style.display = "none"; element.id = "yte-message-from-youtube"; document.documentElement.appendChild(element); -window.onload = function () { +window.onload = async function () { enableRememberVolume(); enableHideScrollBar(); + const enableFeatures = () => { eventManager.removeAllEventListeners(["featureMenu"]); enableFeatureMenu(); @@ -85,6 +88,13 @@ window.onload = function () { promptUserToResumeVideo(); setupRemainingTime(); }; + const response = await waitForSpecificMessage("language", "request_data", "content"); + if (!response) return; + const { + data: { language } + } = response; + const i18nextInstance = await i18nService(language); + window.i18nextInstance = i18nextInstance; document.addEventListener("yt-player-updated", enableFeatures); /** * Listens for the "yte-message-from-youtube" event and handles incoming messages from the YouTube page. @@ -111,14 +121,24 @@ window.onload = function () { } = message; if (volumeBoostEnabled) { if (window.audioCtx && window.gainNode) { - browserColorLog(`Setting volume boost to ${Math.pow(10, Number(volumeBoostAmount) / 20)}`, "FgMagenta"); + browserColorLog( + i18nextInstance.t("messages.settingVolume", { + VOLUME_BOOST_AMOUNT: Math.pow(10, Number(volumeBoostAmount) / 20) + }), + "FgMagenta" + ); window.gainNode.gain.value = Math.pow(10, Number(volumeBoostAmount) / 20); } else { volumeBoost(); } } else { if (window.audioCtx && window.gainNode) { - browserColorLog(`Setting volume boost to 1x`, "FgMagenta"); + browserColorLog( + i18nextInstance.t("messages.settingVolume", { + VOLUME_BOOST_AMOUNT: "1x" + }), + "FgMagenta" + ); window.gainNode.gain.value = 1; } } @@ -239,6 +259,17 @@ window.onload = function () { } break; } + case "languageChange": { + const { + data: { language } + } = message; + window.i18nextInstance = await i18nService(language); + updateFeatureMenuTitle(window.i18nextInstance.t("pages.content.features.featureMenu.label")); + updateFeatureMenuItemLabel("screenshotButton", window.i18nextInstance.t("pages.content.features.screenshotButton.label")); + updateFeatureMenuItemLabel("maximizePlayerButton", window.i18nextInstance.t("pages.content.features.maximizePlayerButton.label")); + updateFeatureMenuItemLabel("loopButton", window.i18nextInstance.t("pages.content.features.loopButton.label")); + break; + } default: { return; } diff --git a/src/pages/inject/index.tsx b/src/pages/inject/index.tsx index db87201d..0c05baa1 100644 --- a/src/pages/inject/index.tsx +++ b/src/pages/inject/index.tsx @@ -1,6 +1,7 @@ import { getVideoHistory, setVideoHistory } from "@/src/features/videoHistory/utils"; -import type { ContentSendOnlyMessageMappings, Messages, StorageChanges, configuration } from "@/src/types"; +import type { ContentSendOnlyMessageMappings, Messages, StorageChanges, configuration } from "@/src/@types"; import { parseReviver, sendExtensionOnlyMessage, sendExtensionMessage, parseStoredValue } from "@/src/utils/utilities"; +import type { AvailableLocales } from "@/src/i18n"; /** * Adds a script element to the document's root element, which loads a JavaScript file from the extension's runtime URL. @@ -108,6 +109,23 @@ document.addEventListener("yte-message-from-youtube", async () => { chrome.storage.local.set({ remembered_volumes: { ...message.data } }); break; } + case "extensionURL": { + sendExtensionMessage("extensionURL", "data_response", { + extensionURL: chrome.runtime.getURL("") + }); + break; + } + case "language": { + const language = await new Promise((resolve) => { + chrome.storage.local.get("language", (o) => { + resolve(o.language); + }); + }); + sendExtensionMessage("language", "data_response", { + language + }); + break; + } } }); const storageListeners = async (changes: StorageChanges, areaName: string) => { @@ -202,6 +220,11 @@ const storageChangeHandler = async (changes: StorageChanges, areaName: string) = sendExtensionOnlyMessage("hideScrollBarChange", { hideScrollBarEnabled: castedChanges.enable_hide_scrollbar.newValue }); + }, + language: () => { + sendExtensionOnlyMessage("languageChange", { + language: castedChanges.language.newValue + }); } }; Object.entries( diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 14ba4f02..f1f8e8d5 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1,5 +1,6 @@ import z from "zod"; -import type { PartialConfigurationToZodSchema, configuration } from "../types"; +import type { PartialConfigurationToZodSchema, configuration } from "../@types"; +import { availableLocales } from "../i18n/index"; import { screenshotFormat, screenshotType, @@ -7,7 +8,7 @@ import { onScreenDisplayType, onScreenDisplayPosition, youtubePlayerQualityLevel -} from "../types"; +} from "../@types"; export const outputFolderName = "dist"; export const defaultConfiguration = { // Options @@ -35,7 +36,8 @@ export const defaultConfiguration = { volume_adjustment_steps: 5, volume_boost_amount: 1, player_quality: "auto", - player_speed: 1 + player_speed: 1, + language: "en-US" } satisfies configuration; export const configurationImportSchema: PartialConfigurationToZodSchema = z.object({ @@ -62,5 +64,6 @@ export const configurationImportSchema: PartialConfigurationToZodSchema (value2: unknown) => value1 === value2; diff --git a/tsconfig.json b/tsconfig.json index 6b7e534b..cf630528 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -29,5 +29,5 @@ "@/hooks/*": ["src/hooks/*"] } }, - "include": ["src", "src/utils", "vite.config.ts"] + "include": ["src", "src/utils", "vite.config.ts", "public/locales"] }