Experience Sitecore ! | Dictionary in XM Cloud

Experience Sitecore !

More than 300 articles about the best DXP by Martin Miles

Dictionary in XM Cloud

Localization is more than translation - it's about delivering a seamless, culturally relevant experience for every visitor. In Sitecore XM Cloud, dictionary items provide a simple, robust way to centralize UI text and labels for all languages, directly accessible in your Next.js frontend. Below, I’ll walk through the practical setup, usage patterns, and expert recommendations based on Sitecore’s official documentation and proven community practices.

What Are Dictionary Items?

Dictionary items in XM Cloud let you store and manage all your UI phrases centrally.
Instead of hardcoding "Read More", "Submit",  or error messages, you create dictionary keys under your site’s /Dictionary item. Each key holds localized values for every language you support.

Why:
  • Centralized: Update once, used everywhere
  • Multi-language: Add a new language, all keys covered
  • Consistent: No more stray hardcoded strings

When to Use Dictionary vs. Fields

Typically, you would use dictionary entries for generic text reused in many places (e.g., button labels like "Submit", form field placeholders, system messages). For content that might be specific to a single page or component (e.g., a marketing tagline, section heading, or lengthy description), consider making it a field in a content item or datasource rather than a shared dictionary entry. This gives content authors more flexibility to customize text in context. 

A hybrid approach can work too, for example, use a dictionary phrase as a fallback but allow an override via a field in the CMS if the editor wants a custom message in one place.

How Dictionary Items Work in XM Cloud

In XM Cloud, dictionary items are stored at: /sitecore/content/{collection}/{site}/Dictionary and is fetched via GraphQL through the Layout/Dictionary service. Essentially, for each page request, a GraphQL query runs to get all dictionary entries under the site’s dictionary path for the given language. The Next.js SDK’s DictionaryServiceFactory handles this, so you typically don’t need to write this query yourself. If curious, the query filters by the dictionary root item path, language, and the Dictionary Entry template, and returns all key-phrase pairs. Because this data is pulled at request-time (or build-time for static pages), the translations are ready by the time React components render. No client-side extra fetch is needed for dictionary data. It’s included in the page HTML/props payload.

Each dictionary item represents a key (e.g., "read"), and under it, each language version holds the translated phrase.

For multi-site or SXA setups, you can create a shared dictionary (often in a Shared site or at the tenant root), and then set up a fallback so multiple sites reuse the same dictionary keys. On the integration side, XM Cloud provides a Dictionary Service API, available via REST or GraphQL, which populates the dictionary data at build or runtime for your Next.js app.

Sitecore’s Next.js SDK (part of Headless JSS) comes pre-configured to utilize dictionary data for localization. Here’s how the pieces fit together:

  • Next.js Internationalization (i18n) Settings. In your Next.js app’s next.config.js, define the locales your site supports and a default locale. These locales should match the languages enabled in Sitecore. For example:
    i18n: {
      locales: ['en', 'fr-CA', 'es'], 
      defaultLocale: 'en'
    }
    This allows Next.js to handle locale-specific routes, such as /fr-ca/page-name for French Canadian. The defaultLocale should correspond to your site’s default language, often pulled from a config setting of package.json in the Sitecore starters. If you add a new language to Sitecore, update this config and redeploy the app so Next.js knows about the new locale.
  • Page Props and Dictionary Data. The Sitecore Next.js SDK fetches dictionary entries for each page request (or build) and injects them into the page properties. Specifically, the JSS Page Props Factory (a plugin in the Next.js sample app) uses the Dictionary Service to retrieve all dictionary phrases for the current site/language, and includes them as a dictionary object in pageProps. This happens server-side, either at build time for static generation or on the server for SSR, so that the dictionary is ready by the time the React components render.
  • I18n Provider. The Next.js app uses the next-localization library (a lightweight i18n solution) to provide translations. In the app’s custom _app.tsx, an <I18nProvider> is configured to wrap the page component, initialized with the locale and the dictionary data from Sitecore. For example:
    // In _app.tsx
    import { I18nProvider } from 'next-localization';
    function App({ Component, pageProps }) {
      const { dictionary, locale, ...rest } = pageProps;
      return (
        <I18nProvider lngDict={dictionary} locale={locale}>
          <Component {...rest} />
        </I18nProvider>
      );
    }
    Here, pageProps.dictionary contains all key–phrase pairs for the current locale, and pageProps.locale is the language code. The I18nProvider makes a translation function available to all components via React context.
  • Using the Translation Hook. Inside your React components, you use the useI18n() hook (from next-localization) to access the localization context. See the below paragraph for the exact dictionary consumption code sample.

The Single useTranslation Hook Pattern (with Fallback Defaults)

To keep things tidy, many teams prefer a single useTranslations hook. Here’s a pattern that ensures you never show a blank label, even if the dictionary entry is missing, by supplying a fallback default:

import { useI18n } from 'next-localization';

