From 19a21b85790de8d43a701e5b641b4e130edafcf3 Mon Sep 17 00:00:00 2001 From: absidue <48293849+absidue@users.noreply.github.com> Date: Wed, 30 Oct 2024 15:47:21 +0100 Subject: [PATCH 1/3] Add a polyfill for vue-i18n's useI18n() composable --- _scripts/eslint-rules/plugin.mjs | 11 +++ .../eslint-rules/use-i18n-polyfill-rule.mjs | 62 +++++++++++++++ eslint.config.mjs | 6 +- .../ft-shaka-video-player.js | 17 ++-- src/renderer/composables/use-i18n-polyfill.js | 79 +++++++++++++++++++ 5 files changed, 166 insertions(+), 9 deletions(-) create mode 100644 _scripts/eslint-rules/plugin.mjs create mode 100644 _scripts/eslint-rules/use-i18n-polyfill-rule.mjs create mode 100644 src/renderer/composables/use-i18n-polyfill.js diff --git a/_scripts/eslint-rules/plugin.mjs b/_scripts/eslint-rules/plugin.mjs new file mode 100644 index 0000000000000..1baf7ddbe4a37 --- /dev/null +++ b/_scripts/eslint-rules/plugin.mjs @@ -0,0 +1,11 @@ +import useI18nPolyfillRule from './use-i18n-polyfill-rule.mjs' + +export default { + meta: { + name: 'eslint-plugin-freetube', + version: '1.0' + }, + rules: { + 'use-i18n-polyfill': useI18nPolyfillRule + } +} diff --git a/_scripts/eslint-rules/use-i18n-polyfill-rule.mjs b/_scripts/eslint-rules/use-i18n-polyfill-rule.mjs new file mode 100644 index 0000000000000..1d27f74f0f9d1 --- /dev/null +++ b/_scripts/eslint-rules/use-i18n-polyfill-rule.mjs @@ -0,0 +1,62 @@ +import { dirname, relative, resolve } from 'path' + +const polyfillPath = resolve(import.meta.dirname, '../../src/renderer/composables/use-i18n-polyfill') + +function getRelativePolyfillPath(filePath) { + const relativePath = relative(dirname(filePath), polyfillPath).replaceAll('\\', '/') + + if (relativePath[0] !== '.') { + return `./${relativePath}` + } + + return relativePath +} + +/** @type {import('eslint').Rule.RuleModule} */ +export default { + meta: { + type: 'problem', + fixable: 'code' + }, + create(context) { + return { + 'ImportDeclaration[source.value="vue-i18n"]'(node) { + const specifierIndex = node.specifiers.findIndex(specifier => specifier.type === 'ImportSpecifier' && specifier.imported.name === 'useI18n') + + if (specifierIndex !== -1) { + context.report({ + node: node.specifiers.length === 1 ? node : node.specifiers[specifierIndex], + message: "Please use FreeTube's useI18n polyfill, as vue-i18n's useI18n composable does not work when the vue-i18n is in legacy mode, which is needed for components using the Options API.", + fix: context.physicalFilename === '' + ? undefined + : (fixer) => { + const relativePath = getRelativePolyfillPath(context.physicalFilename) + + // If the import only imports `useI18n`, we can just update the source/from text + // Else we need to create a new import for `useI18n` and remove useI18n from the original one + if (node.specifiers.length === 1) { + return fixer.replaceText(node.source, `'${relativePath}'`) + } else { + const specifier = node.specifiers[specifierIndex] + + let specifierText = 'useI18n' + + if (specifier.imported.name !== specifier.local.name) { + specifierText += ` as ${specifier.local.name}` + } + + return [ + fixer.removeRange([ + specifierIndex === 0 ? specifier.start : node.specifiers[specifierIndex - 1].end, + specifierIndex === node.specifiers.length - 1 ? specifier.end : node.specifiers[specifierIndex + 1].start + ]), + fixer.insertTextAfter(node, `\nimport { ${specifierText} } from '${relativePath}'`) + ] + } + } + }) + } + } + } + } +} diff --git a/eslint.config.mjs b/eslint.config.mjs index 5971c36f0f75d..c35387ce33f88 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -10,6 +10,7 @@ import eslintPluginJsonc from 'eslint-plugin-jsonc' import eslintPluginYml from 'eslint-plugin-yml' import yamlEslintParser from 'yaml-eslint-parser' import neostandard from 'neostandard' +import eslintPluginFreeTube from './_scripts/eslint-rules/plugin.mjs' import activeLocales from './static/locales/activeLocales.json' with { type: 'json' } @@ -40,6 +41,7 @@ export default [ ], plugins: { unicorn: eslintPluginUnicorn, + freetube: eslintPluginFreeTube }, languageOptions: { @@ -115,6 +117,8 @@ export default [ '@intlify/vue-i18n/no-deprecated-tc': 'off', 'vue/require-explicit-emits': 'error', 'vue/no-unused-emit-declarations': 'error', + + 'freetube/use-i18n-polyfill': 'error' }, }, @@ -209,7 +213,7 @@ export default [ } }, { - files: ['_scripts/*.mjs'], + files: ['_scripts/**/*.mjs'], languageOptions: { globals: { ...globals.node, diff --git a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js index 5f3c5e5be6266..0c4741639d8ec 100644 --- a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js +++ b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js @@ -3,10 +3,9 @@ import path from 'path' import { computed, defineComponent, onBeforeUnmount, onMounted, reactive, ref, shallowRef, watch } from 'vue' import shaka from 'shaka-player' +import { useI18n } from '../../composables/use-i18n-polyfill' import store from '../../store/index' -import i18n from '../../i18n/index' - import { IpcChannels } from '../../../constants' import { AudioTrackSelection } from './player-components/AudioTrackSelection' import { FullWindowButton } from './player-components/FullWindowButton' @@ -115,6 +114,8 @@ export default defineComponent({ 'toggle-theatre-mode' ], setup: function (props, { emit, expose }) { + const { locale, t } = useI18n() + /** @type {shaka.Player|null} */ let player = null @@ -991,7 +992,7 @@ export default defineComponent({ events.dispatchEvent(new CustomEvent('localeChanged')) } - watch(() => i18n.locale, setLocale) + watch(locale, setLocale) // #endregion player locales @@ -1500,7 +1501,7 @@ export default defineComponent({ }) } catch (err) { console.error(`Parse failed: ${err.message}`) - showToast(i18n.t('Screenshot Error', { error: err.message })) + showToast(t('Screenshot Error', { error: err.message })) canvas.remove() return } @@ -1565,7 +1566,7 @@ export default defineComponent({ await fs.mkdir(dirPath, { recursive: true }) } catch (err) { console.error(err) - showToast(i18n.t('Screenshot Error', { error: err })) + showToast(t('Screenshot Error', { error: err })) canvas.remove() return } @@ -1579,11 +1580,11 @@ export default defineComponent({ fs.writeFile(filePath, arr) .then(() => { - showToast(i18n.t('Screenshot Success', { filePath })) + showToast(t('Screenshot Success', { filePath })) }) .catch((err) => { console.error(err) - showToast(i18n.t('Screenshot Error', { error: err })) + showToast(t('Screenshot Error', { error: err })) }) }) }, mimeType, imageQuality) @@ -2314,7 +2315,7 @@ export default defineComponent({ player.getNetworkingEngine().registerResponseFilter(responseFilter) } - await setLocale(i18n.locale) + await setLocale(locale.value) // check if the component is already getting destroyed // which is possible because this function runs asynchronously diff --git a/src/renderer/composables/use-i18n-polyfill.js b/src/renderer/composables/use-i18n-polyfill.js new file mode 100644 index 0000000000000..4c1ec8a95a720 --- /dev/null +++ b/src/renderer/composables/use-i18n-polyfill.js @@ -0,0 +1,79 @@ +/* eslint-disable @intlify/vue-i18n/no-dynamic-keys */ +import { computed } from 'vue' + +import i18n from '../i18n/index' + +/** + * Polyfill for vue-i18n's useI18n composable, as it is not available in Vue 2 + * and doesn't work when vue-i18n 9+ (used for Vue 3) is set to `legacy: true`, + * which is needed for Options API components. + * + * Yes, vue-i18n 9 has an `allowComposition` option, + * but it comes with limitations that this polyfill doesn't have and was removed in vue-i18n 10. + * + * @see https://vue-i18n.intlify.dev/guide/migration/vue3#limitations + * @see https://vue-i18n.intlify.dev/guide/migration/breaking10.html#drop-allowcomposition-option + */ +export function useI18n() { + const locale = computed({ + get() { + return i18n.locale + }, + set(locale) { + i18n.locale = locale + } + }) + + return { + locale, + t + } +} + +/** + * @overload + * @param {string} key + * @returns {string} + * + * @overload + * @param {string} key + * @param {number} plural + * @returns {string} + * + * @overload + * @param {string} key + * @param {unknown[]} list + * @returns {string} + * + * @overload + * @param {string} key + * @param {unknown[]} list + * @param {number} plural + * @returns {string} + * + * @overload + * @param {string} key + * @param {Record} named + * @returns {string} + * + * @overload + * @param {string} key + * @param {Record} named + * @param {number} plural + * @returns {string} + * + * @param {string} key + * @param {number | unknown[] | Record | undefined} arg1 + * @param {number | undefined} arg2 + * @returns {string} + */ +function t(key, arg1, arg2) { + // Remove these lines in the Vue 3 migration and pass all args to the `.t()` call + if (typeof arg1 === 'number') { + return i18n.tc(key, arg1) + } else if (typeof arg2 === 'number') { + return i18n.tc(key, arg2, arg1) + } + + return i18n.t(key, arg1) +} From 91c3918b305838f65740b9e05174f052ab97d879 Mon Sep 17 00:00:00 2001 From: absidue <48293849+absidue@users.noreply.github.com> Date: Thu, 31 Oct 2024 18:45:02 +0100 Subject: [PATCH 2/3] Address feedback --- package.json | 4 ++-- src/renderer/composables/use-i18n-polyfill.js | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index c24f4c088db64..10431bc057068 100644 --- a/package.json +++ b/package.json @@ -37,8 +37,8 @@ "lint-all": "run-p lint lint-json", "lint": "run-p eslint-lint lint-style", "lint-fix": "run-p eslint-lint-fix lint-style-fix", - "eslint-lint": "eslint --config eslint.config.mjs \"./src/**/*.js\" \"./src/**/*.vue\" \"./static/**/*.js\" \"./_scripts/*.js\" \"./_scripts/*.mjs\"", - "eslint-lint-fix": "eslint --config eslint.config.mjs --fix \"./src/**/*.js\" \"./src/**/*.vue\" \"./static/**/*.js\" \"./_scripts/*.js\" \"./_scripts/*.mjs\"", + "eslint-lint": "eslint --config eslint.config.mjs \"./src/**/*.js\" \"./src/**/*.vue\" \"./static/**/*.js\" \"./_scripts/*.js\" \"_scripts/**/*.mjs\"", + "eslint-lint-fix": "eslint --config eslint.config.mjs --fix \"./src/**/*.js\" \"./src/**/*.vue\" \"./static/**/*.js\" \"./_scripts/*.js\" \"_scripts/**/*.mjs\"", "lint-json": "eslint --config eslint.config.mjs \"./static/**/*.json\"", "lint-style": "stylelint \"**/*.{css,scss}\"", "lint-style-fix": "stylelint --fix \"**/*.{css,scss}\"", diff --git a/src/renderer/composables/use-i18n-polyfill.js b/src/renderer/composables/use-i18n-polyfill.js index 4c1ec8a95a720..2b9607e58305f 100644 --- a/src/renderer/composables/use-i18n-polyfill.js +++ b/src/renderer/composables/use-i18n-polyfill.js @@ -75,5 +75,9 @@ function t(key, arg1, arg2) { return i18n.tc(key, arg2, arg1) } - return i18n.t(key, arg1) + if (arg1 != null) { + return i18n.t(key, arg1) + } + + return i18n.t(key) } From 60f62d0796806a22c26bd79e963e558f1494d40f Mon Sep 17 00:00:00 2001 From: absidue <48293849+absidue@users.noreply.github.com> Date: Sun, 3 Nov 2024 12:34:16 +0100 Subject: [PATCH 3/3] JSDoc ESLint plugin requires each overload to be in a separate comment block --- src/renderer/composables/use-i18n-polyfill.js | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/renderer/composables/use-i18n-polyfill.js b/src/renderer/composables/use-i18n-polyfill.js index 2b9607e58305f..a8b7fa871dc82 100644 --- a/src/renderer/composables/use-i18n-polyfill.js +++ b/src/renderer/composables/use-i18n-polyfill.js @@ -34,34 +34,46 @@ export function useI18n() { * @overload * @param {string} key * @returns {string} - * + */ + +/** * @overload * @param {string} key * @param {number} plural * @returns {string} - * + */ + +/** * @overload * @param {string} key * @param {unknown[]} list * @returns {string} - * + */ + +/** * @overload * @param {string} key * @param {unknown[]} list * @param {number} plural * @returns {string} - * + */ + +/** * @overload * @param {string} key * @param {Record} named * @returns {string} - * + */ + +/** * @overload * @param {string} key * @param {Record} named * @param {number} plural * @returns {string} - * + */ + +/** * @param {string} key * @param {number | unknown[] | Record | undefined} arg1 * @param {number | undefined} arg2