
Managing Content on Small Websites: Guide for 2025 & Beyond
22 Nov 2022
So you're running a news channel, where you update your users in real time.
You want to take it a step further and add translation to your news article, but you don't want to manually write that translation out. How do you achieve that, writing in one language and the article being translated into multiple languages based on the user's preference?
This is where AI comes in with its translation services.
AI is the perfect solution for multilingual websites because it offers real-time, accurate translations without the need for human translators for every language. It can handle various languages simultaneously, adapt to context, and maintain the original meaning of the content while being cost-effective.
AI has advanced significantly in recent years, enabling content to be translated with the cultural nuances unique to each language, which is crucial for resonating with diverse audiences, for a news website, this is exactly what you need, you want your readers to understand your tone and have the right nuance when it is translated to their native language.
AI enables content to be translated with the cultural nuances unique to each language, which is crucial for resonating with diverse audiences.
So how do you take advantage of AI? To do this, you first need to have a Content Management System where you upload your news articles, and then your application frontend where you pull the news article from the CMS.
It is no news that Headless CMS is slowly taking over from traditional CMS, so choosing a headless CMS will be natural, and using a headless CMS is the beautiful part of this application.
You host your content on a headless CMS, entirely decoupled from your frontend and then you get this content which is the news article from your Headless CMS and you send it to AI, so it can translate, it translates and then you can replace the article on the frontend with the translated data.
Your data doesn't change on the Headless CMS but only on the frontend thereby preventing mutation on your data.
The best way to see the headless CMS benefits is to explain the structure that you will build:
Application breakdown:
Host content on Headless CMS: BCMS to host your content, ensuring it is decoupled from the frontend for flexibility and scalability. Store all Article content in the CMS ensuring a single source of truth.
Fetch content: Retrieve the desired news articles from the headless CMS to be processed for translation.
User preference management: Implement a translation button to allow readers to select their preferred language for viewing articles.
Integrate AI translation Send the fetched content to an AI translation service (Gemini) for real-time translation into a user-selected language, and after that, you receive the translated content in the response Gemini sends.
Update frontend only: Replace the article content on the frontend with the translated content returned by Gemini. This update doesn’t mutate the article content stored in BCMS.
Sounds good? Let's get to it.
In this article, I will be creating a news website where the content is managed through a headless CMS, while the application itself is rendered using a separate frontend framework.
For a creating multilingual website I will need:
Headless CMS. BCMS is a headless content management system that facilitates straightforward content modeling for creative applications.
For the frontend, I will utilize Next.js due to its popularity and robust features.
Additionally, I will employ Google's Gemini as the AI platform for translation.
💡 Note:
You do not need to use AI for all applications you build with BCMS, because BCMS already has a built-in multiple language content editing functionality which will suit your needs for the translations you will likely need on a web page.
On your BCMS dashboard, you have the option to create multiple languages when creating an entry.
But if you want to give users the ability to translate to other languages not created by you on BCMS, then this approach will definitely interest you.
Next.js Application setup
Create News article Content In BCMS
Install BCMS into the built application frontend.
Display BCMS data in frontend
Sending Data to Gemini
Rendering Translated Data on the frontend
Now let's get started with setting up the Next.js application
This application template is already built and ready to go. Head over to this Github Repo, clone the starter branch and you will have the already designed Next.js application. Follow the readme instructions on how to get the application running.
With that, you can follow along with the rest of the tutorial.
When it comes to using BCMS, we have the option to use it on the cloud alone. Using BCMS on the cloud takes the headache of setting up our own servers and scaling it, since this is already covered by the BCMS team.
Using BCMS on the cloud simplifies your tasks. It handles server setup and maintenance, as well as ensuring continuous uptime, allowing you to concentrate on front-end development. The API URL is the same from development to production.
Visit https://app.thebcms.com/ to create a new BCMS account
After logging in, on your dashboard create a new project. I'm gonna name this project Multilingual Bcms Website.
Feel free to name it anything you want.
With the project created, you can now model the data that will live there and go ahead to create the data based on the models.
What does this mean?
The principle behind BCMS data creation is no different from the ones you’ve been using, be it your custom-made backend with either PHP or Nodejs or another CMS if any: You create a data model, this is where you specify the inputs you need, and the property types for that input and then you create data based on those data models.
On the left side of your BCMS dashboard is an administration panel containing Templates, Widgets, and Groups…., and then further down, you will find entries. What does this all mean, and how do they come together?
Templates - This is your content structure, your building block. Here is where you spell out what fields you need for the section you’re creating a template for.
Entries - An Entry is a simple record of a template, a dynamic piece of content that follows the content structure you defined in your template.
Groups - Groups in BCMS are reusable building blocks made of multiple properties. Groups can be included in any template, widget, or other group.
You will use all these assets to make a multilingual website template for your project.
Let’s see this in action.
To model data, you need to create a template for the data. Head over to your dashboard, on the administration tab, click on templates, and create a new template. I will name this template "News Article".
Upon creating this template, you automatically get the title property and also the slug property, which you use to query this data in your frontend app (more on this later) also on the right side of the page, you will see the available properties you can drag to this template to model your data. So what properties do you need for the news article?
Taking a look at the picture of the website below gives you the necessary information regarding the fields needed to build out a news article.
Title - String
Preview Text - String ( I'll use this as the subheading)
These will be the only information you will need to have predefined and required. All other article details can be dynamically added when adding an entry.
Now, after creating the model for the data, you can go ahead to create the data. You do that by clicking on entries on the left side of the dashboard.
Under entries, you will find the News Article entry there, click on it and click on Create new entry. Fill in the required Title and subheading fields, then go further in the textbox to write your dynamic article content.
Great, with that done, you have successfully modeled and created your data in BCMS. The next steps are now to install BCMS in the frontend, query our article data, and also send it to our AI for translation.
To be able to query our data from the frontend, we need an API key from BCMS that allows us access to this content.
To create an API key, navigate to settings, API keys -> Add new key.
On the next interface, toggle on the functionalities you want this key to have. Since I will only be using this key to get data and not update or delete, I will toggle on "can get" only.
With that, I will now go ahead to install BCMS on my frontend application and query my article data.
Back to the Next.js application, it is time to install BCMS. I will follow the instructions in the Next.js Integration guide on BCMS docs.
Since I have done the part of creating a BCMS project, creating templates and entries as well as creating the Next.js application, I will proceed to install the BCMS packages section in the guide.
Run the following command in your terminal to install BCMS-related packages: npm i --save @thebcms/cli @thebcms/client @thebcms/components-react
Update package.json
Modify the scripts section in package.json
as follows:
"scripts": { "dev": "bcms --pull types --lng ts && next dev", "build": "bcms --pull types --lng ts && next build", "start": "bcms --pull types --lng ts && next start", "lint": "bcms --pull types --lng ts && next lint" }
The bcms --pull types --lng ts
command starts the BCMS CLI and pulls types from BCMS, saving them in /bcms/types/
. These types work for both JavaScript and TypeScript.
Update Typescript Configuration: If you’re using TypeScript, open tsconfig.json
and set moduleResolution
to Node
:
{ "compilerOptions":{ "lib":[ "dom", "dom.iterable", "esnext" ], "allowJs":true, "skipLibCheck":true, "strict":true, "noEmit":true, "esModuleInterop":true, "module":"esnext", "moduleResolution":"Node", "resolveJsonModule":true, "isolatedModules":true, "jsx":"preserve", "incremental":true, "plugins":[ { "name":"next" } ], "paths":{ "@/*":[ "./src/*" ] } }, "include":[ "next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts" ], "exclude":[ "node_modules" ] }
Create .env
file
At the root of your project create an env
file containing your API key details:
BCMS_ORG_ID=YOUR_PROJECT_ORG_ID BCMS_INSTANCE_ID=YOUR_PROJECT_INSTANCE_ID BCMS_API_KEY_ID=API_KEY_ID BCMS_API_KEY_SECRET=API_KEY_SECRET
All the information for this configuration can be found on the API key page you created earlier, which is under settings.
Create bcms.config.cjs
In the root of your project, create a file called bcms.config.cjs
and add the following configuration:
/** * @type {import('@thebcms/cli/config').BCMSConfig} */ module.exports = { client: { orgId: process.env.BCMS_ORG_ID, instanceId: process.env.BCMS_INSTANCE_ID, apiKey: { id: process.env.BCMS_API_KEY_ID, secret: process.env.BCMS_API_KEY_SECRET, }, }, };
Initialize BCMS Client
In the /src/app
directory, create a new file called bcms-client.ts
Inside, initialize the BCMS client:
import { Client } from '@thebcms/client'; export const bcms = new Client( process.env.BCMS_ORG_ID || '', process.env.BCMS_INSTANCE_ID || '', { id: process.env.BCMS_API_KEY_ID || '', secret: process.env.BCMS_API_KEY_SECRET || '', }, { injectSvg: true, }, );
Restart dev process: npm run dev
Great, you have successfully linked BCMS to your Next.js project now, but to confirm if this installation was smooth, let's try to fetch data from BCMS.
To fetch data, go to the page.tsx
file in the app folder, at the top level of the file, paste this code imports. First of all, you import the BCMS client so you're able to use the functions BCMS provides import { bcms } from './bcms-client';
Now, secondly, you import the NewsArticle types, which was automatically generated for you when you ran npm run dev, since it was modified in the package.json to pull types upon startup at step no. 2 of installing BCMS. import { NewsArticleEntry, NewsArticleEntryMetaItem } from '../../bcms/types/ts';
Fetching data is an async function, so it makes sense to make your react component an async component: const Home: FC = async () => {
and inside the component statement, paste this code:
const newsArticles = (await bcms.entry.getAll('news-article')) as NewsArticleEntry[]; const items = newsArticles.map((newsArticle) => { return newsArticle.meta.en as NewsArticleEntryMetaItem; }); console.log(items)
This code above uses bcms.entry.get
: All methods to get all entries in a template using either the template name or template ID and it type-casts the result to NewsArticleEntry[] interface. If the template is a two-word name, you simply write the names in lowercase with a dash in the middle to separate them: News Article -> news-article.
Now the items variable simply loops over the extracted results and gets the meta.en property and also type-casts it to NewsArticleEntryMetaItem, and then stores the extracted metadata.
If all is set up correctly, you should see the NewsArticle logged on your terminal:
With my data being fetched successfully, I can now display it on the frontend, replacing the dummy data that was there.
import { FC } from "react"; import { bcms } from './bcms-client'; import { NewsArticleEntry, NewsArticleEntryMetaItem } from '../../bcms/types/ts'; import Article, { ArticleProps } from "./components/article"; import LanguageSwitch from "./components/lang-switch"; const Home: FC = async () => { const newsArticles = (await bcms.entry.getAll('news-article')) as NewsArticleEntry[]; const items = newsArticles.map((newsArticle) => { return newsArticle.meta.en as NewsArticleEntryMetaItem; }); return ( <div> <div className="mb-9 lg:mb-20"> <h1 className="text-center text-4xl font-bold leading-none tracking-[-0.72px] mb-3 lg:text-left lg:text-[56px] lg:leading-[-1.12px] lg:mb-6"> Welcome to our Blog </h1> <p className="text-main-light text-center text-sm leading-[1.4] lg:text-left lg:text-lg lg:leading-[1.4]"> Welcome to our blog page. Below you’ll find a list of posts on a variety of topics, all aimed at sparking thought and conversation. Whether you’re looking for helpful tips or personal reflections, we hope you enjoy browsing through what we’ve shared. Feel free to leave your own thoughts in the comments. </p> </div> <LanguageSwitch /> <div className="grid grid-cols-1 gap-[18px] lg:gap-9"> {items.map((item) => { return <Article {...item} key={item.slug} />; })} </div> </div> ); }; export default Home;
Getting Data here is exactly the same process, but this time, instead of using the bcms getAll
method, you use getByslug
.
But before that, I will have to get the slug params using Next.js inbuilt generateStaticParams
Navigate to src/app/article/[slug]/page.tsx
and paste this code below to get the slug from the URL params.
import { bcms } from '../../bcms-client'; import { NewsArticleEntry, NewsArticleEntryMetaItem } from '../../../../bcms/types/ts'; // Import the parsed content that BCMS automatically generates import { BCMSEntryContentParsedItem } from '../../../../bcms/types/ts'; export async function generateStaticParams() { const articles = (await bcms.entry.getAll('news-article')) as NewsArticleEntry[]; return articles.map((article) => { const meta = article.meta.en as NewsArticleEntryMetaItem; return { slug: meta.slug, }; }); }
After extracting the slug, you can now use it to search for the article on BCMS like so:
const Article: FC = async ({ params }) => { const articles = (await bcms.entry.getAll('news-article')) as NewsArticleEntry[]; const article = await articles.find((e) => e.meta.en?.slug === params.slug); if (!article) { return "Not found"; } const data = { meta: article.meta.en as NewsArticleEntryMetaItem, content: article.content.en as BCMSEntryContentParsedItem[], }; console.log(data); }
In the console, you can now see the returned data from BCMS:
From the statement above, you see that you get the field defined in the template in the meta section in addition to the other content you added dynamically when making the entry. You already saw how to manually display the defined fields, but what about the dynamic content section? How do you display it given that the fields are not predefined, so different entries have different fields?
To be able to handle this dynamic content section, you need a content manager component to parse these fields and render them based on what is on the entry.
On the components folder, create a new file named ContentManager.tsx
and paste the code below into it.
'use client'; import { useEffect, useRef } from 'react'; import { useRouter } from 'next/navigation'; import { BCMSContentManager, BCMSWidgetComponents, } from '@thebcms/components-react'; import { EntryContentParsedItem } from '@thebcms/types'; interface Props { items: EntryContentParsedItem[]; widgetComponents?: BCMSWidgetComponents; className?: string; } const ContentManager: React.FC<Props> = ({ items, widgetComponents, className = '', }) => { const managerDOM = useRef<HTMLDivElement>(null); const router = useRouter(); const parseInternalLinks = (): void => { if (managerDOM.current) { const links = managerDOM.current.querySelectorAll('a'); links.forEach((link: HTMLAnchorElement) => { const href = link.getAttribute('href'); if (href && href.startsWith('/')) { link.target = '_self'; const clickHandler = (event: Event): void => { event.preventDefault(); void router.push(href); }; link.addEventListener('click', clickHandler); return () => { link.removeEventListener('click', clickHandler); }; } }); } }; useEffect(() => { parseInternalLinks(); }, []); return ( <div ref={managerDOM}> <BCMSContentManager className={className} items={items} widgetComponents={widgetComponents || {}} /> </div> ); }; export default ContentManager;
The code above simply receives the content as a prop and then parses it, parses the links, and then renders each element that appears dynamically.
So now, back to the index.tsx
slug page, I will import the ContentManager component so I will be able to use it.
import ContentManager from '../../components/ContentManager';
After importing, I will clean up my index.tsx
slug page and add the content manager component to render my content.
<ContentManager items={data.content} className="prose lg:prose-lg" /> // The prose style is using a tailwindCSS typography plugin to render the different html // tags nicely. The plugin is tailwindcss/typography which is installed for you in the // starter.
Your finished [slug]/page.tsx
should look like this at this point.
import { FC } from 'react'; import LanguageSwitch from '../../components/lang-switch'; import Image from 'next/image'; import { bcms } from '../../bcms-client'; import { NewsArticleEntry, NewsArticleEntryMetaItem, } from '../../../../bcms/types/ts'; import { BCMSEntryContentParsedItem } from '../../../../bcms/types/ts'; import ContentManager from '../../components/ContentManager'; export async function generateStaticParams() { const articles = (await bcms.entry.getAll( 'news-article' )) as NewsArticleEntry[]; return articles.map((article) => { const meta = article.meta.en as NewsArticleEntryMetaItem; return { slug: meta.slug, }; }); } const Article: FC = async ({ params }) => { const articles = (await bcms.entry.getAll( 'news-article' )) as NewsArticleEntry[]; const article = await articles.find((e) => e.meta.en?.slug === params.slug); if (!article) { return 'Not found'; } const data = { meta: article.meta.en as NewsArticleEntryMetaItem, content: article.content.en as BCMSEntryContentParsedItem[], }; console.log(data); return ( <div> <LanguageSwitch /> <div className="grid grid-cols-1 gap-9 lg:gap-12"> <div className="pb-9 border-b border-border lg:pb-12"> <h1 className="text-center text-4xl font-bold leading-none tracking-[-0.72px] mb-3 lg:text-left lg:text-[56px] lg:leading-[-1.12px] lg:mb-6"> {data.meta.title} </h1> <p className="text-main-light text-center text-sm leading-[1.4] lg:text-left lg:text-lg lg:leading-[1.4]"> {data.meta.subheading} </p> </div> <ContentManager items={data.content} className="prose lg:prose-lg" /> </div> </div> ); }; export default Article;
And it should render beautifully on your browser.
Congratulations, your article is now successfully created on BCMS and rendered on your browser using Next.js. Time for the other fun part of this tutorial: Sending Article data to Gemini.
To use Gemini, you'll need a Gemini account, so head over to gemini.google.com to create an account which is pretty straightforward.
After that, you can follow these easy steps to set up Gemini in your Next.js application.
npm install @google/generative-ai
Go to https://aistudio.google.com/app/apikey to create an API key, copy the API key and in your application navigate to your .env file and paste the API key like so:
GEMINI_API_KEY=YOUR_API_KEY
Create a src/lib/gemini.ts
file in your project folder.
This file will hold the logic that sends the content needed for machine translation to Gemini.
import { GoogleGenerativeAI, GenerateContentResult } from "@google/generative-ai"; const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY || ""); interface TranslationResult { translatedText: string; } async function translateText(text: string, targetLanguage: string): Promise<TranslationResult> { const model = genAI.getGenerativeModel({ model: "gemini-pro"}); const prompt = `Translate the following text to ${targetLanguage}:\n\n${text}`; try { const result: GenerateContentResult = await model.generateContent(prompt); const response = result.response; const translatedText = response.text(); return { translatedText }; } catch (error: any) { console.error("Error during translation:", error); throw new Error(error.message || "Failed to translate text"); } } export { translateText };
With this code, you have successfully set up the Gemini API client and also wrote the logic that sends the content to Gemini for translation.
To send data to the Gemini API client, you’d need to use a Next.js route handler.
Create a src/pages/api/translate/route.ts
file with the following content:
// app/api/translate/route.ts import { translateText } from '../../../lib/gemini'; import { NextResponse } from 'next/server'; export async function POST(request: Request) { try { const { text, targetLanguage } = await request.json(); if (!text || !targetLanguage) { return NextResponse.json( { message: 'Text and target language are required' }, { status: 400 } ); } const { translatedText } = await translateText(text, targetLanguage); return NextResponse.json({ translatedText }); } catch (error: any) { return NextResponse.json( { message: error.message || 'Failed to translate text' }, { status: 500 } ); } }
The code above simply receives a post request with the data needed for translation, validates it, and sends it to the translate Text function in the Gemini API client for translation, and also delivers error handling for a fail case scenario.
Your content is ready for translation, and Gemini is ready to accomplish its role of language switcher.
I'll extract the functionality that handles the data fetching, staticParams generator, and UI into different files in the [slug] folder.
app/
[slug]/
page.tsx (server component)
ArtcleClient.tsx (client component)
server.ts (static params)
First, create a new file named ArticleClient.tsx
that will be used as a client-side component, this is necessary because to listen to events from the LanguageSwitch component when the button changes, you need a Client Side component.
"use client"; import { FC } from 'react'; import LanguageSwitch from '../../components/lang-switch'; import { NewsArticleEntry, NewsArticleEntryMetaItem, } from '../../../../bcms/types/ts'; import { BCMSEntryContentParsedItem } from '../../../../bcms/types/ts'; import ContentManager from '../../components/ContentManager'; interface ArticleClientProps { article: NewsArticleEntry; } const ArticleClient: FC<ArticleClientProps> = ({ article }) => { const data = { meta: article.meta.en as NewsArticleEntryMetaItem, content: article.content.en as BCMSEntryContentParsedItem[], }; // We'll handle the translation event here. const handleTranslationClick = (button: string) => { console.log('button clicked is:' + button); }; return ( <div> // LanguageSwitch Component is now listening for a translation click. <LanguageSwitch onLanguageChange={handleTranslationClick} /> <div className="grid grid-cols-1 gap-9 lg:gap-12"> <div className="pb-9 border-b border-border lg:pb-12"> <h1 className="text-center text-4xl font-bold leading-none tracking-[-0.72px] mb-3 lg:text-left lg:text-[56px] lg:leading-[-1.12px] lg:mb-6"> {data.meta.title} </h1> <p className="text-main-light text-center text-sm leading-[1.4] lg:text-left lg:text-lg lg:leading-[1.4]"> {data.meta.subheading} </p> </div> <ContentManager items={data.content} className="prose lg:prose-lg" /> </div> </div> ); }; export default ArticleClient;
Now, clean up your [slug]/page.tsx
file, so the only functionality there is to handle data fetching, loading UI and also passing down the data to the ArticleClient component:
import { bcms } from '../../bcms-client'; import { NewsArticleEntry } from '../../../../bcms/types/ts'; import ArticleClient from './ArticleClient'; async function Page({ params }: { params: { slug: string } }) { const articles = await bcms.entry.getAll('news-article') as NewsArticleEntry[]; const article = articles.find((e) => e.meta.en?.slug === params.slug); if (!article) { return <div>Article not found</div>; } return <ArticleClient article={article} />; } export default Page;
For that to work completely, you need to get the params.
Now create a new file named server.ts
which will get the params and make it accessible to page.tsx:
import { bcms } from '../../bcms-client'; import { NewsArticleEntry } from '../../../../bcms/types/ts'; export async function generateStaticParams() { const articles = await bcms.entry.getAll('news-article') as NewsArticleEntry[]; return articles.map((article) => ({ slug: article.meta.en?.slug, })); }
In the LanguageSwitch.tsx
component, I am going to:
Add proper type definitions for the component props which I will get from ArticleClient
Added proper handling of the onLanguageChange
prop in the AI language buttons:
"use client"; import Image from "next/image"; import { FC, useState } from "react"; interface LanguageSwitchProps { onLanguageChange?: (language: string) => void; } const LanguageSwitch: FC <LanguageSwitchProps> = ({ onLanguageChange }) => { const [showLangs, setShowLangs] = useState(false); const [currentLang, setCurrentLang] = useState("en"); const langs = [ { label: "English", code: "en", icon: "/flag/en.png", }, { label: "French", code: "fr", icon: "/flag/fr.png", }, { label: "Spanish", code: "es", icon: "/flag/es.png", }, { label: "Greek", code: "el", icon: "/flag/el.png", ai: true, }, { label: "Portuguese", code: "pt", icon: "/flag/pt.png", ai: true, }, { label: "Mandarin", code: "zh", icon: "/flag/zh.png", ai: true, }, { label: "German", code: "de", icon: "/flag/de.png", ai: true, }, ]; const nonAiLangs = langs.filter((lang) => !lang.ai); const aiLangs = langs.filter((lang) => lang.ai); return ( <div className="relative mb-[18px] lg:mb-9"> <button className="flex items-center justify-between p-3 rounded-full w-[100px] ml-auto lg:w-[180px] lg:p-4" style={{ boxShadow: "0px 0px 8px -4px rgba(16, 24, 40, 0.04), 0px 4px 24px -4px rgba(16, 24, 40, 0.04)", }} onClick={() => { setShowLangs(!showLangs); }} > <span className="flex items-center gap-1 mr-2 lg:gap-2"> <Image src={`/flag/${currentLang}.png`} alt={currentLang} width={18} height={18} className="size-3 object-contain lg:size-[18px]" /> <span className="text-xs leading-none tracking-[-0.36px] lg:text-lg lg:tracking-[-0.54px]"> {langs.find((lang) => lang.code === currentLang)?.label} </span> </span> <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className="size-6" > <path d="M6 9L12 15L18 9" stroke="#1D1D1D" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" /> </svg> </button> {showLangs && ( <div className="absolute z-10 -bottom-3 right-0 translate-y-full w-[120px] p-1.5 rounded-[18px] bg-white grid grid-cols-1 gap-1 lg:-bottom-2 lg:w-[190px] lg:p-2 lg:rounded-3xl" style={{ boxShadow: "0px 0px 8px -4px rgba(16, 24, 40, 0.04), 0px 4px 24px -4px rgba(16, 24, 40, 0.04)", }} > {nonAiLangs.map((lng) => { return ( <button key={lng.code} className={`flex items-center gap-1 w-full p-1.5 rounded-3xl transition-colors duration-300 ${ currentLang === lng.code ? "bg-accent-100" : "" } hover:bg-accent-100 focus-visible:bg-accent-100 lg:gap-2 lg:px-[18px] lg:py-2`} onClick={() => { setCurrentLang(lng.code); setShowLangs(false); }} > <Image src={`/flag/${lng.code}.png`} alt={lng.code} width={18} height={18} className="size-3 object-contain lg:size-[18px]" /> <span className="text-xs leading-none tracking-[-0.36px] lg:text-lg lg:tracking-[-0.54px]"> {lng.label} </span> </button> ); })} <div className="flex mt-1 max-w-max mx-auto pt-1 border-t border-border lg:mt-2"> <span className="text-main-light text-[10px] leading-none lg:text-xs lg:leading-none"> Translated by AI </span> </div> {aiLangs.map((lng) => { return ( <button key={lng.code} className={`flex items-center gap-1 w-full p-1.5 rounded-3xl transition-colors duration-300 ${ currentLang === lng.code ? "bg-accent-100" : "" } hover:bg-accent-100 focus-visible:bg-accent-100 lg:gap-2 lg:px-[18px] lg:py-2`} onClick={() => { setCurrentLang(lng.code); setShowLangs(false); if (onLanguageChange) { onLanguageChange(lng.label); } }} > <Image src={`/flag/${lng.code}.png`} alt={lng.code} width={18} height={18} className="size-3 object-contain lg:size-[18px]" /> <span className="text-xs leading-none tracking-[-0.36px] lg:text-lg lg:tracking-[-0.54px]"> {lng.label} </span> <svg viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg" className="size-3 ml-auto flex-shrink-0 lg:size-3.5" > <path fillRule="evenodd" clipRule="evenodd" d="M5.25814 1.75004C5.41285 1.75004 5.56122 1.8115 5.67062 1.92089C5.78002 2.03029 5.84147 2.17866 5.84147 2.33337V2.91671H7.59147C7.74618 2.91671 7.89456 2.97817 8.00395 3.08756C8.11335 3.19696 8.17481 3.34533 8.17481 3.50004C8.17481 3.65475 8.11335 3.80312 8.00395 3.91252C7.89456 4.02192 7.74618 4.08337 7.59147 4.08337H7.55939C7.42931 5.26871 6.90839 6.41087 6.07481 7.40137C6.45713 7.7324 6.87161 8.02436 7.31206 8.27287L8.51781 5.59421C8.56391 5.4917 8.63865 5.4047 8.73303 5.34366C8.82741 5.28262 8.93741 5.25015 9.04981 5.25015C9.1622 5.25015 9.27221 5.28262 9.36659 5.34366C9.46097 5.4047 9.5357 5.4917 9.58181 5.59421L12.2068 11.4275C12.268 11.5683 12.2712 11.7274 12.2159 11.8705C12.1605 12.0137 12.051 12.1292 11.911 12.1921C11.7711 12.255 11.6119 12.2603 11.4681 12.2067C11.3244 12.1531 11.2075 12.045 11.1428 11.9059L10.5099 10.5H7.58914L6.95681 11.9059C6.89215 12.045 6.77526 12.1531 6.63147 12.2067C6.48767 12.2603 6.32856 12.255 6.18861 12.1921C6.04865 12.1292 5.93913 12.0137 5.88376 11.8705C5.82839 11.7274 5.83164 11.5683 5.89281 11.4275L6.83197 9.34154C6.26843 9.03129 5.74032 8.66064 5.25697 8.23612C4.50622 8.89879 3.59564 9.45996 2.55789 9.87529C2.41424 9.93277 2.25365 9.93082 2.11143 9.86989C1.96922 9.80895 1.85703 9.69402 1.79956 9.55037C1.74208 9.40673 1.74403 9.24613 1.80496 9.10392C1.86589 8.9617 1.98083 8.84951 2.12447 8.79204C3.03272 8.42862 3.80856 7.95146 4.44147 7.40254C3.94924 6.82317 3.55907 6.16434 3.28764 5.45421C3.23334 5.30932 3.23881 5.1488 3.30287 5.00795C3.36692 4.8671 3.4843 4.75747 3.62918 4.70317C3.77407 4.64886 3.93459 4.65434 4.07544 4.71839C4.21629 4.78244 4.32592 4.89982 4.38022 5.04471C4.58989 5.59324 4.8863 6.10453 5.25814 6.55904C5.87939 5.78729 6.26147 4.93387 6.38281 4.08337H2.34147C2.18676 4.08337 2.03839 4.02192 1.92899 3.91252C1.8196 3.80312 1.75814 3.65475 1.75814 3.50004C1.75814 3.34533 1.8196 3.19696 1.92899 3.08756C2.03839 2.97817 2.18676 2.91671 2.34147 2.91671H4.67481V2.33337C4.67481 2.17866 4.73627 2.03029 4.84566 1.92089C4.95506 1.8115 5.10343 1.75004 5.25814 1.75004ZM9.98489 9.33337L9.04981 7.25496L8.11531 9.33337H9.98489ZM11.0915 0.583374C11.2132 0.583423 11.3318 0.621521 11.4307 0.692335C11.5297 0.763149 11.604 0.863133 11.6433 0.978291L11.7191 1.19879C11.8055 1.45181 11.9486 1.68169 12.1376 1.87079C12.3266 2.05988 12.5564 2.20316 12.8094 2.28962L13.0299 2.36487C13.1449 2.40429 13.2448 2.47867 13.3154 2.57761C13.3861 2.67655 13.4241 2.79511 13.4241 2.91671C13.4241 3.0383 13.3861 3.15686 13.3154 3.2558C13.2448 3.35474 13.1449 3.42912 13.0299 3.46854L12.8094 3.54437C12.5564 3.63071 12.3265 3.77386 12.1374 3.96285C11.9483 4.15184 11.805 4.38165 11.7186 4.63462L11.6433 4.85512C11.6039 4.97015 11.5295 5.06999 11.4306 5.14068C11.3316 5.21136 11.2131 5.24936 11.0915 5.24936C10.9699 5.24936 10.8513 5.21136 10.7524 5.14068C10.6534 5.06999 10.5791 4.97015 10.5396 4.85512L10.4638 4.63462C10.3775 4.3816 10.2343 4.15172 10.0453 3.96263C9.85634 3.77354 9.62653 3.63026 9.37356 3.54379L9.15306 3.46854C9.03803 3.42912 8.93819 3.35474 8.86751 3.2558C8.79682 3.15686 8.75882 3.0383 8.75882 2.91671C8.75882 2.79511 8.79682 2.67655 8.86751 2.57761C8.93819 2.47867 9.03803 2.40429 9.15306 2.36487L9.37356 2.28904C9.62658 2.20271 9.85646 2.05955 10.0456 1.87056C10.2346 1.68157 10.3779 1.45177 10.4644 1.19879L10.5396 0.978291C10.5789 0.863133 10.6533 0.763149 10.7522 0.692335C10.8512 0.621521 10.9698 0.583423 11.0915 0.583374ZM11.0915 2.44771C10.9535 2.62137 10.7961 2.77872 10.6225 2.91671C10.7967 3.05476 10.953 3.2111 11.0915 3.38571C11.2295 3.21148 11.3859 3.05515 11.5605 2.91671C11.3868 2.77872 11.2295 2.62137 11.0915 2.44771Z" fill="#697586" /> </svg> </button> ); })} </div> )} </div> ); }; export default LanguageSwitch;
Save up your file, go to your browser on an article page, click on an AI language, and on your browser console, you should see the message language you clicked logged onto the console with the message button clicked is:language
To send data, to the API route, modify your ArticleContent.tsx
file to send the data on the handleTranslationClick
event.
const handleTranslationClick = async (button: string) => { try { const response = await fetch('/api/translate', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ text: data.content.map(item => item.value).join(' '), targetLanguage: button }), }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const { translatedText } = await response.json(); console.log(translatedText); return translatedText; } catch (error) { console.error("Error during translation request:", error); throw error; } };
The code above extracts the value of data.content
and sends it to the API route to handle the translation. On your browser now, when you click on an AI language, it should return the translatedText along with the html tags from Gemini.
With the translation data now being set back, I can now create a loading UI for users when the data is being translated and also show the translated data on the frontend page.
// article/[slug]/ArticleClient.tsx "use client"; import { FC, useState } from 'react'; import LanguageSwitch from '../../components/lang-switch'; import { NewsArticleEntry, NewsArticleEntryMetaItem, } from '../../../../bcms/types/ts'; import { BCMSEntryContentParsedItem } from '../../../../bcms/types/ts'; import ContentManager from '../../components/ContentManager'; interface ArticleClientProps { article: NewsArticleEntry; } const ArticleClient: FC<ArticleClientProps> = ({ article }) => { const [isLoading, setIsLoading] = useState(false); const [translatedContent, setTranslatedContent] = useState<string | null>(null); const data = { meta: article.meta.en as NewsArticleEntryMetaItem, content: article.content.en as BCMSEntryContentParsedItem[], }; const handleTranslationClick = async (button: string) => { setIsLoading(true); try { const response = await fetch('/api/translate', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ text: data.content.map(item => item.value).join(' '), targetLanguage: button }), }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const { translatedText } = await response.json(); setTranslatedContent(translatedText); } catch (error) { console.error("Error during translation request:", error); throw error; } finally { setIsLoading(false); } }; return ( <div> <LanguageSwitch onLanguageChange={handleTranslationClick} /> <div className="grid grid-cols-1 gap-9 lg:gap-12"> <div className="pb-9 border-b border-border lg:pb-12"> <h1 className="text-center text-4xl font-bold leading-none tracking-[-0.72px] mb-3 lg:text-left lg:text-[56px] lg:leading-[-1.12px] lg:mb-6"> {data.meta.title} </h1> <p className="text-main-light text-center text-sm leading-[1.4] lg:text-left lg:text-lg lg:leading-[1.4]"> {data.meta.subheading} </p> </div> {isLoading ? ( <div className="text-center">Translating content...</div> ) : ( <ContentManager items={translatedContent ? [{value: translatedContent, type: 'text'}] : data.content} className="prose lg:prose-lg" /> )} </div> </div> ); }; export default ArticleClient;
Now, on your browser, click on an AI language and see the magic happen. First, it will show you a translating content message, and after translation, voila, your data will be returned and well-formatted like it was. yayyyyyyy 🎉
What a ride it has been, from setting up our BCMS instance to retrieving our BCMS data, sending that data to Gemini, and also retrieving translated content and loading it onto our UI. Truly amazing.
What are the possible next steps you can take?
First of all, you can see the codebase on the GitHub URL and the finished application on Vercel: https://multilingual-news-website-nextjs-bcms.vercel.app/.
Some possible next steps are:
Include the header and subheading to the data being sent for translation.
Listening to a click event on the English button so data can be translated back to English.
Creating multiple content translations in BCMS and then using the nonAiLangs buttons to translate content to those languages you defined in BCMS. See BCMS multi-language in action.
Deploy to your hosting provider.
As you can see, combining headless CMS and AI tools makes website translation pretty straightforward and intuitive, which has a positive impact on user experience.
So, if your next project needs to have multilingual content, consider trying BCMS for building the best multilingual website.
Get all the latest BCMS updates, news and events.
By submitting this form you consent to us emailing you occasionally about our products and services. You can unsubscribe from emails at any time, and we will never pass your email to third parties.
There are many actionable insights in this blog post. Learn more: