mirror of
https://github.com/FreeTubeApp/FreeTube
synced 2024-11-23 02:16:10 +01:00
Add a polyfill for vue-i18n's useI18n()
composable (#6042)
* Add a polyfill for vue-i18n's useI18n() composable * Address feedback * JSDoc ESLint plugin requires each overload to be in a separate comment block
This commit is contained in:
parent
6d5296dd4c
commit
c1d020d3df
11
_scripts/eslint-rules/plugin.mjs
Normal file
11
_scripts/eslint-rules/plugin.mjs
Normal file
@ -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
_scripts/eslint-rules/use-i18n-polyfill-rule.mjs
Normal file
62
_scripts/eslint-rules/use-i18n-polyfill-rule.mjs
Normal file
@ -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}'`)
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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' }
|
||||
|
||||
@ -42,6 +43,7 @@ export default [
|
||||
plugins: {
|
||||
unicorn: eslintPluginUnicorn,
|
||||
jsdoc,
|
||||
freetube,
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
@ -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',
|
||||
},
|
||||
},
|
||||
|
||||
@ -220,7 +224,7 @@ export default [
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['_scripts/*.mjs'],
|
||||
files: ['_scripts/**/*.mjs'],
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
|
@ -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}\"",
|
||||
|
@ -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
|
||||
|
||||
@ -993,7 +994,7 @@ export default defineComponent({
|
||||
events.dispatchEvent(new CustomEvent('localeChanged'))
|
||||
}
|
||||
|
||||
watch(() => i18n.locale, setLocale)
|
||||
watch(locale, setLocale)
|
||||
|
||||
// #endregion player locales
|
||||
|
||||
@ -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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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)
|
||||
@ -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
|
||||
|
95
src/renderer/composables/use-i18n-polyfill.js
Normal file
95
src/renderer/composables/use-i18n-polyfill.js
Normal file
@ -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)
|
||||
}
|
Loading…
Reference in New Issue
Block a user