Reading time: 17 min read

How to implement multilingual UI labels using Sitecore Dictionary in SitecoreAI

A practical guide to centralizing, fetching, and rendering translated UI strings in Next.js with Sitecore Content SDK.

Portrait photo of Gerald Encabo, article author

Why UI strings need a dictionary

Every multilingual website has two translation problems.

The first is content translation. It's the one everyone thinks about: page titles, body copy, hero images. Sitecore handles this natively through language versions on content items.

The second is quieter but just as pervasive: UI string translation. Every "Read More" button, every "Home" breadcrumb label, every "Search site content..." placeholder, every "No results found" empty state. These strings live in code, not in content items, and they don't translate themselves.

Hardcoding works fine until the day your site goes bilingual. Then every "Read More" scattered across your codebase becomes a bug, visible to users, invisible to content authors, and tedious to fix one component at a time.

The Sitecore Dictionary solves this cleanly: a centralized, CMS-managed key-value store for UI strings with full language versioning. Content authors manage translations in an interface they already know. Developers reference keys in code without worrying about specific languages. Neither side blocks the other.

This guide walks through a complete implementation (from portal configuration through production component patterns) using a real SitecoreAI starter kit with @sitecore-content-sdk/nextjs and next-localization.

Architecture overview

Before touching any code, it helps to see the full pipeline. A translated string passes through five layers on its way from CMS to screen:

Each layer has a single responsibility:

Layer Owns Changes require Who
CMS Translation content CMS publish (no deploy) Content Author
Server Locale-aware fetching Code change Developer
Page Props Data transport Automatic (pass-through)

React Context Distribution to tree Automatic (provided by SDK)
 
Component Rendering the string Code change Developer

This separation means content authors can update translations without code deployments, and developers can add new keys without CMS access.

Step 1: Configuring languages in the SitecoreAI portal

Everything starts in the CMS. Before writing a single line of Next.js code, you need to register your target languages in Sitecore. This is a multi-layer process, and skipping any layer produces confusing downstream failures: a language that "exists" but doesn't appear, or translations that work in preview but not on the live site.

1A. Adding a language to your SitecoreAI environment

Every SitecoreAI environment starts with English (en). To support French, German, or any other locale, you must explicitly add it.

Adding a predefined language (e.g., French - Canada, German):

  1. Open the Content Editor in your SitecoreAI instance
  2. Navigate to /sitecore/System/Languages in the content tree
  3. Right-click the Languages node and select Insert > Language
  4. In the Add a New Language dialog, use the "Choose a predefined language code" dropdown
  5. Select the language and region code (e.g., fr-CA for French-Canada, de-DE for German)
  6. Click Next, review the settings, then click Close

The new language item now appears under /sitecore/System/Languages/:

/sitecore/System/Languages/
├── en          (English - default)
├── fr-CA       (French - Canada)

Alternatively, you can manage languages from the SitecoreAI portal's Settings > Languages screen:

  1. Go to Settings > Languages
  2. Click the Add language button
  3. Select the language and region code
  4. Optionally set a Fallback language (e.g., English for fr-CA)
  5. Save

The Languages screen shows all registered languages with their fallback configuration and language codes:

SitecoreAI Languages Dashboard

1B. Enabling the language on your site

Adding a language to the environment makes it available. To make it active for a specific site, you need to register it in the site's settings.

  1. Go to the Sites management area in the SitecoreAI portal
  2. Select your site
  3. In the Languages section, enable the newly added languages
  4. Save the configuration

1C. Configuring language fallback

Language fallback determines what happens when content doesn't have a version in the requested language. Instead of showing a blank page, Sitecore can fall back to another language.

Setting fallback on the language item:

  1. In Content Editor, navigate to /sitecore/System/Languages/fr-CA (or your target language)
  2. In the Data section, find the Fallback Language field
  3. Set it to en (or whichever language should serve as the fallback)
  4. Save

