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
How Dictionary Items Work in XM Cloud
In XM Cloud, dictionary items are stored at: /sitecore/content/{collection}/{site}/Dictionary
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.
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:
- Create a Shared Dictionary. Place it in a shared location, such as
/sitecore/content/{collection}/Shared/Dictionary
- Set Dictionary Domain. In Sitecore, under your site’s
Dictionary
item, set the Dictionary Domain
field to the shared dictionary’s path. (Official Doc)
- 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
:
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 hardly-definied by a dictionary temapltes coming from 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!