diff --git a/.eslintrc.js b/.eslintrc.js index ea7a59c493..53ae8e253d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,6 +1,14 @@ // WebStorm fix for `~` alias not working: // https://intellij-support.jetbrains.com/hc/en-us/community/posts/115000771544-ESLint-does-not-work-with-webpack-import-resolver-in-2017-3 process.chdir(__dirname) +// [id.properties:has([key.name="${methodName}"])] +const i18nDestructureRules = ['t', 'tc', 'te', 'td', 'd', 'n'].map( + (methodName) => ({ + selector: `VariableDeclarator[id.type="ObjectPattern"]:has(Property[key.name="${methodName}"])[init.callee.name="useI18n"]`, + message: `Do not destructure ${methodName} from the i18n object as its methods internally depend on "this". Instead, use it directly (e.g., "i18n.${methodName}"). If you need an independent reference to the function then bind it or wrap it in a closure.`, + }) +) + module.exports = { root: true, env: { @@ -78,6 +86,7 @@ module.exports = { message: 'Use the component instead of .', }, ], + 'no-restricted-syntax': ['error', ...i18nDestructureRules], 'unicorn/filename-case': ['error', { case: 'kebabCase' }], '@typescript-eslint/ban-ts-comment': ['warn'], '@typescript-eslint/no-var-requires': ['off'], diff --git a/nuxt.config.ts b/nuxt.config.ts index 7277753622..2750ce1708 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -11,6 +11,7 @@ import { sentryConfig } from './src/utils/sentry-config' import { env } from './src/utils/env' import type { NuxtConfig } from '@nuxt/types' +import type { LocaleObject } from '@nuxtjs/i18n' /** * The default metadata for the site. Can be extended and/or overwritten per page. And even in components! @@ -189,7 +190,7 @@ const config: NuxtConfig = { file: 'en.json', }, ...(locales ?? []), - ].filter((l) => Boolean(l.iso)), + ].filter((l) => Boolean(l.iso)) as LocaleObject[], lazy: true, langDir: 'locales', defaultLocale: 'en', diff --git a/src/components/VLink.vue b/src/components/VLink.vue index 33878a12a0..9b5977d15e 100644 --- a/src/components/VLink.vue +++ b/src/components/VLink.vue @@ -33,9 +33,11 @@ export default defineComponent({ }, setup(props) { const { app } = useContext() - const hasHref = computed( - () => typeof props.href === 'string' && !['', '#'].includes(props.href) - ) + function checkHref(p: typeof props): p is { href: string } { + return typeof p.href === 'string' && !['', '#'].includes(p.href) + } + + const hasHref = computed(() => checkHref(props)) const isInternal = computed( () => hasHref.value && props.href?.startsWith('/') ) @@ -44,7 +46,7 @@ export default defineComponent({ ) let linkProperties = computed(() => - hasHref.value + checkHref(props) ? isInternal.value ? { to: app?.localePath(props.href) ?? props.href } : { ...defaultProps, href: props.href } diff --git a/src/composables/use-i18n-sync.ts b/src/composables/use-i18n-sync.ts new file mode 100644 index 0000000000..138942929c --- /dev/null +++ b/src/composables/use-i18n-sync.ts @@ -0,0 +1,30 @@ +import { computed, useContext } from '@nuxtjs/composition-api' + +import type { LocaleObject } from '@nuxtjs/i18n' + +const BASE_URL = 'https://translate.wordpress.org/projects/meta/openverse/' + +export function useI18nSync() { + const { app } = useContext() + const currentLocale = computed(() => { + return (app.i18n?.locales as LocaleObject[]).find( + (item) => item.code === app.i18n.locale + ) + }) + + const needsTranslationBanner = computed(() => { + if (!currentLocale.value || currentLocale.value.code === 'en') return false + + return (currentLocale.value?.translated ?? 100) <= 90 + }) + + const translationLink = computed( + () => `${BASE_URL}${currentLocale.value?.code || 'en'}/default/` + ) + + return { + currentLocale, + needsTranslationBanner, + translationLink, + } +} diff --git a/src/composables/use-i18n.js b/src/composables/use-i18n.ts similarity index 100% rename from src/composables/use-i18n.js rename to src/composables/use-i18n.ts diff --git a/src/utils/license.ts b/src/utils/license.ts index 1181767fc5..4ef8eff42b 100644 --- a/src/utils/license.ts +++ b/src/utils/license.ts @@ -5,7 +5,7 @@ import { PUBLIC_DOMAIN_MARKS, } from '~/constants/license' -import type { IVueI18n } from 'vue-i18n' +import type VueI18n from 'vue-i18n' /** * Get the full name of the license in a displayable format from the license @@ -19,7 +19,7 @@ import type { IVueI18n } from 'vue-i18n' export const getFullLicenseName = ( license: License, licenseVersion: LicenseVersion = '', // unknown version - i18n: IVueI18n | null = null + i18n: VueI18n | null = null ): string => { let licenseName diff --git a/tsconfig.json b/tsconfig.json index 79204084b0..a577198dd1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -25,7 +25,7 @@ /* Module Resolution Options */ "moduleResolution": "Node", "resolveJsonModule": true, - "types": ["@types/jest"], + "types": ["@types/jest", "@nuxt/types", "@nuxtjs/i18n"], "typeRoots": ["./typings", "./node_modules/@types"], "paths": { "~/*": ["./src/*"], diff --git a/typings/nuxt__types/index.d.ts b/typings/nuxt__types/index.d.ts index a2d55de351..10a53e1e7d 100644 --- a/typings/nuxt__types/index.d.ts +++ b/typings/nuxt__types/index.d.ts @@ -1,12 +1,8 @@ import '@nuxt/types' -import type { IVueI18n } from 'vue-i18n' import type { Details as UADetails } from 'express-useragent' declare module '@nuxt/types' { export interface NuxtAppOptions { $ua: UADetails | null } - export interface Context { - i18n: IVueI18n - } }