Here's what this looks like in a serialized language item:

/sitecore/system/Languages/fr-CA.yml
SharedFields:
  - Hint: Regional Iso Code
    Value: "fr-CA"
  - Hint: Charset
    Value: "iso-8859-1"
  - Hint: Encoding
    Value: "utf-8"
  - Hint: Fallback Language
    Value: en # Falls back to English
  - Hint: Iso
    Value: fr

1D. Creating language versions for content

With the language enabled, you need to create language versions so authors have something to translate.

Bulk-create versions using the SXA script:

  1. In Content Editor, right-click your site root item (e.g., /sitecore/content/{YourSiteCollectionName}/main)
  2. Click Scripts > Add Site Language
  3. Set:
    • Existing Language: en (source)
    • New Language: fr-CA (target)
  4. Click OK

This copies all field values from English to the target language across the entire site tree, giving content authors a starting point for translation. Dictionary items under the site tree are included in this operation, but you'll still want to review and translate them individually — covered in detail in Step 2

1E. Serializing language and dictionary items

To version-control languages and dictionary entries, include them in your SCS module configuration. Add /sitecore/system/Languages (scope: DescendantsOnly) to a languages module, and serialize dictionary items under /sitecore/content/{YourSiteCollectionName}/{YourSiteName}/Dictionary.

In SitecoreAI, serialized items are packaged into an IAR (.dat) file by the deployment process and automatically deployed to the CM filesystem — developers don't need to run any push commands for cloud environments. However, if you're using Docker for local development, you'll need to run dotnet sitecore ser push during initial setup and whenever you switch branches.

Checkpoint

Before moving to the Next.js side, verify each layer is in place. A missing layer here produces confusing bugs later: languages that "exist" but show no content, or translations that work in one environment but not another.

Layer What to verify Example
Environment Language items at /sitecore/System/Languages/ en, fr-CA
Site Languages enabled in SitecoreAI Portal > Sites en + fr-CA
Fallback Fallback Language field on language items fr-CAen
Serialization .module.json includes for languages and dictionary common.languages.module.json

Step 2: Creating dictionary items in Sitecore

With languages configured, it's time to populate the dictionary itself. This is where the CMS work directly connects to what developers will consume in code. Every key you create here becomes a t() call in a React component.

Dictionary items live under your site's content tree at:

/sitecore/content/{YourSiteCollectionName}/{SiteName}/Dictionary/

For maintainability on large sites, organize entries in alphabetical subfolders:

/Dictionary/
├── A/
│   ├── Achievements.yml
│   └── Applications.yml
├── B/
│   └── BackTo.yml
├── H/
│   └── Home.yml
├── R/
│   └── ReadMore.yml
└── S/
    ├── Search.yml
    └── SubmitSearch.yml

Each dictionary entry item has two critical fields:

Field Scope Purpose
Key Shared (language-independent) The lookup identifier used in code via t('...')
Phrase Versioned (per language) The translated output displayed to users

Here's what a serialized dictionary item looks like:

/Dictionary/A/Achievements.yml
SharedFields:
  - ID: "580c75a8-c01a-4580-83cb-987776ceb3af"
    Hint: Key
    Value: Achievements
Languages:
  - Language: en
    Fields:
      - ID: "2ba3454a-9a9c-4cdf-a9f8-107fd484eb6e"
        Hint: Phrase
        Value: Achievements
  - Language: fr-CA
    Fields:
      - ID: "2ba3454a-9a9c-4cdf-a9f8-107fd484eb6e"
        Hint: Phrase
        Value: Réalisations

Warning: The Key field value must exactly match the string you pass to t() in your code. Not the item name, not the display name — the Key field. A mismatch like t('Filtering by tags:') in code versus a Key of Filtering by tag(s): in Sitecore is a silent failure that returns the key string as-is. These mismatches are invisible to end users on English pages (since the fallback is the English key) and only surface when someone switches to another language.

