Delivering a website in your native language knowing that Google or other tools will automatically translate gives a little peace of mind, but between contextual differences in languages and not always getting comprehensive translations, you might not know what you're actually telling your potential customers.
Next.js supports localization out of the box, allowing us to configure exactly what languages we want and using that context to pull in the translations we need.
We'll walk through setting up localization in our Next.js app and adding Spanish translations to our page copy in GraphCMS along with dynamically querying for each language in our pages.
Instructor: [0:00] We have our store up and running, and we have copy dispersed throughout the store. Where on our home page, we have this prepare for liftoff. Or if we even go to one of our products, we can see that we have a description for each one.
[0:12] The problem is, I know that all my customers don't speak English. How can I help them out by providing translations? The great thing is, GraphCMS provides localization right inside of the content editor.
[0:23] On top of that, Next.js provides internationalized routing, which allows us to easily support that. To get started with our localization, we're going to head into our GraphCMS dashboard, where we're going to head to the settings page.
[0:36] Where inside, we're going to find the locales page. We see that we have, by default, our English locale. If we open up this drop down, we can see that we have a lot of available options. Specifically, we're going to work on setting up Spanish.
[0:50] You can set up whatever language you want as part of this, but I'm going to work through Spanish. We can see that it automatically fills in the default API ID, where we're going to use ES, which is the code for that language.
[1:03] I'm going to go ahead and click add. Once that's saved, I'm going to head over to my schema's section. What we need to do is, we need to set up localization for every field that we want localized.
[1:15] Looking at our product, we can choose what fields we want to localize. If we look at this product for instance, maybe we don't want to localize the title because it's the title. We want that to be the actual title no matter what language you're looking at.
[1:29] We definitely want to localize the description. Inside of my product schema, we can skip the name. If I go to my description and click edit field, I can scroll to the bottom and we're going to see the option for localized field.
[1:41] I'm going to hit that check box and I'm going to click update. Looking at our other schema models, we probably don't need to do anything with category because each of them just simply have a title, no kind of description.
[1:52] We know on our home page we have text. Let's click page and we can see that we have our name, our hero title, our hero text. We probably want to localize the hero title and the hero text.
[2:04] I'm going to do the same exact thing and localize both my hero title and my hero text to make sure that both of them can provide language specific versions. Again, we want to make sure that we're localizing the fields wherever we want.
[2:19] For instance, again with categories, maybe you do want to localize the title. Maybe it makes sense to show these in that particular language.
[2:27] We also want to keep in mind that we can only add this localization for GraphCMS for things that are managed inside GraphCMS. For instance, products here, we don't have that managed in GraphCMS, that's being managed inside of the code right now.
[2:42] If we wanted to manage localization through GraphCMS, we'd likely want to create that in some other kind of way. Now that we have localization set up on some of our fields, let's head over to our content section, where we can start off with our page.
[2:56] If we go into our home page, we can start to see that it looks a little bit different. Where we now have EN next to our different values that are specifically localized. Meaning this is going to be the English version.
[3:08] If I want to set up a localized version of those fields, I can go down inside of my document information and I can see the available localizations. Right now, Spanish isn't activated.
[3:19] If I click the plus sign next to it, we can see that it's going to add that second option, where I can add the Spanish version of those values. In practice, I highly recommend you using a native speaker or somebody who can provide real translations for that particular language.
[3:36] For the sake of this walkthrough, I'm going to use Bing translate which my wife says, it's the better translator for at least Portuguese. I'm going to take my English version of prepare for liftoff, where we can see, we get the Spanish version.
[3:48] Inside our GraphCMS, I'm going to paste in that value. I did the exact same thing for apparel that's out of this world. I'm going to hit save and publish, and save and publish both the EN and the ES localizations of those fields.
[4:03] Products are the other model where we set up localization. If we look inside and we go to the description, we can see that same thing where we start to see that EN value.
[4:12] If we go to the sidebar and click Spanish for plus, we can see that we now have the ability to put those translations. I'm going to skip ahead and add all my translations to all my products.
[4:24] Now that I have all my translations in there, I'm going to head back over to my playground explorer. I'm going to scroll down and I'm going to look through my products. I'm going to start by looking through the fields.
[4:33] I'm going to open up my description just so that we can see what things look like when I show the localizations. If we open up this field called localizations, we're going to see that we have all the fields that we have on the product itself.
[4:46] We see this description. If I click that and open it and hit HTML, we're going to see if we click play, that I'm going to actually get both the description on that top level with my default description.
[4:59] Then inside of localizations, we're going to see that translated description. Of course, we want to know which locale this is. We can also select the locale field and click play. We can see that that particular locale is our Spanish.
[5:14] Now that we have our data and we can see how we can query that localization data, let's get started setting it up inside of Next.js so that we can use those translations. The great thing is, getting started in Next.js is really easy, where we need to define this i18n which is internationalization property, where we can see we have this configuration object.
[5:36] Where for our case, all we need to define is these locales and the default locale. I'm going to copy these values so we can paste them in easily. Where inside of my next.config.js file, which is in the root of the project, I'm going to go ahead and paste that in.
[5:50] I'm going to get rid of those comments since we don't need that. We do know that this default locale is going to be English. I'm going to get rid of the US part of it because we're going to be using the two-letter format. Then, we're going to have an additional format or localization rather of ES or Spanish. We're going to get rid of the third for now.
[6:11] Now, that we have that configured, Next.js knows that in the context of everything that those two languages are going to be available. We can start to see which locales are going to be available on each page when it's getting rendered.
[6:25] We have multiple ways of doing that. Where for instance, we can use the useRouter to grab the current locale, where we can also set the locale. We can also see inside of getStaticProps and getServerSideProps where that actual locale is set, as well as getStaticPaths to get all the locales available so that we can render new pages for each one.
[6:46] Let's start off inside of our index.js or home page since that's going to be one of the simpler ones. If we look at our getStaticProps function, this is going to pass us context. Where inside of that context, we're going to be able to access the locale.
[7:01] To test this out, let's console.log that out like we normally do to see what that will look like. Also, as a reminder, because we changed our next.config.js, we want to make sure that we restart our Dev server. Once the page reloads, we can see that it shows that the current locale is English or EN.
[7:18] Before we set up the data to grab that additional locale, we need a way to be able to change that locale. We can even test this out in the first place. We're going to take a quick field trip to header.js where we want to set up a link to be able to click and change that locale.
[7:34] If we look in the header visually, we already have that link set up, where this ES stands for Spanish. Downward that set up, the first thing we want to do is, programmatically be able to list out all of the different localizations that we have available.
[7:48] To do this, we're going to use the useRouter hook. Where, if you remember, we talked about that a little bit ago. Where once I have that router object, I have access to not only the active locale, but I also have all the locales that are going to be supported in that application.
[8:03] At the top of header.js, I'm going to import the destructured useRouter from next/router. Then at the top of my component, I'm going to create a new constant, where I destructure the locale and the locales with an S from my useRouter instance.
[8:21] To make it a little easier to understand, I'm going to rename locale to active locale just to make it super clear that that is the active locale. Then, let's console.log out those values, so we can see what's inside of both our active locale and/or locales.
[8:38] In our console, we can see both the active locale, which is English in the locales that we have available. Now, I want to take these locales and I only want to show the locales that aren't active so I can provide a link to them. I'm going to create a new constant called available locales.
[8:54] I'm going to set that equal to locales, where I'm going to filter that array. For each locale, I'm going to say that it should not equal my active locale. Then down inside of my unordered list, I'm going to take that available locales and map through.
[9:10] For each locale, I'm going to return one of these list items, where inside, I'm going to say that I want to replace this value with my locale, but I'm going to make it to upper case and remove that S. As usual on our outer list item, we want to make sure we add a key to make sure React is happy.
[9:32] When the page refreshes, it shouldn't look any different, which is a good thing, because now we have our ES or Spanish locale being added automatically. The cool thing is, because we're already using this link component, this has locale changing capabilities already built in.
[9:49] We can simply add a new prop of locale and set it equal to our locale, and it's going to use that to change the locale. When we do that, we want to make sure that we're still passing in the link that it should navigate to.
[10:03] Because we're inside of our header, we want to make sure we're using the active path or where that person currently is located. In addition to using locale and locales, we're going to use the as-path, which is exactly that and it's going to give our current path. In my useRouter, I'm going to destructure my as-half. I'm going to head back down to my list of links.
[10:25] I'm going to replace that href with my as-path. Back on our home page, if we click that ES link, we can see already that it flips to EN.
[10:36] We can also see inside of getStaticProps that we're logging out from the home page, that active locale, where we just changed it to ES or Espanol. Now we want to use that locale value to change how we're querying for our data.
[10:51] In order to build that query inside of our API playground, I'm going to first copy the existing query and head into my playground and paste that in. I'm going to click play and we're going to get that current data for our home page.
[11:04] Inside page, we have that same localizations feel that we saw on products, but we can see that the only two things that we're going to need is the hero text and the hero title. If I click play, we can see that that's going to give me an array.
[11:17] If I scroll all the way down past our data to get that, that I have those localizations for hero text and hero title. First of all, I do want to see what locale that is.
[11:27] I'm going to also include that locale, but I also only want to grab the locale for that specific locale that I'm querying for the active locale. To do that, I can provide a filter for my query. We can see under localizations, I have this locale's ability.
[11:43] I'm going to click check box next to that. We can see that it's already pre-filling with my locale. Where I have it set to ES, or if I click play, it's going to look the same for us right now. If I had multiple locales, it would only show me that specific one that I'm querying.
[11:59] We can see that right inside of localizations, where it's querying for that locale. Similar to what we were doing when we were creating dynamic routes, we also want to make a variable out of this. To do that, I'm going to go up to my locales.
[12:13] I'm going to click that dollar sign to turn it into a variable. I'm going to make a few tweaks here. I'm going to make it locale because I only want to make one. I'm going to remove the array around that again because I only want one.
[12:24] I'm going to remove the default because I don't want it to default to a non-default value. Then we can see that we're going to take this locale value and we're going to pass it in as locale without the S.
[12:35] As one last thing, because locales is expecting an array inside of the localizations, we do need to wrap that single locale with an array when we are going to pass in that dynamic value. To test that out, I can click the query variables tab.
[12:51] I'm going to create a JSON document, where we can start typing the quotes. We can see that we get the autocomplete of locale, where I can pass in that ES for my locale. I can click play.
[13:03] If I scroll all the way down the bottom, it should look exactly the same because we do only want to get that ES. Because we only have one locale different from the default English, that's all we can really query for at this point.
[13:16] Let's take this back to the application. I'm going to copy everything inside and I'm going to replace exactly what we copied. I'm going to remove that final bracket to make sure that we have proper syntax here.
[13:28] Because we have that locale variable, we need to set that variable. Underneath, as a new property, I'm going to say variables. I'm going to pass in my locale as is. This home is going to include that array of localizations.
[13:43] Instead of trying to wiggle that in somehow inside of React, let's just bubble up that data so that we can leave everything as is, but it'll replace everything intact with the localization data. To do that, I'm going to first change this home into a letVariable.
[14:00] Down below, I'm going to say if home.localizations has a length that's greater than zero, meaning it has localizations, I'm going to say that I want my home to be equal to. First of all, I'm going to spread out all the home data that exists in there.
[14:16] Then I'm going to additionally spread out home.localizations and grab that first instance inside there because that's going to be our localization for that page. When we look at our page, nothing should look different at this point.
[14:33] If we click that ES at the top right, we can see that it instantly changes to our Spanish. We can see in the URL, that's because it changed to the route for Spanish. Let's do the same thing for our product pages.
[14:47] If we go to one of our product pages, we can see that if we try to click ES there, it's going to change to /ES/products, but that doesn't currently exist. The first thing we need to do is create a new route for each and every one of those localizations.
[15:00] Inside our product slug, I'm going to head all the way to the bottom, where I'm going to find my getStaticPaths. This time, I'm going to destructure that context. Instead of locale, I'm going to look for the locales with an S because I want to grab all the locales.
[15:15] We're already creating all of our paths for just the default routes. What we can do is we can take advantage of those existing paths and just simply tack on our locale for those paths. To do that, we're going to be a little tricky here.
[15:30] I'm going to first open this up in the return statement to a new array, where I'm going to also spread out the existing paths, which are going to be our default routes.
[15:39] Then I'm going to spread out another instance of paths, but this time I'm going to use flatMap which is similar to map but it's going to flatten two levels deep of arrays. I'm going to say for each path, I want to then return a new map statement.
[15:57] This time, I'm going to map through the locales. Then for each locale that I map through, I want to then return the path and the locale. That was a lot. Just to quickly think about how that works.
[16:12] If we have arrays, where they're two level deep, and maybe we have our paths in here where our params product slug, but then we want to have a locale for each one. Where there's going to be the same thing, but we have our locale is equal to ES.
[16:27] What we're going to do is, we want to have a single top-level array of everything. What flatMap will do is, it's going to basically remove these other instances of array and flatten it out so that all of our path objects are in that top level array.
[16:43] Not only does that allow us to map through our paths, where we want to be able to create a new set of these for each path. We can create a new set of paths for each locale as well, since we're mapping through that as well.
[16:58] If we open back up the app, we should see that that ES route is now being fulfilled. Heading back up to getStaticProps, we're going to do the same thing we did in the home page where we want to use a variable of locale in order to get that locale details.
[17:13] To do this, let's make sure we do it right and use the playground again. I'm going to copy this query. We're going to paste it in and we want to make sure we set up a slug variable for this product.
[17:24] I'm going to create a new JSON object, where my slug is equal to Cosmo mug. I'm going to click play and we can see our data. We can start filling in that query again, where we're going to go to localizations.
[17:35] We're going to find those locales first and we're going to make that a variable. Remember, we want to update what that looks like, just to make it a little bit easier to use. I'm going to make it a single locale and get rid of the default.
[17:48] I'm also going to change my locales here because we know this expects an array. We want to make sure we make that an array and uses that locale singular value.
[17:57] Then I also want to grab the data for the fields that use it. Right now, we're only using the description. I'm going to grab my HTML as well as the actual locale so I have that information inside of that object.
[18:10] Then I'm going to tack on an additional query variable, just to make sure that I can test this out that it's working properly. Then when I click play, we're going to get a refresh of that data.
[18:21] If I scroll all the way down to the bottom, we're going to see those localizations with my Spanish description. I'm going to again, copy this query. Replace that existing query with my newly formatted query.
[18:32] As well as make sure to update my variables to include a locale, which I do need to destructure from my contacts inside of this getStaticProps. I'm additionally going to destructure that locale from getStaticProps.
[18:48] I'm going to do the same thing with my product data that I did with the home data on the home page. I'm going to first turn this into a let, then I'm going to say, if product.localizations.length is greater than zero, I'm going to say product is equal to spreading out products. Then I'm going to say I want to spread out my product.localizations.
[19:12] I'm going to grab the zero index of those localizations. Back inside of my application, on top of having the English version, when I click yes, it's going to change to my Spanish description.
[19:23] Finally, as one last step, any time we create our localizations like this, because we're using getStaticPaths to dynamically create some of our pages, we need to make sure that any route that is dynamically created, that we create those routes just like we did the product page.
[19:40] For example, because our home page is just a static route, it's able to automatically recognize the /ES. Similar to find a store, we can see that that route is going to work.
[19:51] If we try to go to one of our category pages, which is dynamically created, we're going to get a 404. We want to just make sure that all of our dynamically created routes, particularly categories if you're following along, have those dynamically created routes.
[20:04] I'm going to go ahead and grab the locales destructuring from getStaticPaths on the product page. I'm going to do the exact same thing inside of getStaticPaths for my category slug.
[20:15] I'm also going to go ahead and copy that same logic for the paths, because that's going to be exactly the same. Head over to my category slug and paste that right in inside of my paths. Hit save.
[20:28] Now my dynamic category routes are magically available. In review, not only did we want to provide a default English experience, we wanted to provide a localized version where I chose Spanish for my localization.
[20:41] To start, we needed to set up GraphCMS to support that locale. Additionally, inside of our schemas, we need to make sure that any product field or any field for our models that wanted to support localization had that enabled on that field.
[20:57] Once it was added to that schema model, we were able to enable that localization for each of the fields that we want, where we can add that additional version, where we were able to programmatically query for that locale data. As well as dynamically create locale paths in order to fully support localization inside of our app.