diff --git a/docs/content/en/api.md b/docs/content/en/api.md
index 3512956a0..41bab3964 100644
--- a/docs/content/en/api.md
+++ b/docs/content/en/api.md
@@ -57,11 +57,20 @@ All [Vue I18n properties and methods](http://kazupon.github.io/vue-i18n/api/#vue
See also [Basic usage - nuxt-link](../basic-usage#nuxt-link).
-#### $nuxtI18nSeo
+#### $nuxtI18nHead
+
+ - **Arguments**:
+ - options: (type: [`NuxtI18nHeadOptions`](https://github.com/nuxt-community/i18n-module/blob/master/types/nuxt-i18n.d.ts))
+ - **Returns**: [`MetaInfo`](https://github.com/nuxt/vue-meta/blob/74182e388ad1b1977cb7217b0ade729321761403/types/vue-meta.d.ts#L173)
+
+ If `options.addDirAttribute` is set to `false`, the `dir` attribute will not be added.
+ If `options.addSeoAttributes` is set to `true`, the seo attributes will be added.
+
+#### $nuxtI18nSeo deprecated (use `$nuxtI18nHead`)
- **Arguments**:
- no arguments
- - **Returns**: `NuxtI18nSeo`
+ - **Returns**: [`MetaInfo`](https://github.com/nuxt/vue-meta/blob/74182e388ad1b1977cb7217b0ade729321761403/types/vue-meta.d.ts#L173)
SEO object provided mostly for use with [SEO - Improving Performance](../seo#improving-performance).
@@ -109,6 +118,12 @@ Instance of [VueI18n class](http://kazupon.github.io/vue-i18n/api/#vuei18n-class
### Properties
+#### defaultDirection
+
+ - **Type**: `Directions`
+
+ Default direction as specified in options.
+
#### defaultLocale
- **Type**: `string`
diff --git a/docs/content/en/options-reference.md b/docs/content/en/options-reference.md
index d98e608bf..6069262fe 100644
--- a/docs/content/en/options-reference.md
+++ b/docs/content/en/options-reference.md
@@ -54,23 +54,45 @@ List of locales supported by your app. Can either be an array of codes (`['en',
```js
[
- { code: 'en', iso: 'en-US', file: 'en.js' },
- { code: 'fr', iso: 'fr-FR', file: 'fr.js' },
- { code: 'es', iso: 'es-ES', file: 'es.js' }
+ { code: 'en', iso: 'en-US', file: 'en.js', dir: 'ltr' },
+ { code: 'ar', iso: 'ar-EG', file: 'ar.js', dir: 'rtl' }
+ { code: 'fr', iso: 'fr-FR', file: 'fr.js', },
]
```
When using an object form, the properties can be:
- `code` (**required**) - unique identifier of the locale
-- `iso` (required when using SEO features and) - The ISO code used for SEO features and for matching browser locales when using [detectBrowserLanguage](#detectbrowserlanguage) functionality. Should be in one of those formats:
+- `iso` (required when using SEO features) - The ISO code used for SEO features and for matching browser locales when using [detectBrowserLanguage](#detectbrowserlanguage) functionality. Should be in one of those formats:
* ISO 639-1 code (e.g. `'en'`)
* ISO 639-1 and ISO 3166-1 alpha-2 codes, separated by hyphen (e.g. `'en-US'`)
- `file` (required when using `lazy`) - the name of the file. Will be resolved relative to `langDir` path when loading locale messages lazily
+- `dir` The dir property specifies the direction of the elements and content, value could be `'rtl'`, `'ltr'` or `'auto'`.
- `domain` (required when using `differentDomains`) - the domain name you'd like to use for that locale (including the port if used)
- `...` - any custom property set on the object will be exposed at runtime. This can be used, for example, to define the language name for the purpose of using it in a language selector on the page.
You can access all the properties of the current locale through the `localeProperties` property. When using an array of codes, it will only include the `code` property.
+
+
+For direction attribute to be set, you MUST use the `$nuxtI18nHead` method in your layout.
+
+```js {}[layouts/default.vue]
+export default {
+ head () {
+ return this.$nuxtI18nHead()
+ }
+}
+```
+
+
+
+## `defaultDirection`
+
+- type: `string`
+- default: `ltr`
+
+The app's default direction. Will only be used when `dir` is not specified.
+
## `defaultLocale`
- type: `string` or `null`
@@ -149,12 +171,18 @@ Set to a path to which you want to redirect users accessing the root URL (`/`).
}
```
-## `seo`
+## `seo` (deprecated)
+
+
+
+This option is deprecated from v6.19.0. The recommended way is to set up SEO as described in [Improving performance](/seo#improving-performance).
+
+
- type: `boolean`
- default: `false`
-If `true`, a SEO metadata will be generated for the routes. Note that performance can suffer with this option enabled and there might be compatibility issues with some plugins. The recommended way is to set up SEO as described in [Improving performance](/seo#improving-performance).
+If `true`, a SEO metadata will be generated for the routes. Note that performance can suffer with this option enabled and there might be compatibility issues with some plugins.
## `differentDomains`
diff --git a/docs/content/en/seo.md b/docs/content/en/seo.md
index 46301633a..41e9b5bf3 100644
--- a/docs/content/en/seo.md
+++ b/docs/content/en/seo.md
@@ -7,7 +7,7 @@ category: Guide
-Using `seo` option (or alternatively the `$nuxtI18nSeo`-based solution - see [Improving Performance](#improving-performance)) requires that locales are configured as an array of objects and not strings.
+Using `seo` option (or preferably the `$nuxtI18nHead`-based solution - see [Improving Performance](#improving-performance)) requires that locales are configured as an array of objects and not strings.
@@ -141,7 +141,7 @@ export default {
}
```
-To override SEO metadata for any page, simply declare your own `head ()` method. Have a look at [src/templates/seo-head.js](https://github.com/nuxt-community/i18n-module/blob/master/src/templates/seo-head.js) if you want to copy some of **nuxt-i18n**'s logic.
+To override SEO metadata for any page, simply declare your own `head ()` method. Have a look at [src/templates/head-meta.js](https://github.com/nuxt-community/i18n-module/blob/master/src/templates/head-meta.js) if you want to copy some of **nuxt-i18n**'s logic.
## Improving performance
@@ -149,7 +149,7 @@ The default method to inject SEO metadata, while convenient, comes at a performa
The `head` method is registered for every component in your app.
This means each time a component is created, the SEO metadata is recomputed for every components.
-To improve performance you can use the `$nuxtI18nSeo` method in your layout instead.
+To improve performance you can use the `$nuxtI18nHead` method in your layout instead.
It will generate i18n SEO metadata for the current context.
First make sure automatic SEO is disabled by setting `seo` to `false` in your configuration or removing that option completely:
@@ -160,12 +160,12 @@ First make sure automatic SEO is disabled by setting `seo` to `false` in your co
}]
```
-Then in your app layout declare the [`head` hook](https://nuxtjs.org/guides/features/meta-tags-seo) and use `$nuxtI18nSeo` inside to generate i18n SEO meta information:
+Then in your app layout declare the [`head` hook](https://nuxtjs.org/guides/features/meta-tags-seo) and use `$nuxtI18nHead` inside to generate i18n SEO meta information:
```js {}[layouts/default.vue]
export default {
head () {
- return this.$nuxtI18nSeo()
+ return this.$nuxtI18nHead({ addSeoAttributes: true })
}
}
```
@@ -177,16 +177,16 @@ Now SEO metadata will only be computed for the layout instead of every component
### Merging i18n SEO metadata with your own
-If you want to add your own meta in the layout you can easily merge the object returned by `$nuxtI18nSeo` with your own:
+If you want to add your own meta in the layout you can easily merge the object returned by `$nuxtI18nHead` with your own:
```js {}[layouts/default.vue]
export default {
head () {
- const i18nSeo = this.$nuxtI18nSeo()
+ const i18nHead = this.$nuxtI18nHead({ addSeoAttributes: true })
return {
htmlAttrs: {
myAttribute: 'My Value',
- ...i18nSeo.htmlAttrs
+ ...i18nHead.htmlAttrs
},
meta: [
{
@@ -194,7 +194,7 @@ export default {
name: 'description',
content: 'My Custom Description'
},
- ...i18nSeo.meta
+ ...i18nHead.meta
],
link: [
{
@@ -203,7 +203,7 @@ export default {
sizes: '180x180',
href: '/apple-touch-icon.png'
},
- ...i18nSeo.link
+ ...i18nHead.link
]
}
}
diff --git a/docs/content/es/api.md b/docs/content/es/api.md
index eb0df66e6..2fedcfc43 100644
--- a/docs/content/es/api.md
+++ b/docs/content/es/api.md
@@ -57,11 +57,20 @@ Todos los [Vue I18n propiedades y métodos](http://kazupon.github.io/vue-i18n/ap
See also [Basic usage - nuxt-link](../basic-usage#nuxt-link).
-#### $nuxtI18nSeo
+#### $nuxtI18nHead
+
+ - **Arguments**:
+ - options: (type: [`NuxtI18nHeadOptions`](https://github.com/nuxt-community/i18n-module/blob/master/types/nuxt-i18n.d.ts))
+ - **Returns**: [`MetaInfo`](https://github.com/nuxt/vue-meta/blob/74182e388ad1b1977cb7217b0ade729321761403/types/vue-meta.d.ts#L173)
+
+ If `options.addDirAttribute` is set to `false`, the `dir` attribute will not be added.
+ If `options.addSeoAttributes` is set to `true`, the seo attributes will be added.
+
+#### $nuxtI18nSeo deprecated (use `$nuxtI18nHead`)
- **Arguments**:
- no arguments
- - **Returns**: `NuxtI18nSeo`
+ - **Returns**: [`MetaInfo`](https://github.com/nuxt/vue-meta/blob/74182e388ad1b1977cb7217b0ade729321761403/types/vue-meta.d.ts#L173)
SEO object provided mostly for use with [SEO - Improving Performance](../seo#improving-performance).
@@ -109,6 +118,12 @@ Instance of [VueI18n class](http://kazupon.github.io/vue-i18n/api/#vuei18n-class
### Properties
+#### defaultDirection
+
+ - **Type**: `Directions`
+
+ Default direction as specified in options.
+
#### defaultLocale
- **Type**: `string`
diff --git a/docs/content/es/options-reference.md b/docs/content/es/options-reference.md
index 66fe68cb6..5a8e8985f 100644
--- a/docs/content/es/options-reference.md
+++ b/docs/content/es/options-reference.md
@@ -54,23 +54,45 @@ List of locales supported by your app. Can either be an array of codes (`['en',
```js
[
- { code: 'en', iso: 'en-US', file: 'en.js' },
- { code: 'fr', iso: 'fr-FR', file: 'fr.js' },
+ { code: 'en', iso: 'en-US', file: 'en.js', dir: 'ltr' },
+ { code: 'ar', iso: 'ar-EG', file: 'ar.js', dir: 'rtl' }
{ code: 'es', iso: 'es-ES', file: 'es.js' }
]
```
When using an object form, the properties can be:
- `code` (**required**) - unique identifier of the locale
-- `iso` (required when using SEO features and) - The ISO code used for SEO features and for matching browser locales when using [detectBrowserLanguage](#detectbrowserlanguage) functionality. Should be in one of those formats:
+- `iso` (required when using SEO features) - The ISO code used for SEO features and for matching browser locales when using [detectBrowserLanguage](#detectbrowserlanguage) functionality. Should be in one of those formats:
* ISO 639-1 code (e.g. `'en'`)
* ISO 639-1 and ISO 3166-1 alpha-2 codes, separated by hyphen (e.g. `'en-US'`)
- `file` (required when using `lazy`) - the name of the file. Will be resolved relative to `langDir` path when loading locale messages lazily
+- `dir` The dir property specifies the direction of the elements and content, value could be `'rtl'`, `'ltr'` or `'auto'`.
- `domain` (required when using `differentDomains`) - the domain name you'd like to use for that locale (including the port if used)
- `...` - any custom property set on the object will be exposed at runtime. This can be used, for example, to define the language name for the purpose of using it in a language selector on the page.
You can access all the properties of the current locale through the `localeProperties` property. When using an array of codes, it will only include the `code` property.
+
+
+For direction attribute to be set, you MUST use the `$nuxtI18nHead` method in your layout.
+
+```js {}[layouts/default.vue]
+export default {
+ head () {
+ return this.$nuxtI18nHead()
+ }
+}
+```
+
+
+
+## `defaultDirection`
+
+- type: `string`
+- default: `ltr`
+
+The app's default direction. Will only be used when `dir` is not specified.
+
## `defaultLocale`
- type: `string` or `null`
@@ -149,12 +171,18 @@ Set to a path to which you want to redirect users accessing the root URL (`/`).
}
```
-## `seo`
+## `seo` (deprecated)
+
+
+
+This option is deprecated from v6.19.0. The recommended way is to set up SEO as described in [Improving performance](/seo#improving-performance).
+
+
- type: `boolean`
- default: `false`
-If `true`, a SEO metadata will be generated for the routes. Note that performance can suffer with this option enabled and there might be compatibility issues with some plugins. The recommended way is to set up SEO as described in [Improving performance](/seo#improving-performance).
+If `true`, a SEO metadata will be generated for the routes. Note that performance can suffer with this option enabled and there might be compatibility issues with some plugins.
## `differentDomains`
diff --git a/docs/content/es/seo.md b/docs/content/es/seo.md
index 1dcb1c471..17bc9e20f 100644
--- a/docs/content/es/seo.md
+++ b/docs/content/es/seo.md
@@ -7,7 +7,7 @@ category: Guía
-Using `seo` option (or alternatively the `$nuxtI18nSeo`-based solution - see [Improving Performance](#improving-performance)) requires that locales are configured as an array of objects and not strings.
+Using `seo` option (or preferably the `$nuxtI18nHead`-based solution - see [Improving Performance](#improving-performance)) requires that locales are configured as an array of objects and not strings.
@@ -140,7 +140,7 @@ export default {
}
```
-Para anular los metadatos de SEO para cualquier página, simplemente declare su propio método `head ()`. Echa un vistazo a [src/templates/seo-head.js](https://github.com/nuxt-community/i18n-module/blob/master/src/templates/seo-head.js) si quieres copie parte de la lógica de **nuxt-i18n**.
+Para anular los metadatos de SEO para cualquier página, simplemente declare su propio método `head ()`. Echa un vistazo a [src/templates/head-meta.js](https://github.com/nuxt-community/i18n-module/blob/master/src/templates/head-meta.js) si quieres copie parte de la lógica de **nuxt-i18n**.
## Mejora del rendimiento
@@ -148,7 +148,7 @@ El método predeterminado para inyectar metadatos de SEO, aunque conveniente, ti
El método `head` se registra para cada componente de su aplicación.
Esto significa que cada vez que se crea un componente, los metadatos de SEO se vuelven a calcular para cada componente.
-Para mejorar el rendimiento, puede utilizar el método `$nuxtI18nSeo` en su diseño. Generará metadatos de SEO i18n para el contexto actual.
+Para mejorar el rendimiento, puede utilizar el método `$nuxtI18nHead` en su diseño. Generará metadatos de SEO i18n para el contexto actual.
Primero, asegúrese de que el SEO automático esté desactivado estableciendo `seo` en `false` en su configuración o eliminando esa opción por completo:
@@ -158,12 +158,12 @@ Primero, asegúrese de que el SEO automático esté desactivado estableciendo `s
}]
```
-Luego, en el diseño de su aplicación, declare el [`head` hook](https://nuxtjs.org/guides/features/meta-tags-seo) y use `$nuxtI18nSeo` dentro para generar la metainformación i18n SEO:
+Luego, en el diseño de su aplicación, declare el [`head` hook](https://nuxtjs.org/guides/features/meta-tags-seo) y use `$nuxtI18nHead` dentro para generar la metainformación i18n SEO:
```js {}[layouts/default.vue]
export default {
head () {
- return this.$nuxtI18nSeo()
+ return this.$nuxtI18nHead({ addSeoAttributes: true })
}
}
```
@@ -174,16 +174,16 @@ Si tiene más diseños, no olvide agregarlo allí también.
### Combinando metadatos de SEO i18n con los tuyos
-Si desea agregar su propio meta en el diseño, puede combinar fácilmente el objeto devuelto por `$nuxtI18nSeo` con el suyo:
+Si desea agregar su propio meta en el diseño, puede combinar fácilmente el objeto devuelto por `$nuxtI18nHead` con el suyo:
```js {}[layouts/default.vue]
export default {
head () {
- const i18nSeo = this.$nuxtI18nSeo()
+ const i18nHead = this.$nuxtI18nHead({ addSeoAttributes: true })
return {
htmlAttrs: {
myAttribute: 'My Value',
- ...i18nSeo.htmlAttrs
+ ...i18nHead.htmlAttrs
},
meta: [
{
@@ -191,7 +191,7 @@ export default {
name: 'description',
content: 'My Custom Description'
},
- ...i18nSeo.meta
+ ...i18nHead.meta
],
link: [
{
@@ -200,7 +200,7 @@ export default {
sizes: '180x180',
href: '/apple-touch-icon.png'
},
- ...i18nSeo.link
+ ...i18nHead.link
]
}
}
diff --git a/src/helpers/constants.js b/src/helpers/constants.js
index 2ebc80239..d5c371fe6 100644
--- a/src/helpers/constants.js
+++ b/src/helpers/constants.js
@@ -5,6 +5,7 @@ exports.MODULE_NAME = packageJson.name
exports.ROOT_DIR = 'nuxt-i18n'
exports.LOCALE_CODE_KEY = 'code'
exports.LOCALE_ISO_KEY = 'iso'
+exports.LOCALE_DIR_KEY = 'dir'
exports.LOCALE_DOMAIN_KEY = 'domain'
exports.LOCALE_FILE_KEY = 'file'
@@ -24,6 +25,7 @@ exports.DEFAULT_OPTIONS = {
vueI18nLoader: false,
locales: [],
defaultLocale: '',
+ defaultDirection: 'ltr',
routesNameSeparator: '___',
defaultLocaleRouteNameSuffix: 'default',
strategy: STRATEGIES.PREFIX_EXCEPT_DEFAULT,
diff --git a/src/index.js b/src/index.js
index 5944ce06f..01ab8cea8 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,7 +1,7 @@
import { resolve, join } from 'path'
import { readdirSync } from 'fs'
import { directive as i18nExtensionsDirective } from '@intlify/vue-i18n-extensions'
-import { MODULE_NAME, COMPONENT_OPTIONS_KEY, DEFAULT_OPTIONS, LOCALE_CODE_KEY, LOCALE_ISO_KEY, LOCALE_DOMAIN_KEY, LOCALE_FILE_KEY, NESTED_OPTIONS, ROOT_DIR, STRATEGIES } from './helpers/constants'
+import { MODULE_NAME, COMPONENT_OPTIONS_KEY, DEFAULT_OPTIONS, LOCALE_CODE_KEY, LOCALE_ISO_KEY, LOCALE_DIR_KEY, LOCALE_DOMAIN_KEY, LOCALE_FILE_KEY, NESTED_OPTIONS, ROOT_DIR, STRATEGIES } from './helpers/constants'
import { getLocaleCodes } from './helpers/utils'
import { buildHook, createExtendRoutesHook } from './core/hooks'
@@ -34,6 +34,7 @@ export default function (userOptions) {
MODULE_NAME,
LOCALE_CODE_KEY,
LOCALE_ISO_KEY,
+ LOCALE_DIR_KEY,
LOCALE_DOMAIN_KEY,
LOCALE_FILE_KEY,
STRATEGIES,
diff --git a/src/templates/head-meta.js b/src/templates/head-meta.js
new file mode 100644
index 000000000..93a07019d
--- /dev/null
+++ b/src/templates/head-meta.js
@@ -0,0 +1,165 @@
+import VueMeta from 'vue-meta'
+import {
+ defaultLocale,
+ defaultDirection,
+ COMPONENT_OPTIONS_KEY,
+ LOCALE_DIR_KEY,
+ LOCALE_ISO_KEY,
+ MODULE_NAME,
+ STRATEGIES,
+ strategy
+} from './options'
+
+export function nuxtI18nHead ({ addDirAttribute = true, addSeoAttributes = false } = {}) {
+ // Can happen when using from a global mixin.
+ if (!this.$i18n) {
+ return {}
+ }
+
+ const metaObject = {
+ htmlAttrs: {},
+ link: [],
+ meta: []
+ }
+
+ const currentLocale = this.$i18n.localeProperties
+ const currentLocaleIso = currentLocale[LOCALE_ISO_KEY]
+ const currentLocaleDir = currentLocale[LOCALE_DIR_KEY] || defaultDirection
+
+ /**
+ * Adding Direction Attribute:
+ */
+ if (addDirAttribute) {
+ metaObject.htmlAttrs.dir = currentLocaleDir
+ }
+
+ /**
+ * Adding SEO Meta:
+ */
+ if (
+ addSeoAttributes &&
+ (VueMeta.hasMetaInfo ? VueMeta.hasMetaInfo(this) : this._hasMetaInfo) &&
+ this.$i18n.locale &&
+ this.$i18n.locales &&
+ this.$options[COMPONENT_OPTIONS_KEY] !== false &&
+ !(this.$options[COMPONENT_OPTIONS_KEY] && this.$options[COMPONENT_OPTIONS_KEY].seo === false)
+ ) {
+ if (currentLocaleIso) {
+ metaObject.htmlAttrs.lang = currentLocaleIso // TODO: simple lang or "specific" lang with territory?
+ }
+
+ addHreflangLinks.bind(this)(this.$i18n.locales, this.$i18n.__baseUrl, metaObject.link)
+ addCanonicalLinks.bind(this)(this.$i18n.__baseUrl, metaObject.link)
+ addCurrentOgLocale.bind(this)(currentLocale, currentLocaleIso, metaObject.meta)
+ addAlternateOgLocales.bind(this)(this.$i18n.locales, currentLocaleIso, metaObject.meta)
+ }
+
+ /**
+ * Internals:
+ */
+
+ function addHreflangLinks (locales, baseUrl, link) {
+ if (strategy === STRATEGIES.NO_PREFIX) {
+ return
+ }
+ const localeMap = new Map()
+ for (const locale of locales) {
+ const localeIso = isoFromLocale(locale)
+
+ if (!localeIso) {
+ // eslint-disable-next-line no-console
+ console.warn(`[${MODULE_NAME}] Locale ISO code is required to generate alternate link`)
+ continue
+ }
+
+ const [language, region] = localeIso.split('-')
+
+ if (language && region && (locale.isCatchallLocale || !localeMap.has(language))) {
+ localeMap.set(language, locale)
+ }
+
+ localeMap.set(localeIso, locale)
+ }
+
+ for (const [iso, mapLocale] of localeMap.entries()) {
+ link.push({
+ hid: `i18n-alt-${iso}`,
+ rel: 'alternate',
+ href: baseUrl + this.switchLocalePath(mapLocale.code),
+ hreflang: iso
+ })
+ }
+
+ if (defaultLocale) {
+ link.push({
+ hid: 'i18n-xd',
+ rel: 'alternate',
+ href: baseUrl + this.switchLocalePath(defaultLocale),
+ hreflang: 'x-default'
+ })
+ }
+ }
+
+ function addCanonicalLinks (baseUrl, link) {
+ const currentRoute = this.localeRoute({
+ ...this.$route,
+ name: this.getRouteBaseName()
+ })
+ const canonicalPath = currentRoute ? currentRoute.path : null
+
+ if (!canonicalPath) {
+ return
+ }
+
+ link.push({
+ hid: 'i18n-can',
+ rel: 'canonical',
+ href: baseUrl + canonicalPath
+ })
+ }
+
+ function addCurrentOgLocale (currentLocale, currentLocaleIso, meta) {
+ const hasCurrentLocaleAndIso = currentLocale && currentLocaleIso
+
+ if (!hasCurrentLocaleAndIso) {
+ return
+ }
+
+ meta.push({
+ hid: 'i18n-og',
+ property: 'og:locale',
+ // Replace dash with underscore as defined in spec: language_TERRITORY
+ content: underscoreIsoFromLocale(currentLocale)
+ })
+ }
+
+ function addAlternateOgLocales (locales, currentLocaleIso, meta) {
+ const localesWithoutCurrent = locales.filter(locale => {
+ const localeIso = isoFromLocale(locale)
+ return localeIso && localeIso !== currentLocaleIso
+ })
+
+ const alternateLocales = localesWithoutCurrent.map(locale => ({
+ hid: `i18n-og-alt-${isoFromLocale(locale)}`,
+ property: 'og:locale:alternate',
+ content: underscoreIsoFromLocale(locale)
+ }))
+
+ meta.push(...alternateLocales)
+ }
+
+ function isoFromLocale (locale) {
+ return locale[LOCALE_ISO_KEY]
+ }
+
+ function underscoreIsoFromLocale (locale) {
+ return isoFromLocale(locale).replace(/-/g, '_')
+ }
+
+ return metaObject
+}
+
+/** @deprecated */
+export function nuxtI18nSeo () {
+ return nuxtI18nHead.call(this, { addDirAttribute: false, addSeoAttributes: true })
+}
diff --git a/src/templates/plugin.main.js b/src/templates/plugin.main.js
index ba8e34982..9e5879f1a 100644
--- a/src/templates/plugin.main.js
+++ b/src/templates/plugin.main.js
@@ -1,6 +1,6 @@
import Vue from 'vue'
import VueI18n from 'vue-i18n'
-import { nuxtI18nSeo } from './seo-head'
+import { nuxtI18nHead, nuxtI18nSeo } from './head-meta'
import {
baseUrl,
beforeLanguageSwitch,
@@ -276,8 +276,8 @@ export default async (context) => {
app.i18n.__baseUrl = resolveBaseUrl(baseUrl, context)
app.i18n.__onNavigate = onNavigate
- // Inject seo function
Vue.prototype.$nuxtI18nSeo = nuxtI18nSeo
+ Vue.prototype.$nuxtI18nHead = nuxtI18nHead
if (store) {
// Inject in store.
diff --git a/src/templates/plugin.seo.js b/src/templates/plugin.seo.js
index ed7159e35..9db015580 100644
--- a/src/templates/plugin.seo.js
+++ b/src/templates/plugin.seo.js
@@ -1,5 +1,5 @@
import Vue from 'vue'
-import { nuxtI18nSeo } from './seo-head'
+import { nuxtI18nSeo } from './head-meta'
const plugin = {
install (Vue) {
diff --git a/src/templates/seo-head.js b/src/templates/seo-head.js
deleted file mode 100644
index 82ba93e68..000000000
--- a/src/templates/seo-head.js
+++ /dev/null
@@ -1,147 +0,0 @@
-import VueMeta from 'vue-meta'
-import {
- defaultLocale,
- COMPONENT_OPTIONS_KEY,
- LOCALE_CODE_KEY,
- LOCALE_ISO_KEY,
- MODULE_NAME,
- STRATEGIES,
- strategy
-} from './options'
-
-export const nuxtI18nSeo = function () {
- if (
- !(VueMeta.hasMetaInfo ? VueMeta.hasMetaInfo(this) : this._hasMetaInfo) ||
- !this.$i18n ||
- !this.$i18n.locale ||
- !this.$i18n.locales ||
- this.$options[COMPONENT_OPTIONS_KEY] === false ||
- (this.$options[COMPONENT_OPTIONS_KEY] && this.$options[COMPONENT_OPTIONS_KEY].seo === false)
- ) {
- return {}
- }
-
- const metaObject = {
- htmlAttrs: {},
- link: [],
- meta: []
- }
-
- const currentLocale = this.$i18n.locales.find(l => codeFromLocale(l) === this.$i18n.locale)
- const currentLocaleIso = isoFromLocale(currentLocale)
-
- if (currentLocale && currentLocaleIso) {
- metaObject.htmlAttrs.lang = currentLocaleIso // TODO: simple lang or "specific" lang with territory?
- }
-
- addHreflangLinks.bind(this)(this.$i18n.locales, this.$i18n.__baseUrl, metaObject.link)
- addCanonicalLinks.bind(this)(this.$i18n.__baseUrl, metaObject.link)
- addCurrentOgLocale.bind(this)(currentLocale, currentLocaleIso, metaObject.meta)
- addAlternateOgLocales.bind(this)(this.$i18n.locales, currentLocaleIso, metaObject.meta)
-
- return metaObject
-}
-
-function addHreflangLinks (locales, baseUrl, link) {
- if (strategy === STRATEGIES.NO_PREFIX) {
- return
- }
-
- const localeMap = new Map()
-
- for (const locale of locales) {
- const localeIso = isoFromLocale(locale)
-
- if (!localeIso) {
- // eslint-disable-next-line no-console
- console.warn(`[${MODULE_NAME}] Locale ISO code is required to generate alternate link`)
- continue
- }
-
- const [language, region] = localeIso.split('-')
-
- if (language && region && (locale.isCatchallLocale || !localeMap.has(language))) {
- localeMap.set(language, locale)
- }
-
- localeMap.set(localeIso, locale)
- }
-
- for (const [iso, mapLocale] of localeMap.entries()) {
- link.push({
- hid: `i18n-alt-${iso}`,
- rel: 'alternate',
- href: baseUrl + this.switchLocalePath(mapLocale.code),
- hreflang: iso
- })
- }
-
- if (defaultLocale) {
- link.push({
- hid: 'i18n-xd',
- rel: 'alternate',
- href: baseUrl + this.switchLocalePath(defaultLocale),
- hreflang: 'x-default'
- })
- }
-}
-
-function addCanonicalLinks (baseUrl, link) {
- const currentRoute = this.localeRoute({
- ...this.$route,
- name: this.getRouteBaseName()
- })
- const canonicalPath = currentRoute ? currentRoute.path : null
-
- if (!canonicalPath) {
- return
- }
-
- link.push({
- hid: 'i18n-can',
- rel: 'canonical',
- href: baseUrl + canonicalPath
- })
-}
-
-function addCurrentOgLocale (currentLocale, currentLocaleIso, meta) {
- const hasCurrentLocaleAndIso = currentLocale && currentLocaleIso
-
- if (!hasCurrentLocaleAndIso) {
- return
- }
-
- meta.push({
- hid: 'i18n-og',
- property: 'og:locale',
- // Replace dash with underscore as defined in spec: language_TERRITORY
- content: underscoreIsoFromLocale(currentLocale)
- })
-}
-
-function addAlternateOgLocales (locales, currentLocaleIso, meta) {
- const localesWithoutCurrent = locales.filter(locale => {
- const localeIso = isoFromLocale(locale)
- return localeIso && localeIso !== currentLocaleIso
- })
-
- const alternateLocales = localesWithoutCurrent.map(locale => ({
- hid: `i18n-og-alt-${isoFromLocale(locale)}`,
- property: 'og:locale:alternate',
- content: underscoreIsoFromLocale(locale)
- }))
-
- meta.push(...alternateLocales)
-}
-
-function isoFromLocale (locale) {
- return locale[LOCALE_ISO_KEY]
-}
-
-function underscoreIsoFromLocale (locale) {
- return isoFromLocale(locale).replace(/-/g, '_')
-}
-
-function codeFromLocale (locale) {
- return locale[LOCALE_CODE_KEY]
-}
diff --git a/test/fixture/basic/pages/about.vue b/test/fixture/basic/pages/about.vue
index b7c8b4d80..7cd376310 100644
--- a/test/fixture/basic/pages/about.vue
+++ b/test/fixture/basic/pages/about.vue
@@ -13,6 +13,9 @@ export default {
components: {
LangSwitcher
},
+ head () {
+ return this.$nuxtI18nHead()
+ },
nuxtI18n: {
paths: {
en: '/about-us',
diff --git a/test/fixture/basic/pages/index.vue b/test/fixture/basic/pages/index.vue
index 52272c4d6..6cb90dc65 100644
--- a/test/fixture/basic/pages/index.vue
+++ b/test/fixture/basic/pages/index.vue
@@ -16,6 +16,7 @@ export default {
},
head () {
return {
+ ...this.$nuxtI18nHead({ addDirAttribute: false }),
title: this.$t('home')
}
}
diff --git a/test/fixture/basic/pages/locale.vue b/test/fixture/basic/pages/locale.vue
index 0d64dc1f2..24c32acda 100644
--- a/test/fixture/basic/pages/locale.vue
+++ b/test/fixture/basic/pages/locale.vue
@@ -3,5 +3,9 @@
diff --git a/test/module.test.js b/test/module.test.js
index 22750b1bc..42a738882 100644
--- a/test/module.test.js
+++ b/test/module.test.js
@@ -37,6 +37,19 @@ describe('locales as string array', () => {
const dom = getDom(html)
expect(dom.querySelector('#current-page')?.textContent).toBe('page: À propos')
})
+
+ test('dir attribute will not be added to the html element', async () => {
+ const html = await get('/about')
+ const dom = getDom(html)
+ expect(dom.documentElement.getAttribute('dir')).toBeNull()
+ })
+
+ test('nuxtI18nHead does not set SEO Meta', async () => {
+ const html = await get('/about')
+ const dom = getDom(html)
+ const seoTags = getSeoTags(dom)
+ expect(seoTags).toEqual([])
+ })
})
describe('differentDomains enabled', () => {
@@ -47,7 +60,8 @@ describe('differentDomains enabled', () => {
const override = {
i18n: {
differentDomains: true,
- seo: false
+ seo: false,
+ defaultDirection: 'auto'
}
}
@@ -59,7 +73,8 @@ describe('differentDomains enabled', () => {
code: 'en',
iso: 'en-US',
name: 'English',
- domain: 'en.nuxt-app.localhost'
+ domain: 'en.nuxt-app.localhost',
+ dir: 'ltr'
},
{
code: 'fr',
@@ -121,6 +136,17 @@ describe('differentDomains enabled', () => {
const dom = getDom(html)
expect(dom.querySelector('body')?.textContent).toContain('page: Accueil')
})
+
+ test('dir attribute exists and is set to the default direction', async () => {
+ const requestOptions = {
+ headers: {
+ 'X-Forwarded-Host': 'fr.nuxt-app.localhost'
+ }
+ }
+ const html = await get('/locale', requestOptions)
+ const dom = getDom(html)
+ expect(dom.documentElement.getAttribute('dir')).toEqual('auto')
+ })
})
const TRAILING_SLASHES = [undefined, false, true]
@@ -204,7 +230,6 @@ for (const trailingSlash of TRAILING_SLASHES) {
href: 'nuxt-app.localhost/'
}
]
-
expect(seoTags).toEqual(expectedSeoTags)
})
@@ -631,7 +656,9 @@ describe('hreflang', () => {
{
code: 'en',
iso: 'en',
- name: 'English'
+ name: 'English',
+ dir: 'auto'
+
},
{
code: 'fr',
@@ -654,8 +681,8 @@ describe('hreflang', () => {
nuxt = (await setup(testConfig)).nuxt
})
- test('sets SEO metadata properly', async () => {
- const html = await get('/')
+ test('sets SEO metadata and dir attribute properly', async () => {
+ const html = await get('/locale')
const dom = getDom(html)
const seoTags = getSeoTags(dom)
@@ -681,54 +708,54 @@ describe('hreflang', () => {
tagName: 'meta'
},
{
- href: 'nuxt-app.localhost/',
+ href: 'nuxt-app.localhost/locale',
hreflang: 'en',
rel: 'alternate',
tagName: 'link'
},
{
- href: 'nuxt-app.localhost/fr',
+ href: 'nuxt-app.localhost/fr/locale',
hreflang: 'fr',
rel: 'alternate',
tagName: 'link'
},
{
- href: 'nuxt-app.localhost/fr',
+ href: 'nuxt-app.localhost/fr/locale',
hreflang: 'fr-FR',
rel: 'alternate',
tagName: 'link'
},
{
- href: 'nuxt-app.localhost/esVe',
+ href: 'nuxt-app.localhost/esVe/locale',
hreflang: 'es',
rel: 'alternate',
tagName: 'link'
},
{
- href: 'nuxt-app.localhost/es',
+ href: 'nuxt-app.localhost/es/locale',
hreflang: 'es-ES',
rel: 'alternate',
tagName: 'link'
},
{
- href: 'nuxt-app.localhost/esVe',
+ href: 'nuxt-app.localhost/esVe/locale',
hreflang: 'es-VE',
rel: 'alternate',
tagName: 'link'
},
{
- href: 'nuxt-app.localhost/',
+ href: 'nuxt-app.localhost/locale',
hreflang: 'x-default',
rel: 'alternate',
tagName: 'link'
},
{
- href: 'nuxt-app.localhost/',
+ href: 'nuxt-app.localhost/locale',
rel: 'canonical',
tagName: 'link'
}
]
-
+ expect(dom.documentElement.getAttribute('dir')).toEqual('auto')
expect(seoTags).toEqual(expectedSeoTags)
})
@@ -745,6 +772,12 @@ describe('hreflang', () => {
})
})
+ test('dir attribute will not be added to the html element', async () => {
+ const html = await get('/fr')
+ const dom = getDom(html)
+ expect(dom.documentElement.getAttribute('dir')).toBeNull()
+ })
+
afterAll(async () => {
await nuxt.close()
})
diff --git a/types/nuxt-i18n.d.ts b/types/nuxt-i18n.d.ts
index 79baf7b3d..86d7e8da5 100644
--- a/types/nuxt-i18n.d.ts
+++ b/types/nuxt-i18n.d.ts
@@ -8,6 +8,7 @@ import { Context as NuxtContext } from '@nuxt/types'
declare namespace NuxtVueI18n {
type Locale = VueI18n.Locale
type Strategies = 'no_prefix' | 'prefix_except_default' | 'prefix' | 'prefix_and_default'
+ type Directions = 'ltr' | 'rtl' | 'auto'
namespace Options {
// e.g.:
@@ -20,6 +21,7 @@ declare namespace NuxtVueI18n {
code: Locale
// can be undefined: https://goo.gl/cCGKUV
iso?: string
+ dir?: Directions
// can be undefined: https://goo.gl/ryc5pF
file?: string
isCatchallLocale?: boolean
@@ -54,6 +56,7 @@ declare namespace NuxtVueI18n {
interface NuxtI18nInterface {
beforeLanguageSwitch?: (oldLocale: string, newLocale: string) => void
defaultLocale?: Locale
+ defaultDirection?: Directions
defaultLocaleRouteNameSuffix?: string
locales?: Array
differentDomains?: boolean
@@ -91,6 +94,19 @@ export interface NuxtI18nSeo {
meta?: MetaInfo['meta']
}
+export interface NuxtI18nHeadOptions {
+ /**
+ * Adds a `dir` attribute to the HTML element.
+ * Default: `true`
+ */
+ addDirAttribute: boolean
+ /**
+ * Adds various SEO attributes.
+ * Default: `false`
+ */
+ addSeoAttributes: boolean
+}
+
export interface NuxtI18nComponentOptions {
paths?: {
[key: string]: string | false
diff --git a/types/vue.d.ts b/types/vue.d.ts
index f091540d6..a1bc4f528 100644
--- a/types/vue.d.ts
+++ b/types/vue.d.ts
@@ -1,7 +1,8 @@
import Vue from 'vue'
import { Location, RawLocation, Route } from 'vue-router'
import VueI18n, { IVueI18n } from 'vue-i18n'
-import { NuxtI18nComponentOptions, NuxtVueI18n, NuxtI18nSeo } from './nuxt-i18n'
+import { MetaInfo } from 'vue-meta'
+import { NuxtI18nComponentOptions, NuxtVueI18n, NuxtI18nSeo, NuxtI18nHeadOptions } from './nuxt-i18n'
/**
* Extends types in vue-i18n
@@ -24,7 +25,9 @@ declare module 'vue-i18n' {
declare module 'vue/types/vue' {
interface Vue {
readonly $i18n: VueI18n & IVueI18n
+ /** @deprecated */
$nuxtI18nSeo(): NuxtI18nSeo
+ $nuxtI18nHead(options?: NuxtI18nHeadOptions): MetaInfo
getRouteBaseName(route?: Route): string
localePath(route: RawLocation, locale?: string): string
localeRoute(route: RawLocation, locale?: string): Location | undefined