Translating dictionary entries

Each dictionary entry needs a Phrase value for every supported language:

  1. Navigate to /sitecore/content/{YourSiteCollectionName}/{YourSiteName}/Dictionary/

Sitecore dictionary content tree showing dictionary items organized in alphabetical subfolders

  1. Select a dictionary entry (e.g., "Home")
  2. Switch to the target language using the language selector in the ribbon
  3. If no version exists, click Add a new version in the notification bar
  4. Enter the translated Phrase value
  5. Save and repeat for all dictionary entries

Step 3: Configuring locales in Next.js

The CMS side is done. Now the front end needs to know which locales exist. Rather than scattering locale strings across config files, create a centralized configuration that both Next.js routing and the Sitecore SDK can reference:

Note: The examples in this guide use the Pages Router (next.config.js with i18n, _app.tsx, getStaticProps). If your project uses App Router, it's handled differently and will not be covered in this guide.

// src/lib/i18n/i18n-config.js
const defaultLocale = "en";
const applicationLocales = [defaultLocale, "fr-CA"];
module.exports = {
  defaultLocale,
  applicationLocales,
};

With this standard setup, Next.js serves the default locale (en) at unprefixed URLs and other locales at prefixed paths:

URL Locale
/about en (default, no prefix)
/fr-CA/about fr-CA

Wire this into next.config.js:

// next.config.js
const {
  applicationLocales,
  defaultLocale,
} = require("./src/lib/i18n/i18n-config");
module.exports = {
  i18n: {
    locales: applicationLocales,
    defaultLocale:
      process.env.DEFAULT_LANGUAGE ||
      process.env.NEXT_PUBLIC_DEFAULT_LANGUAGE ||
      defaultLocale,
    localeDetection: false,
  },
  // ... rest of config
};

Wire this into next.config.js:

// next.config.js
const {
  applicationLocales,
  defaultLocale,
} = require("./src/lib/i18n/i18n-config");
module.exports = {
  i18n: {
    locales: applicationLocales,
    defaultLocale: defaultLocale,
    localeDetection: false,
  },
  
  // ... rest of config
};

And into Sitecore's redirect configuration:

// sitecore.config.ts
import { defineConfig } from "@sitecore-content-sdk/nextjs/config";
import { applicationLocales } from "lib/i18n/i18n-config";
export default defineConfig({
  // ... api config ...
  redirects: {
    enabled: true,
    locales: applicationLocales,
  },
});

Why localeDetection: false? In a SitecoreAI setup, Sitecore controls locale routing, not the browser's Accept-Language header. Disabling Next.js auto-detection prevents conflicts with Sitecore's language resolution.

Advanced: Forcing a locale prefix on every URL. Some projects require every URL to include a locale segment (e.g., /en/about instead of /about). This can be achieved by setting defaultLocale to a sentinel value like default and using custom middleware to redirect unprefixed URLs to their locale-prefixed equivalents. This approach adds complexity and is not covered in this guide.

Step 4: Fetching the dictionary and wiring the provider

With routing configured, the dictionary needs to reach each page and become available to every component. This happens in two parts: fetching the dictionary at build time, then injecting it into React context.

Good news: This dictionary fetching is already included in all Content SDK Next.js Pages Router sample apps and starter kits (content-sdk starter, xmcloud-starter-js). If you scaffolded your project from one of these, the wiring is already in place.

4A. Fetching with SSG

The dictionary is fetched at build time alongside layout data using getStaticProps. This is the standard Static Site Generation (SSG) pattern — pages are pre-rendered at build time, with optional ISR revalidate to refresh them periodically after deployment.