export function useTranslation() {
  const i18n = useI18n();
  // Utility: always show a fallback if dictionary value is missing
  const getTranslation = (key, fallback) => i18n?.t(key) || fallback;

  return {
    readLabel: getTranslation('read', 'Read'),
    submitLabel: getTranslation('submit', 'Submit'),
    // Add more keys as needed
  };
}

// Usage in a component
const translations = useTranslation();

return (
  <div>
    <button>{translations.readLabel}</button>
  </div>
);

Why this is better:

  • No undefined or blank UI if a dictionary key is missing
  • All translations accessible via one hook - simple, scalable
  • Easy to add more keys as your UI grows

Using Shared SXA Dictionaries & Fallback Domains

If you’re using SXA or multiple sites, you can set up a shared dictionary and configure fallback so every site can leverage the same keys.

Setup Steps:

  1. Create a Shared Dictionary. Place it in a shared location, such as /sitecore/content/{collection}/Shared/Dictionary
  2. Set Dictionary Domain. In Sitecore, under your site’s Dictionary item, set the Dictionary Domain field to the shared dictionary’s path. (Official Doc)
  3. Configure Fallback. On your site’s dictionary, enable "fallback domain" to point to the shared dictionary.

In your Next.js app, the Dictionary Service will automatically merge keys from both the site and shared domains. 

Storybook: Mocking Dictionaries for Isolated UI Development

When using Storybook, components expecting dictionary values may break outside the app context. To avoid this, you can mock the next-localization context in .storybook/preview.tsx:

import { I18nProvider } from 'next-localization';

const mockDictionary = {
  read: 'Read',
  submit: 'Submit',
  // Add other keys as needed
};

export const decorators = [
  (Story) => (
    <I18nProvider lngDict={mockDictionary} locale="en">
      <Story />
    </I18nProvider>
  ),
];
Why this matters:
  • Ensures all components render correctly in isolation
  • Lets designers and QA see accurate UI text in stories
  • Helps catch missing dictionary keys before production

Troubleshooting Dictionary Issues

If you encounter an issue where dictionary translations are not appearing in the Next.js app, it is highly likely that it happens because of one of the very frequently missed things. Consider the following checks:
  • Verify Config and App Name. Make sure the Next.js app’s configured site name (app name) matches the Sitecore site name. This is typically set in your app’s package.json (config.appName) or .env and used by the JSS config. If it mismatches Sitecore’s expected name, the dictionary service might look in the wrong place. In XM Cloud projects, the app name is usually the same as your site name in Sitecore.
  • Check Dictionary Domain Path. As noted, ensure the site’s dictionary domain/path settings in Sitecore are correct. If the Dictionary item was renamed or moved, update the site settings or the config patch so the head application knows where to fetch phrases.
  • Publish Content. Confirm that the dictionary items are published to the target environment (Edge). In a development scenario, if using Connected Mode, ensure the dictionary items exist in the expected database. After editing dictionary entries in the Pages or Content Editor interface, run a publish. Unpublished items will not appear in the GraphQL results.
  • For preview mode issues, rebuild the search index. On XM Cloud, indexes are auto-rebuilt on deploy, but if you import items or restore content, an index rebuild might be necessary.
  • Ensure a valid site root is defined. If you see errors like “Valid value for rootItemId not provided” in logs or GraphQL responses, it indicates the dictionary GraphQL service couldn’t auto-resolve the site’s root. Ensure your site template is correct; if fixing it, you can remove any hardcoded rootItemId overrides you may have added during debugging.

Bonus: Dictionary Migration and Conversion SPE Script

The below script copies dictionary items from an old XP database (referred to as old) to the master database of the locally-run XM Cloud environment and also converts Dictionary entries from a custom format of the Habitat website (which many folks have used as a starter kit, regardless) to the XM Cloud format, which is hard-defined by a dictionary template coming from the items resource file:
param(
    [string]$SourceDatabase = "old",
    [string]$SourcePath = "/sitecore/content/Habitat/Global/Dictionary",
    [string]$TargetDatabase = "master",
    [string]$TargetPath = "/sitecore/content/Zont/Habitat/Dictionary"
)

$srcFolderTemplate = "{98E4BDC6-9B43-4EB2-BAA3-D4303C35852E}"
$srcEntryTemplate = "{EC4DD3F2-590D-404B-9189-2A12679749CC}"
$srcPhraseField = "{DDACDD55-5B08-405F-9E58-04F09AED640A}"

$trgFolderTemplate = "{267D9AC7-5D85-4E9D-AF89-99AB296CC218}"
$trgEntryTemplate = "{6D1CD897-1936-4A3A-A511-289A94C2A7B1}"
$trgKeyField = "{580C75A8-C01A-4580-83CB-987776CEB3AF}"
$trgPhraseField = "{2BA3454A-9A9C-4CDF-A9F8-107FD484EB6E}"

