From 6a29addd3ff3223afc58a2dcf69e3a2a2665ec1e Mon Sep 17 00:00:00 2001 From: Bobbie Goede Date: Sun, 11 Aug 2024 15:55:03 +0200 Subject: [PATCH] fix: rename locale `iso` property to `language` (#3055) * fix: rename locale property `iso` to `language` * refactor: rename parameters * fix: locale `language` merging * docs: expand `iso` rename explanation --- .../5.v9/2.guide/18.breaking-changes-in-v9.md | 6 ++- .../2.guide/5.browser-language-detection.md | 2 +- docs/content/docs/5.v9/2.guide/6.seo.md | 26 ++++++------ docs/content/docs/5.v9/3.options/2.routing.md | 8 ++-- src/runtime/composables/index.ts | 6 +-- src/runtime/routing/compatibles/head.ts | 40 +++++++++---------- src/runtime/routing/utils.ts | 10 ++--- src/types.ts | 4 ++ src/utils.ts | 12 +++++- test/routing-utils.test.ts | 10 ++--- 10 files changed, 70 insertions(+), 54 deletions(-) diff --git a/docs/content/docs/5.v9/2.guide/18.breaking-changes-in-v9.md b/docs/content/docs/5.v9/2.guide/18.breaking-changes-in-v9.md index 25390b4b2..17d7ff1d5 100644 --- a/docs/content/docs/5.v9/2.guide/18.breaking-changes-in-v9.md +++ b/docs/content/docs/5.v9/2.guide/18.breaking-changes-in-v9.md @@ -46,4 +46,8 @@ nuxt.config.ts Reasons for change 1. Context - i18n files are used both server-side and client-side, using a dedicated `i18n/` folder in the root directory outside `app/` and `server/` makes more sense. - 2. Clean - less clutter/fragmentation of i18n files, and should make resolving and loading files easier for us. \ No newline at end of file + 2. Clean - less clutter/fragmentation of i18n files, and should make resolving and loading files easier for us. + +## Locale `iso` renamed to `language` + +The `iso` property on a locale object has been renamed to `language` to be consistent with the usage of Language Tags on the web (e.g. `navigator.language` and `Accept-Language`). The original `iso` property name referred to ISO standards which describe valid Language Tags, see the [related issue](https://github.com/nuxt-modules/i18n/issues/2449) for more details. \ No newline at end of file diff --git a/docs/content/docs/5.v9/2.guide/5.browser-language-detection.md b/docs/content/docs/5.v9/2.guide/5.browser-language-detection.md index 4efe6002a..036d5e5c8 100644 --- a/docs/content/docs/5.v9/2.guide/5.browser-language-detection.md +++ b/docs/content/docs/5.v9/2.guide/5.browser-language-detection.md @@ -21,7 +21,7 @@ export default defineNuxtConfig({ For better SEO, it's recommended to set `redirectOn` to `root` (which is the default value). When set, the language detection is only attempted when the user visits the root path (`/`) of the site. This allows crawlers to access the requested page rather than being redirected away based on detected locale. It also allows linking to pages in specific locales. :: -Browser language is detected either from `navigator` when running on client-side, or from the `accept-language` HTTP header. Configured `locales` (or locales `iso` and/or `code` when locales are specified in object form) are matched against locales reported by the browser (for example `en-US,en;q=0.9,no;q=0.8`). If there is no exact match for the full locale, the language code (letters before `-`) are matched against configured locales. +Browser language is detected either from `navigator` when running on client-side, or from the `accept-language` HTTP header. Configured `locales` (or locales `language` and/or `code` when locales are specified in object form) are matched against locales reported by the browser (for example `en-US,en;q=0.9,no;q=0.8`). If there is no exact match for the full locale, the language code (letters before `-`) are matched against configured locales. To prevent redirecting users every time they visit the app, **Nuxt i18n module** sets a cookie using the detected locale. You can change the cookie's name by setting `detectBrowserLanguage.cookieKey` option to whatever you'd like, the default is _i18n_redirected_. diff --git a/docs/content/docs/5.v9/2.guide/6.seo.md b/docs/content/docs/5.v9/2.guide/6.seo.md index f11039cd2..b61543961 100644 --- a/docs/content/docs/5.v9/2.guide/6.seo.md +++ b/docs/content/docs/5.v9/2.guide/6.seo.md @@ -16,7 +16,7 @@ Here are the specific optimizations and features that it enables: ## Requirements -To leverage the SEO benefits, you must configure the `locales` option as an array of objects, where each object has an `iso` option set to the locale language tags: +To leverage the SEO benefits, you must configure the `locales` option as an array of objects, where each object has an `language` option set to the locale language tags: ```ts [nuxt.config.ts] export default defineNuxtConfig({ @@ -24,15 +24,15 @@ export default defineNuxtConfig({ locales: [ { code: 'en', - iso: 'en-US' + language: 'en-US' }, { code: 'es', - iso: 'es-ES' + language: 'es-ES' }, { code: 'fr', - iso: 'fr-FR' + language: 'fr-FR' } ] } @@ -165,11 +165,11 @@ useHead({ - `lang` attribute for the `` tag - Sets the correct `lang` attribute, equivalent to the current locale's `iso` value, in the `` tag. + Sets the correct `lang` attribute, equivalent to the current locale's `language` value, in the `` tag. - `hreflang` alternate link - Generates `` tags for every configured locale. The locales' `iso` value are used as `hreflang` values. + Generates `` tags for every configured locale. The locales' `language` value are used as `hreflang` values. A "catchall" locale hreflang link is provided for each locale group (e.g. `en-*`). By default, it is the first locale provided, but another locale can be selected by setting `isCatchallLocale` to `true` on that specific locale object in your **Nuxt i18n module** configuration. [More on hreflang](https://support.google.com/webmasters/answer/189077) @@ -181,11 +181,11 @@ useHead({ locales: [ { code: 'en', - iso: 'en-US' // Will be used as "catchall" locale by default + language: 'en-US' // Will be used as "catchall" locale by default }, { code: 'gb', - iso: 'en-GB' + language: 'en-GB' } ] } @@ -200,11 +200,11 @@ useHead({ locales: [ { code: 'en', - iso: 'en-US' + language: 'en-US' }, { code: 'gb', - iso: 'en-GB', + language: 'en-GB', isCatchallLocale: true // This one will be used as catchall locale } ] @@ -212,7 +212,7 @@ useHead({ }) ``` - In case you already have an `en` locale `iso` set, it'll be used as the "catchall" without doing anything + In case you already have an `en` locale `language` set, it'll be used as the "catchall" without doing anything ```ts [nuxt.config.ts] export default defineNuxtConfig({ @@ -220,11 +220,11 @@ useHead({ locales: [ { code: 'gb', - iso: 'en-GB' + language: 'en-GB' }, { code: 'en', - iso: 'en' // will be used as "catchall" locale + language: 'en' // will be used as "catchall" locale } ] } diff --git a/docs/content/docs/5.v9/3.options/2.routing.md b/docs/content/docs/5.v9/3.options/2.routing.md index 418c79cc4..a2f1089ea 100644 --- a/docs/content/docs/5.v9/3.options/2.routing.md +++ b/docs/content/docs/5.v9/3.options/2.routing.md @@ -27,16 +27,16 @@ List of locales supported by your app. Can either be an array of codes (`['en', ```json [ - { "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" } + { "code": "en", "language": "en-US", "file": "en.js", "dir": "ltr" }, + { "code": "ar", "language": "ar-EG", "file": "ar.js", "dir": "rtl" }, + { "code": "fr", "language": "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) - A language-range used for SEO features and for matching browser locales when using [`detectBrowserLanguage`](/docs/options/browser#detectbrowserlanguage) functionality. Should use the [language tag syntax](https://www.w3.org/International/articles/language-tags/) as defined by the IETF's [BCP47](https://www.rfc-editor.org/info/bcp47), for example: +- `language` (required when using SEO features) - A language-range used for SEO features and for matching browser locales when using [`detectBrowserLanguage`](/docs/options/browser#detectbrowserlanguage) functionality. Should use the [language tag syntax](https://www.w3.org/International/articles/language-tags/) as defined by the IETF's [BCP47](https://www.rfc-editor.org/info/bcp47), for example: - `'en'` (`language` subtag for English) - `'fr-CA'` (`language+region` subtags for French as used in Canada) - `'zh-Hans'` (`language+script` subtags for Chinese written with Simplified script) diff --git a/src/runtime/composables/index.ts b/src/runtime/composables/index.ts index c0ac8c9d2..ee6c63bdc 100644 --- a/src/runtime/composables/index.ts +++ b/src/runtime/composables/index.ts @@ -73,7 +73,7 @@ export function useSetI18nParams(seoAttributes?: SeoAttributesOptions): SetI18nP }) const currentLocale = getNormalizedLocales(locales).find(l => l.code === locale) || { code: locale } - const currentLocaleIso = currentLocale.iso + const currentLocaleLanguage = currentLocale.language const setMeta = () => { const metaObject: HeadParam = { @@ -94,8 +94,8 @@ export function useSetI18nParams(seoAttributes?: SeoAttributesOptions): SetI18nP metaObject.meta.push( ...getOgUrl(common, idAttribute, seoAttributes), - ...getCurrentOgLocale(currentLocale, currentLocaleIso, idAttribute), - ...getAlternateOgLocales(locales, currentLocaleIso, idAttribute) + ...getCurrentOgLocale(currentLocale, currentLocaleLanguage, idAttribute), + ...getAlternateOgLocales(locales, currentLocaleLanguage, idAttribute) ) } diff --git a/src/runtime/routing/compatibles/head.ts b/src/runtime/routing/compatibles/head.ts index d4d2645b8..7b9faad7b 100644 --- a/src/runtime/routing/compatibles/head.ts +++ b/src/runtime/routing/compatibles/head.ts @@ -45,7 +45,7 @@ export function localeHead( const currentLocale = getNormalizedLocales(locales).find(l => l.code === locale) || { code: locale } - const currentIso = currentLocale.iso + const currentLanguage = currentLocale.language const currentDir = currentLocale.dir || defaultDirection // Adding Direction Attribute @@ -55,8 +55,8 @@ export function localeHead( // Adding SEO Meta if (seoAttributes && locale && unref(i18n.locales)) { - if (currentIso) { - metaObject.htmlAttrs.lang = currentIso + if (currentLanguage) { + metaObject.htmlAttrs.lang = currentLanguage } metaObject.link.push( @@ -66,8 +66,8 @@ export function localeHead( metaObject.meta.push( ...getOgUrl(common, idAttribute, seoAttributes), - ...getCurrentOgLocale(currentLocale, currentIso, idAttribute), - ...getAlternateOgLocales(unref(locales) as LocaleObject[], currentIso, idAttribute) + ...getCurrentOgLocale(currentLocale, currentLanguage, idAttribute), + ...getAlternateOgLocales(unref(locales) as LocaleObject[], currentLanguage, idAttribute) ) } @@ -93,29 +93,29 @@ export function getHreflangLinks( const localeMap = new Map() for (const locale of locales) { - const localeIso = locale.iso + const localeLanguage = locale.language - if (!localeIso) { - console.warn('Locale ISO code is required to generate alternate link') + if (!localeLanguage) { + console.warn('Locale `language` ISO code is required to generate alternate link') continue } - const [language, region] = localeIso.split('-') + const [language, region] = localeLanguage.split('-') if (language && region && (locale.isCatchallLocale || !localeMap.has(language))) { localeMap.set(language, locale) } - localeMap.set(localeIso, locale) + localeMap.set(localeLanguage, locale) } - for (const [iso, mapLocale] of localeMap.entries()) { + for (const [language, mapLocale] of localeMap.entries()) { const localePath = switchLocalePath(common, mapLocale.code) if (localePath) { links.push({ - [idAttribute]: `i18n-alt-${iso}`, + [idAttribute]: `i18n-alt-${language}`, rel: 'alternate', href: toAbsoluteUrl(localePath, baseUrl), - hreflang: iso + hreflang: language }) } } @@ -199,26 +199,26 @@ export function getOgUrl( export function getCurrentOgLocale( currentLocale: LocaleObject, - currentIso: string | undefined, + currentLanguage: string | undefined, idAttribute: NonNullable ) { - if (!currentLocale || !currentIso) return [] + if (!currentLocale || !currentLanguage) return [] // Replace dash with underscore as defined in spec: language_TERRITORY - return [{ [idAttribute]: 'i18n-og', property: 'og:locale', content: hypenToUnderscore(currentIso) }] + return [{ [idAttribute]: 'i18n-og', property: 'og:locale', content: hypenToUnderscore(currentLanguage) }] } export function getAlternateOgLocales( locales: LocaleObject[], - currentIso: string | undefined, + currentLanguage: string | undefined, idAttribute: NonNullable ) { - const alternateLocales = locales.filter(locale => locale.iso && locale.iso !== currentIso) + const alternateLocales = locales.filter(locale => locale.language && locale.language !== currentLanguage) return alternateLocales.map(locale => ({ - [idAttribute]: `i18n-og-alt-${locale.iso}`, + [idAttribute]: `i18n-og-alt-${locale.language}`, property: 'og:locale:alternate', - content: hypenToUnderscore(locale.iso!) + content: hypenToUnderscore(locale.language!) })) } diff --git a/src/runtime/routing/utils.ts b/src/runtime/routing/utils.ts index 95c6f556b..d10165022 100644 --- a/src/runtime/routing/utils.ts +++ b/src/runtime/routing/utils.ts @@ -91,7 +91,7 @@ export interface BrowserLocale { * @remarks * This type is used by {@link BrowserLocaleMatcher} first argument */ -export type TargetLocale = Required> +export type TargetLocale = Required> /** * The browser locale matcher @@ -119,7 +119,7 @@ function matchBrowserLocale(locales: TargetLocale[], browserLocales: string[]): // first pass: match exact locale. for (const [index, browserCode] of browserLocales.entries()) { - const matchedLocale = locales.find(l => l.iso.toLowerCase() === browserCode.toLowerCase()) + const matchedLocale = locales.find(l => l.language.toLowerCase() === browserCode.toLowerCase()) if (matchedLocale) { matchedLocales.push({ code: matchedLocale.code, score: 1 - index / browserLocales.length }) break @@ -129,7 +129,7 @@ function matchBrowserLocale(locales: TargetLocale[], browserLocales: string[]): // second pass: match only locale code part of the browser locale (not including country). for (const [index, browserCode] of browserLocales.entries()) { const languageCode = browserCode.split('-')[0].toLowerCase() - const matchedLocale = locales.find(l => l.iso.split('-')[0].toLowerCase() === languageCode) + const matchedLocale = locales.find(l => l.language.split('-')[0].toLowerCase() === languageCode) if (matchedLocale) { // deduct a thousandth for being non-exact match. matchedLocales.push({ code: matchedLocale.code, score: 0.999 - index / browserLocales.length }) @@ -175,8 +175,8 @@ export function findBrowserLocale( const normalizedLocales = [] for (const l of locales) { const { code } = l - const iso = l.iso || code - normalizedLocales.push({ code, iso }) + const language = l.language || code + normalizedLocales.push({ code, language }) } // finding! diff --git a/src/types.ts b/src/types.ts index c6d0d6673..05b56028b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -255,7 +255,11 @@ export interface LocaleObject extends Record { file?: string | LocaleFile files?: string[] | LocaleFile[] isCatchallLocale?: boolean + /** + * @deprecated in v9, use `language` instead + */ iso?: string + language?: string } /** diff --git a/src/utils.ts b/src/utils.ts index 81b0b8ad8..a4200f42b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -45,7 +45,7 @@ export function getNormalizedLocales(locales: NuxtI18nOptions['locales']): Local const normalized: LocaleObject[] = [] for (const locale of locales) { if (isString(locale)) { - normalized.push({ code: locale, iso: locale }) + normalized.push({ code: locale, language: locale }) } else { normalized.push(locale) } @@ -508,13 +508,21 @@ export const mergeConfigLocales = (configs: LocaleConfig[], baseLocales: LocaleO // set normalized locale or to existing entry if (typeof locale === 'string') { - mergedLocales.set(code, merged ?? { iso: code, code }) + mergedLocales.set(code, merged ?? { language: code, code }) continue } const resolvedFiles = resolveRelativeLocales(locale, config) delete locale.file + if (locale.iso) { + console.warn( + `Locale ${locale.iso} uses deprecated \`iso\` property, this will be replaced with \`language\` in v9` + ) + locale.language = locale.iso + delete locale.iso + } + // merge locale and files with existing entry if (merged != null) { merged.files ??= [] as LocaleFile[] diff --git a/test/routing-utils.test.ts b/test/routing-utils.test.ts index 0ee14620e..38ba30f96 100644 --- a/test/routing-utils.test.ts +++ b/test/routing-utils.test.ts @@ -171,8 +171,8 @@ describe('findBrowserLocale', () => { test('matches ISO locale code', () => { const locales = [ - { code: 'cn', iso: 'zh-CN' }, - { code: 'en', iso: 'en-US' } + { code: 'cn', language: 'zh-CN' }, + { code: 'en', language: 'en-US' } ] const browserLocales = ['zh', 'zh-CN'] @@ -181,8 +181,8 @@ describe('findBrowserLocale', () => { test('matches full ISO code', () => { const locales = [ - { code: 'us', iso: 'en-US' }, - { code: 'gb', iso: 'en-GB' } + { code: 'us', language: 'en-US' }, + { code: 'gb', language: 'en-GB' } ] const browserLocales = ['en-GB', 'en'] @@ -203,7 +203,7 @@ describe('findBrowserLocale', () => { const matchedLocales = [] as utils.BrowserLocale[] for (const [index, browserCode] of browserLocales.entries()) { const languageCode = browserCode.split('-')[0].toLowerCase() - const matchedLocale = locales.find(l => l.iso.split('-')[0].toLowerCase() === languageCode) + const matchedLocale = locales.find(l => l.language.split('-')[0].toLowerCase() === languageCode) if (matchedLocale) { matchedLocales.push({ code: matchedLocale.code, score: 1 * index }) break