// src/pages/[[...path]].tsx
import { GetStaticPaths, GetStaticProps } from "next";
import {
  SitecorePageProps,
  StaticPath,
  SiteInfo,
} from "@sitecore-content-sdk/nextjs";
import { extractPath } from "@sitecore-content-sdk/nextjs/utils";
import { isDesignLibraryPreviewData } from "@sitecore-content-sdk/nextjs/editing";
import client from "lib/sitecore-client";
import components from ".sitecore/component-map";
import scConfig from "sitecore.config";
import sites from ".sitecore/sites.json";
export const getStaticPaths: GetStaticPaths = async (context) => {
  let paths: StaticPath[] = [];
  let fallback: boolean | "blocking" = "blocking";
  if (
    process.env.NODE_ENV !== "development" &&
    scConfig.generateStaticPaths
  ) {
    try {
      paths = await client.getPagePaths(
        sites.map((site: SiteInfo) => site.name),
        context?.locales || []
      );
    } catch (error) {
      console.log("Error occurred while fetching static paths");
      console.log(error);
    }
    fallback = process.env.EXPORT_MODE ? false : fallback;
  }
  return { paths, fallback };
};
export const getStaticProps: GetStaticProps = async (context) => {
  let props = {};
  const path = extractPath(context);
  let page;
  if (context.preview && isDesignLibraryPreviewData(context.previewData)) {
    page = await client.getDesignLibraryData(context.previewData);
  } else {
    page = context.preview
      ? await client.getPreview(context.previewData)
      : await client.getPage(path, { locale: context.locale });
  }
  if (page) {
    props = {
      page,
      dictionary: await client.getDictionary({
        site: page.siteName,
        locale: page.locale,
      }),
      componentProps: await client.getComponentData(
        page.layout,
        context,
        components
      ),
    };
  }
  return {
    props,
    revalidate: 5, // ISR: regenerate at most every 5 seconds
    notFound: !page,
  };
};

The key method here is getDictionary({ site, locale }) — Sitecore Content SDK calls the Edge endpoint with a GraphQL query that fetches all dictionary entries for the given site and locale. The response is flattened into a Record<string, string> where keys are the Key field values and values are the Phrase field values. You don't write this query yourself — the SDK handles it — but knowing this helps when debugging missing translations in the Edge delivery network.

For an English request, getDictionary() returns:

{
  "Home": "Home",
  "Search": "Search",
  "Read More": "Read More",
  "Back to": "Back to"
}

For a French-Canada request:

{
  "Home": "Accueil",
  "Search": "Rechercher",
  "Read More": "En savoir plus",
  "Back to": "Retour à"
}

4B. Injecting into React context

The dictionary is now in page props. To make it accessible everywhere without prop-drilling, wrap the application with a translation provider in _app.tsx:

// src/pages/_app.tsx
import type { AppProps } from "next/app";
import { I18nProvider } from "next-localization";
import { SitecorePageProps } from "@sitecore-content-sdk/nextjs";
import scConfig from "sitecore.config";
function App({
  Component,
  pageProps,
}: AppProps<SitecorePageProps>): JSX.Element {
  const { dictionary, ...rest } = pageProps;
  return (
    <I18nProvider
      lngDict={dictionary}
      locale={pageProps.page?.locale || scConfig.defaultLanguage}
    >
      <Component {...rest} />
    </I18nProvider>
  );
}
export default App;

The next-localization package stores the flat dictionary in React context and exposes a useI18n() hook for lookups. Like the dictionary fetching in Step 4A, this provider is already wired up in all Content SDK Next.js Pages Router sample apps and starter kits.

Step 5: Building a robust translation hook

Calling useI18n() directly in every component means duplicating error handling and coupling your entire codebase to a specific library. A thin custom hook solves both:

// src/lib/hooks/useTranslation.tsx
import { useI18n } from "next-localization";
export const useTranslation = () => {
  const i18nResult = useI18n();
  const translate = (key: string): string => {
    if (!i18nResult || typeof i18nResult.t !== "function") {
      console.warn(
        `Translation warning: i18n context not available. Returning key "${key}" as fallback.`
      );
      return key;
    }
    const result = i18nResult.t(key);
    if (result === "" || typeof result !== "string") {
      console.warn(
        `Translation warning: No translation found for key "${key}" in the dictionary.`
      );
      return key;
    }
    return result;
  };
  return { t: translate };
};

