How To Build A Multilingual Website With Nuxt.js<\/h1>\nTim Benniks<\/address>\n 2024-08-01T15:00:00+00:00
\n 2024-10-15T23:05:45+00:00
\n <\/header>\n
This article is sponsored by Hygraph<\/b><\/p>\n
Internationalization, often abbreviated as i18n, is the process of designing and developing software applications in a way that they can be easily adapted to various spoken languages like English, German, French, and more without requiring substantial changes to the codebase. It involves moving away from hardcoded strings and techniques<\/strong> for translating text, formatting dates and numbers, and handling different character encodings, among other tasks.<\/p>\nInternationalization can give users the choice<\/strong> to access a given website or application in their native language, which can have a positive impression on them, making it crucial for reaching a global audience.<\/p>\nWhat We\u2019re Making<\/h2>\n
In this tutorial, we\u2019re making a website that puts these i18n pieces together using a combination of libraries and a UI framework. You\u2019ll want to have intermediate proficiency with JavaScript, Vue, and Nuxt to follow along. Throughout this article, we will learn by examples<\/strong> and incrementally build a multilingual Nuxt website<\/strong>. Together, we will learn how to provide i18n support for different languages, lazy-load locale messages, and switch locale on runtime.<\/p>\nAfter that, we will explore features like interpolation, pluralization, and date\/time translations.<\/p>\n
And finally, we will fetch dynamic localized content from an API server using Hygraph as our API server to get localized content. If you do not have a Hygraph account please create one for free<\/a> before jumping in.<\/p>\nAs a final detail, we will use Vuetify<\/a> as our UI framework, but please feel free to use another framework if you want. The final code for what we\u2019re building is published in a GitHub repository<\/a> for reference. And finally, you can also take a look at the final result in a live demo<\/a>.<\/p>\nThe nuxt-i18n<\/code> Library<\/h2>\nnuxt-i18n<\/code><\/a> is a library for implementing internationalization in Nuxt.js applications, and it\u2019s what we will be using in this tutorial. The library is built on top of Vue I18n<\/a>, which, again, is the de facto standard library for implementing i18n in Vue applications.<\/p>\nWhat makes nuxt-i18n<\/code> ideal for our work is that it provides the comprehensive set of features included in Vue I18n while adding more functionalities that are specific to Nuxt, like lazy loading locale messages, route generation and redirection for different locales, SEO metadata per locale, locale-specific domains, and more.<\/p>\nInitial Setup<\/h2>\n
Start a new Nuxt.js project and set it up with a UI framework of your choice. Again, I will be using Vue to establish the interface for this tutorial.<\/p>\n
Let us add a basic layout for our website and set up some sample Vue templates.<\/p>\n
First, a \u201cBlog\u201d page:<\/p>\n
<!-- pages\/blog.vue -->\n<template>\n <div>\n <v-card color=\"cardBackground\">\n <v-card-title class=\"text-overline\">\n Home\n <\/v-card-title>\n <v-card-text>\n This is the home page description\n <\/v-card-text>\n <\/v-card>\n <\/div>\n<\/template>\n<\/code><\/pre>\nNext, an \u201cAbout\u201d page:<\/p>\n
<!-- pages\/about.vue -->\n<template>\n <div>\n <v-card color=\"cardBackground\">\n <v-card-title class=\"text-overline\">\n About\n <\/v-card-title>\n <v-card-text>\n This is the about page description\n <\/v-card-text>\n <\/v-card>\n <\/div>\n<\/template>\n<\/code><\/pre>\nThis gives us a bit of a boilerplate that we can integrate our i18n work into.<\/p>\n
Translating Plain Text<\/h2>\n
The page templates look good, but notice how the text is hardcoded. As far as i18n goes, hardcoded content is difficult to translate into different locales. That is where the nuxt-i18n<\/code> library comes in, providing the language-specific strings we need for the Vue components in the templates.<\/p>\nWe\u2019ll start by installing the library via the command line:<\/p>\n
npx nuxi@latest module add i18n\n<\/code><\/pre>\nInside the nuxt.config.ts<\/code> file, we need to ensure that we have @nuxtjs\/i18n<\/code> inside the modules<\/code> array. We can use the i18n<\/code> property to provide module-specific configurations.<\/p>\n\/\/ nuxt.config.ts\nexport default defineNuxtConfig({\n \/\/ ...\n modules: [\n ...\n \"@nuxtjs\/i18n\",\n \/\/ ...\n ],\n i18n: {\n \/\/ nuxt-i18n module configurations here\n }\n \/\/ ...\n});\n<\/code><\/pre>\nSince the nuxt-i18n<\/code> library is built on top of the Vue I18n library, we can utilize its features in our Nuxt application as well. Let us create a new file, i18n.config.ts<\/code>, which we will use to provide all vue-i18n<\/code> configurations.<\/p>\n\/\/ i18n.config.ts\nexport default defineI18nConfig(() => ({\n legacy: false,\n locale: \"en\",\n messages: {\n en: {\n homePage: {\n title: \"Home\",\n description: \"This is the home page description.\"\n },\n aboutPage: {\n title: \"About\",\n description: \"This is the about page description.\"\n },\n },\n },\n}));\n<\/code><\/pre>\nHere, we have specified internationalization configurations, like using the en<\/code> locale, and added messages for the en<\/code> locale. These messages can be used inside the markup in the templates we made with the help of a $t<\/code> function from Vue I18n.<\/p>\nNext, we need to link the i18n.config.ts<\/code> configurations in our Nuxt config file.<\/p>\n\/\/ nuxt.config.ts\nexport default defineNuxtConfig({\n ...\n i18n: {\n vueI18n: \".\/i18n.config.ts\"\n }\n ...\n});\n<\/code><\/pre>\nNow, we can use the $t<\/code> function in our components — as shown below — to parse strings from our internationalization configurations.<\/p>\nNote<\/strong>: There\u2019s no need to import $t<\/code> since we have Nuxt\u2019s default auto-import functionality.<\/em><\/p>\n<!-- i18n.config.ts -->\n<template>\n <div>\n <v-card color=\"cardBackground\">\n <v-card-title class=\"text-overline\">\n {{ $t(\"homePage.title\") }}\n <\/v-card-title>\n <v-card-text>\n {{ $t(\"homePage.description\") }}\n <\/v-card-text>\n <\/v-card>\n <\/div>\n<\/template>\n<\/code><\/pre>\n<\/p>\n <\/p>\n
<\/p>\n
<\/a>\n (Large preview<\/a>)
\n <\/figcaption><\/figure>\nLazy Loading Translations<\/h2>\n
We have the title and description served from the configurations. Next, we can add more languages to the same config. For example, here\u2019s how we can establish translations for English (en<\/code>), French (fr<\/code>) and Spanish (es<\/code>):<\/p>\n\/\/ i18n.config.ts\nexport default defineI18nConfig(() => ({\n legacy: false,\n locale: \"en\",\n messages: {\n en: {\n \/\/ English\n },\n fr: {\n \/\/ French\n },\n es: {\n \/\/ Spanish\n }\n },\n}));\n<\/code><\/pre>\nFor a production website with a lot of content that needs translating, it would be unwise to bundle all of the messages from different locales in the main bundle. Instead, we should use the nuxt-i18<\/code> lazy loading feature asynchronously load only the required language rather than all of them at once. Also, having messages for all locales in a single configuration file can become difficult to manage over time, and breaking them up like this makes things easier to find.<\/p>\nLet\u2019s set up the lazy loading feature in nuxt.config.ts<\/code>:<\/p>\n\/\/ etc.\n i18n: {\n vueI18n: \".\/i18n.config.ts\",\n lazy: true,\n langDir: \"locales\",\n locales: [\n {\n code: \"en\",\n file: \"en.json\",\n name: \"English\",\n },\n {\n code: \"es\",\n file: \"es.json\",\n name: \"Spanish\",\n },\n {\n code: \"fr\",\n file: \"fr.json\",\n name: \"French\",\n },\n ],\n defaultLocale: \"en\",\n strategy: \"no_prefix\",\n },\n\n\/\/ etc.\n<\/code><\/pre>\nThis enables lazy loading and specifies the locales<\/code> directory that will contain our locale files. The locales<\/code> array configuration specifies from which files Nuxt.js should pick up messages for a specific language.<\/p>\nNow, we can create individual files for each language. I\u2019ll drop all three of them right here:<\/p>\n
\n\/\/ locales\/en.json\n{\n \"homePage\": {\n \"title\": \"Home\",\n \"description\": \"This is the home page description.\"\n },\n \"aboutPage\": {\n \"title\": \"About\",\n \"description\": \"This is the about page description.\"\n },\n \"selectLocale\": {\n \"label\": \"Select Locale\"\n },\n \"navbar\": {\n \"homeButton\": \"Home\",\n \"aboutButton\": \"About\"\n }\n}\n<\/code><\/pre>\n\n\/\/ locales\/fr.json\n{\n \"homePage\": {\n \"title\": \"Bienvenue sur la page d'accueil\",\n \"description\": \"Ceci est la description de la page d'accueil.\"\n },\n \"aboutPage\": {\n \"title\": \"\u00c0 propos de nous\",\n \"description\": \"Ceci est la description de la page \u00e0 propos de nous.\"\n },\n \"selectLocale\": {\n \"label\": \"S\u00e9lectionner la langue\"\n },\n \"navbar\": {\n \"homeButton\": \"Accueil\",\n \"aboutButton\": \"\u00c0 propos\"\n }\n}\n<\/code><\/pre>\n<\/div>\n\n\/\/ locales\/es.json\n{\n \"homePage\": {\n \"title\": \"Bienvenido a la p\u00e1gina de inicio\",\n \"description\": \"Esta es la descripci\u00f3n de la p\u00e1gina de inicio.\"\n },\n \"aboutPage\": {\n \"title\": \"Sobre nosotros\",\n \"description\": \"Esta es la descripci\u00f3n de la p\u00e1gina sobre nosotros.\"\n },\n \"selectLocale\": {\n \"label\": \"Seleccione el idioma\"\n },\n \"navbar\": {\n \"homeButton\": \"Inicio\",\n \"aboutButton\": \"Acerca de\"\n }\n}\n<\/code><\/pre>\n<\/div>\nWe have set up lazy loading, added multiple languages to our application, and moved our locale messages to separate files. The user gets the right locale for the right message, and the locale messages are kept in a maintainable manner inside the code base.<\/p>\n
Switching Between Languages<\/h2>\n
We have different locales, but to see them in action, we will build a component that can be used to switch between the available locales.<\/p>\n
<!-- components\/select-locale.vue -->\n<script setup>\nconst { locale, locales, setLocale } = useI18n();\n\nconst language = computed({\n get: () => locale.value,\n set: (value) => setLocale(value),\n});\n<\/script>\n\n<template>\n <v-select\n :label=\"$t('selectLocale.label')\"\n variant=\"outlined\"\n color=\"primary\"\n density=\"compact\" \n :items=\"locales\"\n item-title=\"name\"\n item-value=\"code\"\n v-model=\"language\"\n ><\/v-select>\n<\/template>\n<\/code><\/pre>\nThis component uses the useI18n<\/code> hook provided by the Vue I18n library and a computed property language<\/code> to get and set the global locale from a <select><\/code> input. To make this even more like a real-world website, we\u2019ll include a small navigation bar that links up all of the website\u2019s pages.<\/p>\n<!-- components\/select-locale.vue -->\n<template>\n <v-app-bar app :elevation=\"2\" class=\"px-2\">\n <div>\n <v-btn color=\"button\" to=\"\/\">\n {{ $t(\"navbar.homeButton\") }}\n <\/v-btn>\n <v-btn color=\"button\" to=\"\/about\">\n {{ $t(\"navbar.aboutButton\") }}\n <\/v-btn>\n <\/div>\n <v-spacer \/>\n <div class=\"mr-4 mt-6\">\n <SelectLocale \/>\n <\/div>\n <\/v-app-bar>\n<\/template>\n<\/code><\/pre>\nThat\u2019s it! Now, we can switch between languages on the fly.<\/p>\n<\/p>\n <\/p>\n
<\/p>\n
<\/a>\n Showing the current state of the homepage. (Large preview<\/a>)
\n <\/figcaption><\/figure>\n<\/p>\n <\/p>\n
<\/p>\n
<\/a>\n Showing a French translation. (Large preview<\/a>)
\n <\/figcaption><\/figure>\nWe have a basic layout, but I thought we\u2019d take this a step further and build a playground page we can use to explore more i18n features that are pretty useful when building a multilingual website.<\/p>\n
Interpolation and Pluralization<\/h2>\n
Interpolation and pluralization are internationalization techniques for handling dynamic content and grammatical variations across different languages. Interpolation<\/strong> allows developers to insert dynamic variables or expressions into translated strings. Pluralization<\/strong> addresses the complexities of plural forms in languages by selecting the appropriate grammatical form based on numeric values. With the help of interpolation and pluralization, we can create more natural and accurate translations.<\/p>\nTo use pluralization in our Nuxt app, we\u2019ll first add a configuration to the English locale file.<\/p>\n
\/\/ locales\/en.json\n{\n \/\/ etc.\n \"playgroundPage\": {\n \"pluralization\": {\n \"title\": \"Pluralization\",\n \"apple\": \"No Apple | One Apple | {count} Apples\",\n \"addApple\": \"Add\"\n }\n }\n \/\/ etc.\n}\n<\/code><\/pre>\nThe pluralization configuration set up for the key apple<\/code> defines an output — No Apple<\/code> — if a count of 0 is passed to it, a second output — One Apple<\/code> — if a count of 1 is passed, and a third — 2 Apples<\/code>, 3 Apples<\/code>, and so on — if the count passed in is greater than 1.<\/p>\nHere is how we can use it in your component: Whenever you click on the add button, you will see pluralization in action, changing the strings.<\/p>\n
\n<!-- pages\/playground.vue -->\n<script setup>\nlet appleCount = ref(0);\nconst addApple = () => {\n appleCount.value += 1;\n};\n<\/script>\n<template>\n <v-container fluid>\n <!-- PLURALIZATION EXAMPLE -->\n <v-card color=\"cardBackground\">\n <v-card-title class=\"text-overline\">\n {{ $t(\"playgroundPage.pluralization.title\") }}\n <\/v-card-title>\n\n <v-card-text>\n {{ $t(\"playgroundPage.pluralization.apple\", { count: appleCount }) }}\n <\/v-card-text>\n <v-card-actions>\n <v-btn\n @click=\"addApple\"\n color=\"primary\"\n variant=\"outlined\"\n density=\"comfortable\"\n >{{ $t(\"playgroundPage.pluralization.addApple\") }}<\/v-btn\n >\n <\/v-card-actions>\n <\/v-card>\n <\/v-container>\n<\/template>\n<\/code><\/pre>\n<\/div>\nTo use interpolation in our Nuxt app, first, add a configuration in the English locale file:<\/p>\n
\n\/\/ locales\/en.json\n{\n ...\n \"playgroundPage\": {\n ... \n \"interpolation\": {\n \"title\": \"Interpolation\",\n \"sayHello\": \"Hello, {name}\",\n \"hobby\": \"My favourite hobby is {0}.\",\n \"email\": \"You can reach out to me at {account}{'@'}{domain}.com\"\n },\n \/\/ etc. \n }\n \/\/ etc.\n}\n<\/code><\/pre>\n<\/div>\nThe message for sayHello<\/code> expects an object passed to it having a key name<\/code> when invoked — a process known as named interpolation<\/a>.<\/p>\nThe message hobby<\/code> expects an array to be passed to it and will pick up the 0th<\/sup> element, which is known as list interpolation<\/a>.<\/p>\nThe message email<\/code> expects an object with keys account<\/code>, and domain<\/code> and joins both with a literal string "@"<\/code>. This is known as literal interpolation<\/a>.<\/p>\nBelow is an example of how to use it in the Vue components:<\/p>\n
\n<!-- pages\/playground.vue -->\n<template>\n <v-container fluid>\n <!-- INTERPOLATION EXAMPLE -->\n <v-card color=\"cardBackground\">\n <v-card-title class=\"text-overline\">\n {{ $t(\"playgroundPage.interpolation.title\") }}\n <\/v-card-title>\n <v-card-text>\n <p>\n {{\n $t(\"playgroundPage.interpolation.sayHello\", {\n name: \"Jane\",\n })\n }}\n <\/p>\n <p>\n {{\n $t(\"playgroundPage.interpolation.hobby\", [\"Football\", \"Cricket\"])\n }}\n <\/p>\n <p>\n {{\n $t(\"playgroundPage.interpolation.email\", {\n account: \"johndoe\",\n domain: \"hygraph\",\n })\n }}\n <\/p>\n <\/v-card-text>\n <\/v-card>\n <\/v-container>\n<\/template>\n<\/code><\/pre>\n<\/div>\n<\/p>\n <\/p>\n
<\/p>\n
<\/a>\n (Large preview<\/a>)
\n <\/figcaption><\/figure>\nDate & Time Translations<\/h2>\n
Translating dates and times involves translating date and time formats according to the conventions of different locales. We can use Vue I18n\u2019s features for formatting date strings, handling time zones, and translating day and month names for managing date time translations. We can give the configuration for the same using the datetimeFormats<\/code> key inside the vue-i18n<\/code> config object.<\/p>\n\/\/ i18n.config.ts\nexport default defineI18nConfig(() => ({\n fallbackLocale: \"en\",\n datetimeFormats: {\n en: {\n short: {\n year: \"numeric\",\n month: \"short\",\n day: \"numeric\",\n },\n long: {\n year: \"numeric\",\n month: \"short\",\n day: \"numeric\",\n weekday: \"short\",\n hour: \"numeric\",\n minute: \"numeric\",\n hour12: false,\n },\n },\n fr: {\n short: {\n year: \"numeric\",\n month: \"short\",\n day: \"numeric\",\n },\n long: {\n year: \"numeric\",\n month: \"short\",\n day: \"numeric\",\n weekday: \"long\",\n hour: \"numeric\",\n minute: \"numeric\",\n hour12: true,\n },\n },\n es: {\n short: {\n year: \"numeric\",\n month: \"short\",\n day: \"numeric\",\n },\n long: {\n year: \"2-digit\",\n month: \"short\",\n day: \"numeric\",\n weekday: \"long\",\n hour: \"numeric\",\n minute: \"numeric\",\n hour12: true,\n },\n },\n },\n}));\n<\/code><\/pre>\nHere, we have set up short<\/code> and long<\/code> formats for all three languages. If you are coding along, you will be able to see available configurations for fields, like month and year, thanks to TypeScript and Intellisense features provided by your code editor. To display the translated dates and times in components, we should use the $d<\/code> function and pass the format to it.<\/p>\n\n<!-- pages.playground.vue -->\n<template>\n <v-container fluid>\n <!-- DATE TIME TRANSLATIONS EXAMPLE -->\n <v-card color=\"cardBackground\">\n <v-card-title class=\"text-overline\">\n {{ $t(\"playgroundPage.dateTime.title\") }}\n <\/v-card-title>\n <v-card-text>\n <p>Short: {{ (new Date(), $d(new Date(), \"short\")) }}<\/p>\n <p>Long: {{ (new Date(), $d(new Date(), \"long\")) }}<\/p>\n <\/v-card-text>\n <\/v-card>\n <\/v-container>\n<\/template>\n<\/code><\/pre>\n<\/div>\n<\/p>\n <\/p>\n
<\/p>\n
<\/a>\n Showing a default date and time. (Large preview<\/a>)
\n <\/figcaption><\/figure>\n<\/p>\n <\/p>\n
<\/p>\n
<\/a>\n Showing the date and time translated in Spanish. (Large preview<\/a>)
\n <\/figcaption><\/figure>\nLocalization On the Hygraph Side<\/h2>\n
We saw how to implement localization with static content. Now, we\u2019ll attempt to understand how to fetch dynamic localized content in Nuxt.<\/p>\n
We can build a blog page in our Nuxt App that fetches data from a server. The server API should accept a locale and return data in that specific locale.<\/p>\n
Hygraph has a flexible localization API that allows you to publish and query localized content. If you haven\u2019t created a free Hygraph account yet, you can do that on the Hygraph website<\/a> to continue following along.<\/p>\nGo to Project Settings<\/strong> → Locales<\/strong> and add locales for the API.<\/p>\n<\/p>\n <\/p>\n
<\/p>\n
<\/a>\n (Large preview<\/a>)
\n <\/figcaption><\/figure>\nWe have added two locales: English and French. Now we need aq localized_post<\/code> model in our schema that only two fields: title<\/code> and body.<\/code> Ensure to make these \u201cLocalized\u201d fields while creating them.<\/p>\n<\/p>\n <\/p>\n
<\/p>\n
<\/a>\n (Large preview<\/a>)
\n <\/figcaption><\/figure>\nAdd permissions to consume the localized content, go to Project settings<\/strong> → Access<\/strong> → API Access<\/strong> → Public Content API<\/strong>, and assign Read<\/strong> permissions to the localized_post<\/code> model.<\/p>\n<\/p>\n <\/p>\n
<\/p>\n
<\/a>\n (Large preview<\/a>)
\n <\/figcaption><\/figure>\n
\n 2024-10-15T23:05:45+00:00
\n <\/header>\n
Internationalization can give users the choice<\/strong> to access a given website or application in their native language, which can have a positive impression on them, making it crucial for reaching a global audience.<\/p>\n In this tutorial, we\u2019re making a website that puts these i18n pieces together using a combination of libraries and a UI framework. You\u2019ll want to have intermediate proficiency with JavaScript, Vue, and Nuxt to follow along. Throughout this article, we will learn by examples<\/strong> and incrementally build a multilingual Nuxt website<\/strong>. Together, we will learn how to provide i18n support for different languages, lazy-load locale messages, and switch locale on runtime.<\/p>\n After that, we will explore features like interpolation, pluralization, and date\/time translations.<\/p>\n And finally, we will fetch dynamic localized content from an API server using Hygraph as our API server to get localized content. If you do not have a Hygraph account please create one for free<\/a> before jumping in.<\/p>\n As a final detail, we will use Vuetify<\/a> as our UI framework, but please feel free to use another framework if you want. The final code for what we\u2019re building is published in a GitHub repository<\/a> for reference. And finally, you can also take a look at the final result in a live demo<\/a>.<\/p>\n What makes Start a new Nuxt.js project and set it up with a UI framework of your choice. Again, I will be using Vue to establish the interface for this tutorial.<\/p>\n Let us add a basic layout for our website and set up some sample Vue templates.<\/p>\n First, a \u201cBlog\u201d page:<\/p>\n Next, an \u201cAbout\u201d page:<\/p>\n This gives us a bit of a boilerplate that we can integrate our i18n work into.<\/p>\n The page templates look good, but notice how the text is hardcoded. As far as i18n goes, hardcoded content is difficult to translate into different locales. That is where the We\u2019ll start by installing the library via the command line:<\/p>\n Inside the Since the Here, we have specified internationalization configurations, like using the Next, we need to link the Now, we can use the Note<\/strong>: There\u2019s no need to import <\/p>\n <\/a> We have the title and description served from the configurations. Next, we can add more languages to the same config. For example, here\u2019s how we can establish translations for English ( For a production website with a lot of content that needs translating, it would be unwise to bundle all of the messages from different locales in the main bundle. Instead, we should use the Let\u2019s set up the lazy loading feature in This enables lazy loading and specifies the Now, we can create individual files for each language. I\u2019ll drop all three of them right here:<\/p>\n We have set up lazy loading, added multiple languages to our application, and moved our locale messages to separate files. The user gets the right locale for the right message, and the locale messages are kept in a maintainable manner inside the code base.<\/p>\n We have different locales, but to see them in action, we will build a component that can be used to switch between the available locales.<\/p>\n This component uses the That\u2019s it! Now, we can switch between languages on the fly.<\/p>\n <\/p>\n <\/a> <\/p>\n <\/a> We have a basic layout, but I thought we\u2019d take this a step further and build a playground page we can use to explore more i18n features that are pretty useful when building a multilingual website.<\/p>\n Interpolation and pluralization are internationalization techniques for handling dynamic content and grammatical variations across different languages. Interpolation<\/strong> allows developers to insert dynamic variables or expressions into translated strings. Pluralization<\/strong> addresses the complexities of plural forms in languages by selecting the appropriate grammatical form based on numeric values. With the help of interpolation and pluralization, we can create more natural and accurate translations.<\/p>\n To use pluralization in our Nuxt app, we\u2019ll first add a configuration to the English locale file.<\/p>\n The pluralization configuration set up for the key Here is how we can use it in your component: Whenever you click on the add button, you will see pluralization in action, changing the strings.<\/p>\n To use interpolation in our Nuxt app, first, add a configuration in the English locale file:<\/p>\n The message for The message The message Below is an example of how to use it in the Vue components:<\/p>\n <\/p>\n <\/a> Translating dates and times involves translating date and time formats according to the conventions of different locales. We can use Vue I18n\u2019s features for formatting date strings, handling time zones, and translating day and month names for managing date time translations. We can give the configuration for the same using the Here, we have set up <\/p>\n <\/a> <\/p>\n <\/a> We saw how to implement localization with static content. Now, we\u2019ll attempt to understand how to fetch dynamic localized content in Nuxt.<\/p>\n We can build a blog page in our Nuxt App that fetches data from a server. The server API should accept a locale and return data in that specific locale.<\/p>\n Hygraph has a flexible localization API that allows you to publish and query localized content. If you haven\u2019t created a free Hygraph account yet, you can do that on the Hygraph website<\/a> to continue following along.<\/p>\n Go to Project Settings<\/strong> → Locales<\/strong> and add locales for the API.<\/p>\n <\/p>\n <\/a> We have added two locales: English and French. Now we need aq <\/p>\n <\/a> Add permissions to consume the localized content, go to Project settings<\/strong> → Access<\/strong> → API Access<\/strong> → Public Content API<\/strong>, and assign Read<\/strong> permissions to the <\/p>\n <\/a>What We\u2019re Making<\/h2>\n
The
nuxt-i18n<\/code> Library<\/h2>\n
nuxt-i18n<\/code><\/a> is a library for implementing internationalization in Nuxt.js applications, and it\u2019s what we will be using in this tutorial. The library is built on top of Vue I18n<\/a>, which, again, is the de facto standard library for implementing i18n in Vue applications.<\/p>\n
nuxt-i18n<\/code> ideal for our work is that it provides the comprehensive set of features included in Vue I18n while adding more functionalities that are specific to Nuxt, like lazy loading locale messages, route generation and redirection for different locales, SEO metadata per locale, locale-specific domains, and more.<\/p>\n
Initial Setup<\/h2>\n
<!-- pages\/blog.vue -->\n<template>\n <div>\n <v-card color=\"cardBackground\">\n <v-card-title class=\"text-overline\">\n Home\n <\/v-card-title>\n <v-card-text>\n This is the home page description\n <\/v-card-text>\n <\/v-card>\n <\/div>\n<\/template>\n<\/code><\/pre>\n
<!-- pages\/about.vue -->\n<template>\n <div>\n <v-card color=\"cardBackground\">\n <v-card-title class=\"text-overline\">\n About\n <\/v-card-title>\n <v-card-text>\n This is the about page description\n <\/v-card-text>\n <\/v-card>\n <\/div>\n<\/template>\n<\/code><\/pre>\n
Translating Plain Text<\/h2>\n
nuxt-i18n<\/code> library comes in, providing the language-specific strings we need for the Vue components in the templates.<\/p>\n
npx nuxi@latest module add i18n\n<\/code><\/pre>\n
nuxt.config.ts<\/code> file, we need to ensure that we have
@nuxtjs\/i18n<\/code> inside the
modules<\/code> array. We can use the
i18n<\/code> property to provide module-specific configurations.<\/p>\n
\/\/ nuxt.config.ts\nexport default defineNuxtConfig({\n \/\/ ...\n modules: [\n ...\n \"@nuxtjs\/i18n\",\n \/\/ ...\n ],\n i18n: {\n \/\/ nuxt-i18n module configurations here\n }\n \/\/ ...\n});\n<\/code><\/pre>\n
nuxt-i18n<\/code> library is built on top of the Vue I18n library, we can utilize its features in our Nuxt application as well. Let us create a new file,
i18n.config.ts<\/code>, which we will use to provide all
vue-i18n<\/code> configurations.<\/p>\n
\/\/ i18n.config.ts\nexport default defineI18nConfig(() => ({\n legacy: false,\n locale: \"en\",\n messages: {\n en: {\n homePage: {\n title: \"Home\",\n description: \"This is the home page description.\"\n },\n aboutPage: {\n title: \"About\",\n description: \"This is the about page description.\"\n },\n },\n },\n}));\n<\/code><\/pre>\n
en<\/code> locale, and added messages for the
en<\/code> locale. These messages can be used inside the markup in the templates we made with the help of a
$t<\/code> function from Vue I18n.<\/p>\n
i18n.config.ts<\/code> configurations in our Nuxt config file.<\/p>\n
\/\/ nuxt.config.ts\nexport default defineNuxtConfig({\n ...\n i18n: {\n vueI18n: \".\/i18n.config.ts\"\n }\n ...\n});\n<\/code><\/pre>\n
$t<\/code> function in our components — as shown below — to parse strings from our internationalization configurations.<\/p>\n
$t<\/code> since we have Nuxt\u2019s default auto-import functionality.<\/em><\/p>\n
<!-- i18n.config.ts -->\n<template>\n <div>\n <v-card color=\"cardBackground\">\n <v-card-title class=\"text-overline\">\n {{ $t(\"homePage.title\") }}\n <\/v-card-title>\n <v-card-text>\n {{ $t(\"homePage.description\") }}\n <\/v-card-text>\n <\/v-card>\n <\/div>\n<\/template>\n<\/code><\/pre>\n
<\/p>\n
\n <\/figcaption><\/figure>\nLazy Loading Translations<\/h2>\n
en<\/code>), French (
fr<\/code>) and Spanish (
es<\/code>):<\/p>\n
\/\/ i18n.config.ts\nexport default defineI18nConfig(() => ({\n legacy: false,\n locale: \"en\",\n messages: {\n en: {\n \/\/ English\n },\n fr: {\n \/\/ French\n },\n es: {\n \/\/ Spanish\n }\n },\n}));\n<\/code><\/pre>\n
nuxt-i18<\/code> lazy loading feature asynchronously load only the required language rather than all of them at once. Also, having messages for all locales in a single configuration file can become difficult to manage over time, and breaking them up like this makes things easier to find.<\/p>\n
nuxt.config.ts<\/code>:<\/p>\n
\/\/ etc.\n i18n: {\n vueI18n: \".\/i18n.config.ts\",\n lazy: true,\n langDir: \"locales\",\n locales: [\n {\n code: \"en\",\n file: \"en.json\",\n name: \"English\",\n },\n {\n code: \"es\",\n file: \"es.json\",\n name: \"Spanish\",\n },\n {\n code: \"fr\",\n file: \"fr.json\",\n name: \"French\",\n },\n ],\n defaultLocale: \"en\",\n strategy: \"no_prefix\",\n },\n\n\/\/ etc.\n<\/code><\/pre>\n
locales<\/code> directory that will contain our locale files. The
locales<\/code> array configuration specifies from which files Nuxt.js should pick up messages for a specific language.<\/p>\n
\n\/\/ locales\/en.json\n{\n \"homePage\": {\n \"title\": \"Home\",\n \"description\": \"This is the home page description.\"\n },\n \"aboutPage\": {\n \"title\": \"About\",\n \"description\": \"This is the about page description.\"\n },\n \"selectLocale\": {\n \"label\": \"Select Locale\"\n },\n \"navbar\": {\n \"homeButton\": \"Home\",\n \"aboutButton\": \"About\"\n }\n}\n<\/code><\/pre>\n
\/\/ locales\/fr.json\n{\n \"homePage\": {\n \"title\": \"Bienvenue sur la page d'accueil\",\n \"description\": \"Ceci est la description de la page d'accueil.\"\n },\n \"aboutPage\": {\n \"title\": \"\u00c0 propos de nous\",\n \"description\": \"Ceci est la description de la page \u00e0 propos de nous.\"\n },\n \"selectLocale\": {\n \"label\": \"S\u00e9lectionner la langue\"\n },\n \"navbar\": {\n \"homeButton\": \"Accueil\",\n \"aboutButton\": \"\u00c0 propos\"\n }\n}\n<\/code><\/pre>\n<\/div>\n
\/\/ locales\/es.json\n{\n \"homePage\": {\n \"title\": \"Bienvenido a la p\u00e1gina de inicio\",\n \"description\": \"Esta es la descripci\u00f3n de la p\u00e1gina de inicio.\"\n },\n \"aboutPage\": {\n \"title\": \"Sobre nosotros\",\n \"description\": \"Esta es la descripci\u00f3n de la p\u00e1gina sobre nosotros.\"\n },\n \"selectLocale\": {\n \"label\": \"Seleccione el idioma\"\n },\n \"navbar\": {\n \"homeButton\": \"Inicio\",\n \"aboutButton\": \"Acerca de\"\n }\n}\n<\/code><\/pre>\n<\/div>\n
Switching Between Languages<\/h2>\n
<!-- components\/select-locale.vue -->\n<script setup>\nconst { locale, locales, setLocale } = useI18n();\n\nconst language = computed({\n get: () => locale.value,\n set: (value) => setLocale(value),\n});\n<\/script>\n\n<template>\n <v-select\n :label=\"$t('selectLocale.label')\"\n variant=\"outlined\"\n color=\"primary\"\n density=\"compact\" \n :items=\"locales\"\n item-title=\"name\"\n item-value=\"code\"\n v-model=\"language\"\n ><\/v-select>\n<\/template>\n<\/code><\/pre>\n
useI18n<\/code> hook provided by the Vue I18n library and a computed property
language<\/code> to get and set the global locale from a
<select><\/code> input. To make this even more like a real-world website, we\u2019ll include a small navigation bar that links up all of the website\u2019s pages.<\/p>\n
<!-- components\/select-locale.vue -->\n<template>\n <v-app-bar app :elevation=\"2\" class=\"px-2\">\n <div>\n <v-btn color=\"button\" to=\"\/\">\n {{ $t(\"navbar.homeButton\") }}\n <\/v-btn>\n <v-btn color=\"button\" to=\"\/about\">\n {{ $t(\"navbar.aboutButton\") }}\n <\/v-btn>\n <\/div>\n <v-spacer \/>\n <div class=\"mr-4 mt-6\">\n <SelectLocale \/>\n <\/div>\n <\/v-app-bar>\n<\/template>\n<\/code><\/pre>\n
<\/p>\n
\n <\/figcaption><\/figure>\n<\/p>\n
\n <\/figcaption><\/figure>\nInterpolation and Pluralization<\/h2>\n
\/\/ locales\/en.json\n{\n \/\/ etc.\n \"playgroundPage\": {\n \"pluralization\": {\n \"title\": \"Pluralization\",\n \"apple\": \"No Apple | One Apple | {count} Apples\",\n \"addApple\": \"Add\"\n }\n }\n \/\/ etc.\n}\n<\/code><\/pre>\n
apple<\/code> defines an output —
No Apple<\/code> — if a count of 0 is passed to it, a second output —
One Apple<\/code> — if a count of 1 is passed, and a third —
2 Apples<\/code>,
3 Apples<\/code>, and so on — if the count passed in is greater than 1.<\/p>\n
<!-- pages\/playground.vue -->\n<script setup>\nlet appleCount = ref(0);\nconst addApple = () => {\n appleCount.value += 1;\n};\n<\/script>\n<template>\n <v-container fluid>\n <!-- PLURALIZATION EXAMPLE -->\n <v-card color=\"cardBackground\">\n <v-card-title class=\"text-overline\">\n {{ $t(\"playgroundPage.pluralization.title\") }}\n <\/v-card-title>\n\n <v-card-text>\n {{ $t(\"playgroundPage.pluralization.apple\", { count: appleCount }) }}\n <\/v-card-text>\n <v-card-actions>\n <v-btn\n @click=\"addApple\"\n color=\"primary\"\n variant=\"outlined\"\n density=\"comfortable\"\n >{{ $t(\"playgroundPage.pluralization.addApple\") }}<\/v-btn\n >\n <\/v-card-actions>\n <\/v-card>\n <\/v-container>\n<\/template>\n<\/code><\/pre>\n<\/div>\n
\/\/ locales\/en.json\n{\n ...\n \"playgroundPage\": {\n ... \n \"interpolation\": {\n \"title\": \"Interpolation\",\n \"sayHello\": \"Hello, {name}\",\n \"hobby\": \"My favourite hobby is {0}.\",\n \"email\": \"You can reach out to me at {account}{'@'}{domain}.com\"\n },\n \/\/ etc. \n }\n \/\/ etc.\n}\n<\/code><\/pre>\n<\/div>\n
sayHello<\/code> expects an object passed to it having a key
name<\/code> when invoked — a process known as named interpolation<\/a>.<\/p>\n
hobby<\/code> expects an array to be passed to it and will pick up the 0th<\/sup> element, which is known as list interpolation<\/a>.<\/p>\n
email<\/code> expects an object with keys
account<\/code>, and
domain<\/code> and joins both with a literal string
"@"<\/code>. This is known as literal interpolation<\/a>.<\/p>\n
<!-- pages\/playground.vue -->\n<template>\n <v-container fluid>\n <!-- INTERPOLATION EXAMPLE -->\n <v-card color=\"cardBackground\">\n <v-card-title class=\"text-overline\">\n {{ $t(\"playgroundPage.interpolation.title\") }}\n <\/v-card-title>\n <v-card-text>\n <p>\n {{\n $t(\"playgroundPage.interpolation.sayHello\", {\n name: \"Jane\",\n })\n }}\n <\/p>\n <p>\n {{\n $t(\"playgroundPage.interpolation.hobby\", [\"Football\", \"Cricket\"])\n }}\n <\/p>\n <p>\n {{\n $t(\"playgroundPage.interpolation.email\", {\n account: \"johndoe\",\n domain: \"hygraph\",\n })\n }}\n <\/p>\n <\/v-card-text>\n <\/v-card>\n <\/v-container>\n<\/template>\n<\/code><\/pre>\n<\/div>\n
<\/p>\n
\n <\/figcaption><\/figure>\nDate & Time Translations<\/h2>\n
datetimeFormats<\/code> key inside the
vue-i18n<\/code> config object.<\/p>\n
\/\/ i18n.config.ts\nexport default defineI18nConfig(() => ({\n fallbackLocale: \"en\",\n datetimeFormats: {\n en: {\n short: {\n year: \"numeric\",\n month: \"short\",\n day: \"numeric\",\n },\n long: {\n year: \"numeric\",\n month: \"short\",\n day: \"numeric\",\n weekday: \"short\",\n hour: \"numeric\",\n minute: \"numeric\",\n hour12: false,\n },\n },\n fr: {\n short: {\n year: \"numeric\",\n month: \"short\",\n day: \"numeric\",\n },\n long: {\n year: \"numeric\",\n month: \"short\",\n day: \"numeric\",\n weekday: \"long\",\n hour: \"numeric\",\n minute: \"numeric\",\n hour12: true,\n },\n },\n es: {\n short: {\n year: \"numeric\",\n month: \"short\",\n day: \"numeric\",\n },\n long: {\n year: \"2-digit\",\n month: \"short\",\n day: \"numeric\",\n weekday: \"long\",\n hour: \"numeric\",\n minute: \"numeric\",\n hour12: true,\n },\n },\n },\n}));\n<\/code><\/pre>\n
short<\/code> and
long<\/code> formats for all three languages. If you are coding along, you will be able to see available configurations for fields, like month and year, thanks to TypeScript and Intellisense features provided by your code editor. To display the translated dates and times in components, we should use the
$d<\/code> function and pass the format to it.<\/p>\n
<!-- pages.playground.vue -->\n<template>\n <v-container fluid>\n <!-- DATE TIME TRANSLATIONS EXAMPLE -->\n <v-card color=\"cardBackground\">\n <v-card-title class=\"text-overline\">\n {{ $t(\"playgroundPage.dateTime.title\") }}\n <\/v-card-title>\n <v-card-text>\n <p>Short: {{ (new Date(), $d(new Date(), \"short\")) }}<\/p>\n <p>Long: {{ (new Date(), $d(new Date(), \"long\")) }}<\/p>\n <\/v-card-text>\n <\/v-card>\n <\/v-container>\n<\/template>\n<\/code><\/pre>\n<\/div>\n
<\/p>\n
\n <\/figcaption><\/figure>\n<\/p>\n
\n <\/figcaption><\/figure>\nLocalization On the Hygraph Side<\/h2>\n
<\/p>\n
\n <\/figcaption><\/figure>\nlocalized_post<\/code> model in our schema that only two fields:
title<\/code> and
body.<\/code> Ensure to make these \u201cLocalized\u201d fields while creating them.<\/p>\n
<\/p>\n
\n <\/figcaption><\/figure>\nlocalized_post<\/code> model.<\/p>\n
<\/p>\n
\n <\/figcaption><\/figure>\n