function Copy-DictionaryItemsRecursive {
    param(
        [Sitecore.Data.Items.Item]$SrcParent,
        [Sitecore.Data.Items.Item]$TrgParent,
        [Sitecore.Data.Database]$SrcDB,
        [Sitecore.Data.Database]$TrgDB,
        [int]$Depth = 0
    )

    $indent = (" " * ($Depth * 2))
    $children = $SrcParent.GetChildren()
    Write-Output "$indent- Processing source: $($SrcParent.Paths.FullPath) | Child count: $($children.Count)"

    foreach ($child in $children) {
        Write-Output "$indent  - Processing child: $($child.Paths.FullPath) | TemplateID: $($child.TemplateID) | Name: $($child.Name)"
        $templateId = $child.TemplateID.ToString()
        $newItemName = $child.Name
        $newItem = $null

        try {
            # Check if this item already exists by name under TrgParent
            $existingByName = $TrgParent.Children | Where-Object { $_.Name -eq $newItemName }
            if ($existingByName) {
                Write-Output "$indent    - Using existing target item (by name): $($existingByName.Paths.FullPath)"
                $newItem = $existingByName
            } else {
                # Prepare template item
                $templateItem = $null
                if ($templateId -eq $srcFolderTemplate) {
                    $templateItem = $TrgDB.GetTemplate($trgFolderTemplate)
                    Write-Output "$indent    - Using target template (folder): $($templateItem -ne $null)"
                } elseif ($templateId -eq $srcEntryTemplate) {
                    $templateItem = $TrgDB.GetTemplate($trgEntryTemplate)
                    Write-Output "$indent    - Using target template (entry): $($templateItem -ne $null)"
                }
                if (-not $templateItem) { throw "$indent    - ERROR: Target template not found for $newItemName" }

                # Attempt to create under the current TrgParent
                Write-Output "$indent    - Attempting to Add '$newItemName' under $($TrgParent.Paths.FullPath)"
                $newItem = $TrgParent.Add($newItemName, $templateItem)
                if (-not $newItem) { throw "$indent    - ERROR: Failed to add item $newItemName under $($TrgParent.Paths.FullPath)" }
                Write-Output "$indent    - Created item: $($newItem.Paths.FullPath) [ID: $($newItem.ID)]"
            }

            # Recurse or fill entry fields
            if ($templateId -eq $srcFolderTemplate) {
                Write-Output "$indent    - Recursing into folder: $($child.Paths.FullPath)"
                Copy-DictionaryItemsRecursive -SrcParent $child -TrgParent $newItem -SrcDB $SrcDB -TrgDB $TrgDB -Depth ($Depth + 1)
            } elseif ($templateId -eq $srcEntryTemplate) {
                foreach ($lang in $child.Languages) {
                    $srcItemLang = $SrcDB.GetItem($child.ID, $lang)
                    if (-not $srcItemLang) { throw "$indent    - ERROR: Source item language $lang missing for $($child.Paths.FullPath)" }
                    $trgItemLang = $TrgDB.GetItem($newItem.ID, $lang)
                    if (-not $trgItemLang) {
                        $trgItemLang = $newItem.Versions.AddVersion($lang)
                        if (-not $trgItemLang) { throw "$indent    - ERROR: Failed to add language version $lang to $($newItem.Paths.FullPath)" }
                    }
                    $phraseValue = $srcItemLang.Fields[$srcPhraseField].Value
                    $trgItemLang.Editing.BeginEdit()
                    $trgItemLang.Fields[$trgKeyField].Value = $newItemName
                    $trgItemLang.Fields[$trgPhraseField].Value = $phraseValue
                    $trgItemLang.Editing.EndEdit()
                    Write-Output "$indent    - Populated entry ($lang): $($newItem.Paths.FullPath) [ID: $($newItem.ID)]"
                }
            } else {
                Write-Output "$indent    - Skipping unsupported template: $($child.Name) [$templateId]"
            }
        } catch {
            Write-Output "$indent    - ERROR: $($_.Exception.Message) at item $($child.Paths.FullPath)"
            throw
        }
    }
}

try {
    $srcDB = Get-Database $SourceDatabase
    $trgDB = Get-Database $TargetDatabase
    $srcParent = $srcDB.GetItem($SourcePath)
    $trgParent = $trgDB.GetItem($TargetPath)
    Write-Output "Start: Copying dictionary items from $SourcePath [$SourceDatabase] to $TargetPath [$TargetDatabase] ..."
    if (-not $srcParent) { throw "Source parent item not found: $SourcePath in DB: $SourceDatabase" }
    if (-not $trgParent) { throw "Target parent item not found: $TargetPath in DB: $TargetDatabase" }

    Copy-DictionaryItemsRecursive -SrcParent $srcParent -TrgParent $trgParent -SrcDB $srcDB -TrgDB $trgDB -Depth 0
    Write-Output "Dictionary copy completed successfully."
} catch {
    Write-Output "SCRIPT ERROR: $($_.Exception.Message)"
    exit 1
}

Summary & Best Practices

  • Use a single, type-safe useTranslation hook for clarity and safety
  • Always supply fallback defaults to prevent UI errors
  • Prefer shared dictionaries for multi-site projects? Configure fallback domains in Sitecore for reusability
  • Mock dictionary data in Storybook to maintain robust component previews

Hope you find this post helpful!

Comments are closed