Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a polyfill for vue-i18n's useI18n() composable #6042

Open
wants to merge 4 commits into
base: development
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions _scripts/eslint-rules/plugin.mjs
Original file line number Diff line number Diff line change
@@ -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
}
}
62 changes: 62 additions & 0 deletions _scripts/eslint-rules/use-i18n-polyfill-rule.mjs
Original file line number Diff line number Diff line change
@@ -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 === '<text>'
? 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}'`)
]
}
}
})
}
}
}
}
}
6 changes: 5 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import eslintPluginYml from 'eslint-plugin-yml'
import yamlEslintParser from 'yaml-eslint-parser'
import neostandard from 'neostandard'
import jsdoc from 'eslint-plugin-jsdoc'
import freetube from './_scripts/eslint-rules/plugin.mjs'

import activeLocales from './static/locales/activeLocales.json' with { type: 'json' }

Expand Down Expand Up @@ -42,6 +43,7 @@ export default [
plugins: {
unicorn: eslintPluginUnicorn,
jsdoc,
freetube,
},

languageOptions: {
Expand Down Expand Up @@ -126,6 +128,8 @@ export default [
'jsdoc/check-types': 'error',
'jsdoc/no-bad-blocks': 'error',
'jsdoc/no-multi-asterisks': 'error',

'freetube/use-i18n-polyfill': 'error',
},
},

Expand Down Expand Up @@ -220,7 +224,7 @@ export default [
}
},
{
files: ['_scripts/*.mjs'],
files: ['_scripts/**/*.mjs'],
absidue marked this conversation as resolved.
Show resolved Hide resolved
languageOptions: {
globals: {
...globals.node,
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}\"",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -993,7 +994,7 @@ export default defineComponent({
events.dispatchEvent(new CustomEvent('localeChanged'))
}

watch(() => i18n.locale, setLocale)
watch(locale, setLocale)

// #endregion player locales

Expand Down Expand Up @@ -1502,7 +1503,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
}
Expand Down Expand Up @@ -1567,7 +1568,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
}
Expand All @@ -1581,11 +1582,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)
Expand Down Expand Up @@ -2319,7 +2320,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
Expand Down
95 changes: 95 additions & 0 deletions src/renderer/composables/use-i18n-polyfill.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/* 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<string, unknown>} named
* @returns {string}
*/

/**
* @overload
* @param {string} key
* @param {Record<string, unknown>} named
* @param {number} plural
* @returns {string}
*/

/**
* @param {string} key
* @param {number | unknown[] | Record<string, unknown> | 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)
}

if (arg1 != null) {
return i18n.t(key, arg1)
}

return i18n.t(key)
}