Why this wrapper matters:

Benefit Without wrapper With wrapper
Missing key Empty string or undefined Returns the English key as fallback
Missing provider Runtime crash Console warning + fallback
Library swap Update every component Change one file
Debugging Silent failure Console warnings pinpoint the key

Step 6: Using translations in components

The infrastructure is in place. Here's where it pays off: five patterns for consuming dictionary keys in real components, covering the vast majority of production translation scenarios.

Pattern 1: Simple text content

The most straightforward use — rendering a translated label:

const { t } = useTranslation();
return <span>{t("Home")}</span>
// en: "Home" | fr-CA: "Accueil"

Pattern 2: Accessibility and form attributes

Translations aren't just visual — screen readers need them too, and form placeholders must match the user's language. Untranslated aria-label or placeholder values mean a broken experience for non-English users:

<Link aria-label={t("Home")} href="/">
  <HomeIcon />
</Link>
<button aria-label={t("Close search modal")} onClick={closeModal}>
  {t("Close")} <IconFas icon="xmark" variant="white" />
</button>
<input
  type="search"
  aria-label={t("Search site content")}
  placeholder={t("Search site content")}
/>

Pattern 3: Template literals with dynamic content

Combine translated fragments with dynamic CMS values:

<a aria-label={`${t('Back to')} ${previousPageName}`}>
  <span>{previousPageName}</span>
</a>

Here, "Back to" is translated via the dictionary, while previousPageName comes from the Sitecore content tree (already language-versioned via displayName).

Pattern 4: Indirect keys via lookup maps

Not all dictionary keys appear as literal t('...') calls. When a key is determined at runtime (for example, by mapping a page's Sitecore template ID to a content type label) searching the codebase for t('Article') returns zero results, even though t() receives 'Article' at runtime:

// src/lib/graphql/id.ts — Sitecore template GUIDs
export const ARTICLE_TEMPLATE_ID = '{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}';
export const NEWS_TEMPLATE_ID = '{YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY}';
export const INSIGHTS_TEMPLATE_ID = '{ZZZZZZZZ-ZZZZ-ZZZZ-ZZZZ-ZZZZZZZZZZZZ}';
// src/component-children/Foundation/BasePage/Metadata.tsx
const CONTENT_TYPE_MAP: { [key: string]: string } = {
  [ARTICLE_TEMPLATE_ID]: 'Article',    // t('Article')
  [NEWS_TEMPLATE_ID]: 'News',          // t('News')
  [INSIGHTS_TEMPLATE_ID]: 'Insight',   // t('Insight')
};
const contentTypeDictionaryKey = CONTENT_TYPE_MAP[templateId] || 'Generic Page';    // t('Generic Page')
const contentType = t(contentTypeDictionaryKey);

Pattern 5: Type-safe dictionary keys (recommended)

As your dictionary grows past a few dozen entries, raw string keys become a liability — typos are silent failures. A constants file provides autocomplete, catches errors at build time, and serves as living documentation of every key the front end depends on:

// src/lib/consts/dictionary-keys.ts
export const dictionaryKeys = {
  HOME: 'Home',
  SEARCH: 'Search',
  SEARCH_SITE_CONTENT: 'Search site content',
  SUBMIT_SEARCH: 'Submit search',
  READ_MORE: 'Read More',
  BACK_TO: 'Back to',
  NO_RESULTS: 'No results',
  CLOSE: 'Close',
  CLOSE_SEARCH_MODAL: 'Close search modal',
  OPEN: 'Open',
  TOP: 'Top',
  PAGINATION: 'Pagination',
  // Content types (indirect keys — used via CONTENT_TYPE_MAP)
  ARTICLE: 'Article',               // t('Article')
  NEWS: 'News',                     // t('News')
  INSIGHT: 'Insight',               // t('Insight')
  GENERIC_PAGE: 'Generic Page',     // t('Generic Page')
} as const;
export type DictionaryKey = (typeof dictionaryKeys)[keyof typeof dictionaryKeys];

Then in components:

import { dictionaryKeys } from "lib/consts/dictionary-keys";
const { t } = useTranslation();
<span>{t(dictionaryKeys.HOME)}</span>
<input placeholder={t(dictionaryKeys.SEARCH_SITE_CONTENT)} />
<button>{t(dictionaryKeys.READ_MORE)}</button>

Benefits: 

  • Autocomplete - Your editor suggests keys as you type, eliminating guesswork
  • Refactoring - Renaming a key updates every usage via "Rename Symbol"
  • Documentation - The constants file is a single-file inventory of every dictionary dependency
  • Auditing - Comparing this file against Sitecore becomes trivial

Common pitfalls and how to avoid them

These are the issues that come up repeatedly in production SitecoreAI projects. Each one is a silent failure. The app doesn't crash, it just shows the wrong language.

1. Key mismatch between code and CMS

Symptom: French users see English text for some strings.

Cause: The t() key in code doesn't exactly match the Key field in Sitecore.

Code:     t('Filtering by tags:')
Sitecore: Key = "Filtering by tag(s):"

Fix: Establish a convention that the Key field is the exact English phrase used in code. Periodically audit by comparing t() calls in the codebase against serialized dictionary YAML files.

2. Missing language versions

Symptom: A specific locale falls back to the key string while others work.

Cause: The dictionary item exists but has no Phrase for that language.

Fix: When creating dictionary items, always create Phrase values for all supported languages before publishing. Use the SXA "Add Site Language" script or Content Import/Export for bulk operations.

3. Forgetting dictionary fetch on error pages

Symptom: 404 and 500 pages show untranslated UI.

Cause: getDictionary() wasn't called in the error page's data fetching.

Fix: Apply the same getStaticProps pattern from Step 4 to your 404.tsx and 500.tsx pages. Ensure getDictionary() is called with the correct site and locale so the translation provider has data to work with, even on error pages.

4. Hardcoded strings in components

Symptom: Strings display in English regardless of locale.

Cause: A developer wrote "Read More" instead of t('Read More').

Fix: Add a code review checklist item for string externalization. Consider ESLint rules that flag string literals in JSX (e.g., eslint-plugin-i18next).

Final thoughts on Sitecore Dictionary

The Sitecore Dictionary bridges the gap between content-managed translations and developer-authored UI. By following the full pipeline — from configuring languages in the Sitecore portal, to creating dictionary items with proper language versions, to fetching and rendering translations in Next.js — you get a system where each role operates independently:

  • Content authors manage translations in a familiar CMS interface without waiting for code deployments
  • Developers add new keys in code without needing CMS access for every string change
  • Users see a fully translated experience — not just page content, but every button label, placeholder, and screen reader attribute
  • The application degrades gracefully when translations are missing, showing the English key rather than a blank or a crash

None of this is particularly complex on its own. The challenge — and where most teams stumble — is consistency. A dictionary key typo in one component, a missing French phrase on one entry, a forgotten getDictionary() call on the 404 page. These small gaps compound into a degraded multilingual experience.

Technology stack reference

Technology Role Documentation
SitecoreAI CMS with Dictionary item support SitecoreAI docs
Sitecore Content SDK getDictionary() API for fetching translations @sitecore-content-sdk/nextjs
next-localization React i18n provider npm: next-localization
Next.js i18n Routing Locale-prefixed URL routing Next.js i18n docs
Sitecore CLI / SCS Serializing dictionary items to YAML Sitecore CLI docs

References