skills/pimcore-studio-ui-i18n/SKILL.md
Internationalization and translation system in Pimcore Studio UI - useTranslation hook, translation keys, and localization
npx skillsauth add pimcore/skills pimcore-studio-ui-i18nInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Internationalization (i18n) and translation system in Pimcore Studio UI:
Use this when:
BEFORE writing any import, read CRITICAL-IMPORT-PATHS.md.
All examples below use bundle imports (@pimcore/studio-ui-bundle/*). For core development (@sdk/*, @Pimcore/*), see the referenced file.
Note:
useTranslationis re-exported from@pimcore/studio-ui-bundle/app. Core code can also import directly fromreact-i18next. The hook at@Pimcore/modules/translations/hooks/use-translationis a different hook for CRUD operations on translation entities.
The useTranslation hook provides access to the translation function and current language.
import { useTranslation } from '@pimcore/studio-ui-bundle/app'
export const MyComponent = () => {
const { t } = useTranslation()
return (
<div>
<h1>{t('my-feature.title')}</h1>
<Button>{t('save')}</Button>
</div>
)
}
const { t, i18n } = useTranslation()
// t - Translation function
// i18n - i18next instance (rarely needed directly)
ALWAYS use flat dot notation in YAML files. NEVER use nested structures!
# ✅ DO - Flat dot notation (CORRECT)
personalization.target-groups: Target Groups
personalization.target-group.add: Add Target Group
personalization.target-group.update.success: Target group updated successfully
personalization.target-group.update.error: Failed to update target group
personalization.target-group.configuration.name: Name
personalization.target-group.configuration.description: Description
# ❌ DON'T - Nested structure (WRONG!)
personalization:
target-groups: Target Groups
target-group:
add: Add Target Group
update:
success: Target group updated successfully
Every translation key MUST have an English translation in studio.en.yaml!
studio.en.yaml in your bundlet('translation.key')// ❌ DON'T - Hardcoded text
<Button>Save</Button>
<h1>Target Groups</h1>
// ✅ DO - Always use translation keys
<Button>{t('save')}</Button>
<h1>{t('personalization.target-groups')}</h1>
Translation keys follow a hierarchical structure using dot notation:
{bundle}.{module}.{component}.{specific-key}
This is the recommended convention for new bundles. However, some existing bundles use different conventions (e.g., snake_case, camelCase). When working with an existing bundle, follow its existing convention rather than imposing this pattern.
# File: studio-ui-bundle/translations/studio.en.yaml
# Common actions
save: Save
delete: Delete
cancel: Cancel
refresh: Refresh
new: New
search: Search
# Form labels
form.label.new-item: New Item
form.validation.required: This field is required
# Navigation
navigation.quick-access: Quick Access
navigation.data-management: Data Management
# Element operations
element.delete.confirmation.title: Delete Element
element.delete.confirmation.text: Are you sure you want to delete this element?
element.tree.copy-success-description: '{{elementType}} "{{name}}" copied to clipboard'
# Toolbar
toolbar.save: Save
toolbar.publish: Publish
# File: personalization-bundle/translations/studio.en.yaml
# Bundle-specific keys (FLAT DOT NOTATION)
personalization: Personalisation / Targeting
personalization.target-groups: Target Groups
personalization.target-group.add: Add Target Group
personalization.target-group.update.success: Target group updated successfully
personalization.target-group.update.error: Failed to update target group
personalization.target-group.delete.success: Target group deleted successfully
personalization.target-group.validation.message: Only alphanumeric characters, hyphens and underscores allowed
personalization.target-group.configuration.name: Name
personalization.target-group.configuration.description: Description
personalization.target-group.configuration.threshold: Threshold
personalization.target-group.configuration.active: Active
personalization.target-group.general-settings: General Settings
# Targeting rules
personalization.targeting-rules.navigation.title: Global Targeting Rules
studio-ui-bundle/translations/studio.{locale}.yaml
Example: studio-ui-bundle/translations/studio.en.yaml
your-bundle/translations/studio.{locale}.yaml
Example: personalization-bundle/translations/studio.en.yaml
By default, ONLY work with English translations (studio.en.yaml)!
studio.{locale}.yaml (e.g., studio.de.yaml, studio.fr.yaml)Translation files use YAML format with flat dot notation:
# ✅ CORRECT - Flat dot notation
save: Save
delete: Delete
personalization.target-groups: Target Groups
personalization.target-group.add: Add Target Group
personalization.target-group.update.success: Target group updated successfully
personalization.target-group.update.error: Failed to update target group
personalization.target-group.configuration.name: Name
personalization.target-group.configuration.description: Description
# With interpolation placeholders
element.tree.copy-success-description: '{{elementType}} "{{name}}" copied to clipboard'
# Pluralization (i18next v23 uses _one/_other suffixes)
notification.items-selected_one: '{{count}} item selected'
notification.items-selected_other: '{{count}} items selected'
# Multi-line text (use pipe for multi-line)
help.description: |
This is a longer description
that spans multiple lines.
# ❌ WRONG - Never use nested structure!
# personalization:
# target-groups: Target Groups
// Translation key:
// element.tree.copy-success-description: '{{elementType}} "{{name}}" copied to clipboard'
const { t } = useTranslation()
const message = t('element.tree.copy-success-description', {
elementType: 'Document',
name: 'Homepage'
})
// Result: 'Document "Homepage" copied to clipboard'
// File: element/actions/copy-paste/tree-copy-paste-context.tsx
const messageApi = useMessage()
const { t } = useTranslation()
const copyNode = useCallback((node: TreeNodeProps, elementType: ElementType): void => {
void messageApi.success(t('element.tree.copy-success-description', {
elementType: t(elementType), // Translate element type
name: getNodeName(node),
interpolation: { escapeValue: false } // Don't escape HTML
}))
}, [messageApi, t])
// When translation contains HTML, disable escaping
const { t } = useTranslation()
const description = t('my-feature.description', {
link: '<a href="#">Click here</a>',
interpolation: { escapeValue: false }
})
# Singular form (i18next v23 uses _one/_other suffixes)
item-selected_one: '{{count}} item selected'
# Plural form (append _other, NOT _plural)
item-selected_other: '{{count}} items selected'
const { t } = useTranslation()
const selectedCount = 5
const message = t('item-selected', { count: selectedCount })
// count = 1: "1 item selected"
// count > 1: "5 items selected"
// Pagination total display
<Pagination
showTotal={(total) => t('pagination.show-total', { total })}
/>
// Translation keys:
// pagination.show-total_one: '{{total}} item'
// pagination.show-total_other: '{{total}} items'
import { Form, Input } from '@pimcore/studio-ui-bundle/components'
import { useTranslation } from '@pimcore/studio-ui-bundle/app'
export const MyForm = () => {
const { t } = useTranslation()
return (
<Form>
<Form.Item
label={t('form.label.name')}
name="name"
rules={[
{ required: true, message: t('form.validation.required') }
]}
>
<Input placeholder={t('form.placeholder.enter-name')} />
</Form.Item>
</Form>
)
}
import { Button } from '@pimcore/studio-ui-bundle/components'
import { useTranslation } from '@pimcore/studio-ui-bundle/app'
export const Toolbar = () => {
const { t } = useTranslation()
return (
<>
<Button onClick={handleCancel}>{t('cancel')}</Button>
<Button type="primary" onClick={handleSave}>{t('save')}</Button>
</>
)
}
import { useFormModal } from '@pimcore/studio-ui-bundle/components'
import { useTranslation } from '@pimcore/studio-ui-bundle/app'
export const useAddItem = () => {
const modal = useFormModal()
const { t } = useTranslation()
const handleAdd = () => {
modal.input({
title: t('my-feature.add-item.title'),
label: t('my-feature.add-item.label'),
rule: {
required: true,
message: t('form.validation.required')
},
onOk: async (value) => {
// Handle add
}
})
}
return { handleAdd }
}
import { useFormModal } from '@pimcore/studio-ui-bundle/components'
import { useTranslation } from '@pimcore/studio-ui-bundle/app'
export const useDeleteItem = () => {
const modal = useFormModal()
const { t } = useTranslation()
const handleDelete = (itemName: string) => {
modal.confirm({
title: t('my-feature.delete.confirmation.title'),
content: (
<>
<span>{t('my-feature.delete.confirmation.text')}</span>
<br />
<b>{itemName}</b>
</>
),
okText: t('delete'),
cancelText: t('cancel'),
onOk: async () => {
// Handle delete
}
})
}
return { handleDelete }
}
import { useMessage } from '@pimcore/studio-ui-bundle/components'
import { useTranslation } from '@pimcore/studio-ui-bundle/app'
export const useSaveItem = () => {
const messageApi = useMessage()
const { t } = useTranslation()
const handleSave = async () => {
try {
await saveItem()
void messageApi.success(t('my-feature.save.success'))
} catch (error) {
void messageApi.error(t('my-feature.save.error'))
}
}
return { handleSave }
}
import { createColumnHelper } from '@tanstack/react-table'
import { useTranslation } from '@pimcore/studio-ui-bundle/app'
export const useColumns = () => {
const { t } = useTranslation()
const columnHelper = createColumnHelper<DataType>()
return [
columnHelper.accessor('name', {
header: t('my-feature.columns.name')
}),
columnHelper.accessor('status', {
header: t('my-feature.columns.status')
})
]
}
// In module initialization
const mainNavRegistry = container.get<MainNavRegistry>(serviceIds.mainNavRegistry)
mainNavRegistry.registerMainNavItem({
path: 'MyGroup/MyFeature',
label: 'my-feature.navigation.title', // Translation key
order: 5,
widgetConfig: {
name: 'My Feature',
id: 'my-feature',
component: 'my-feature',
config: {
translationKey: 'my-feature.navigation.title', // Translation key
icon: {
type: 'name',
value: 'my-icon'
}
}
}
})
// File: personalization-bundle/.../target-group-detail.tsx
import { Form, Input, TextArea, InputNumber, Switch } from '@pimcore/studio-ui-bundle/components'
import { useTranslation } from '@pimcore/studio-ui-bundle/app'
export const TargetGroupDetail = ({ id }: { id: number }) => {
const { t } = useTranslation()
const [form] = Form.useForm()
return (
<Form form={form} layout="vertical">
<Panel title={t('personalization.target-group.general-settings')}>
<Form.Item
label={t('personalization.target-group.configuration.name')}
name="name"
>
<Input
disabled
placeholder={t('personalization.target-group.configuration.name')}
/>
</Form.Item>
<Form.Item
label={t('personalization.target-group.configuration.description')}
name="description"
>
<TextArea
placeholder={t('personalization.target-group.configuration.description')}
rows={4}
/>
</Form.Item>
<Form.Item
label={t('personalization.target-group.configuration.threshold')}
name="threshold"
>
<InputNumber />
</Form.Item>
<Form.Item
name="active"
valuePropName="checked"
>
<Switch
labelRight={t('personalization.target-group.configuration.active')}
/>
</Form.Item>
</Panel>
</Form>
)
}
NEVER hardcode user-facing text. ALWAYS use translation keys!
// ❌ DON'T - Hardcoded text (NEVER DO THIS!)
<Button>Save</Button>
<h1>Settings</h1>
<p>Are you sure you want to delete this item?</p>
// ✅ DO - Always use translation keys
const { t } = useTranslation()
<Button>{t('save')}</Button>
<h1>{t('settings.title')}</h1>
<p>{t('delete.confirmation.text')}</p>
# ❌ DON'T - Nested structure
personalization:
target-groups: Target Groups
target-group:
add: Add Target Group
# ✅ DO - Flat dot notation
personalization.target-groups: Target Groups
personalization.target-group.add: Add Target Group
# ❌ DON'T - Only German translations, no English base
# File: studio.de.yaml
mein-feature.titel: Mein Feature
# ✅ DO - Always have English in studio.en.yaml first
# File: studio.en.yaml
my-feature.title: My Feature
# File: studio.de.yaml
my-feature.title: Mein Feature
// DON'T DO THIS
const message = t('saved') + ' ' + itemName
// DO THIS
const message = t('saved-item', { name: itemName })
// Translation: 'saved-item': 'Saved {{name}} successfully'
// DON'T - Key doesn't exist in translation file
t('non-existent-key') // Shows the key itself
// DO - Always add keys to translation files first
// Then use them in code
t('my-feature.existing-key')
// DON'T - Manual plural handling
const message = count === 1 ? t('item') : t('items')
// DO - Use count parameter (i18next v23 uses _one/_other suffixes)
const message = t('item', { count })
// Translation keys:
// item_one: '{{count}} item'
// item_other: '{{count}} items'
Step 1: ALWAYS Add English First (REQUIRED)
Add translations to studio.en.yaml in your bundle using flat dot notation:
# File: your-bundle/translations/studio.en.yaml
# ✅ CORRECT - Flat dot notation
my-feature.title: My Feature
my-feature.description: This is my feature description
my-feature.add-item.title: Add Item
my-feature.add-item.label: Item Name
my-feature.save.success: Item saved successfully
my-feature.save.error: Failed to save item
# ❌ WRONG - Never use nested structure!
# my-feature:
# title: My Feature
Step 2: Use in Components (Never Hardcode!)
import { useTranslation } from '@pimcore/studio-ui-bundle/app'
export const MyFeature = () => {
const { t } = useTranslation()
return (
<div>
{/* ✅ DO - Always use translation keys */}
<h1>{t('my-feature.title')}</h1>
<p>{t('my-feature.description')}</p>
{/* ❌ DON'T - Never hardcode */}
{/* <h1>My Feature</h1> */}
</div>
)
}
Step 3: Add Other Languages (Optional)
German: studio.de.yaml
# ✅ CORRECT - Flat dot notation, same keys as English
my-feature.title: Meine Funktion
my-feature.description: Dies ist meine Funktionsbeschreibung
my-feature.add-item.title: Element hinzufügen
my-feature.add-item.label: Elementname
# ❌ WRONG - Never use nested structure!
French: studio.fr.yaml
# ✅ CORRECT - Flat dot notation
my-feature.title: Ma Fonctionnalité
my-feature.description: Ceci est ma description de fonctionnalité
studio.en.yaml) is MANDATORY - Must contain all translation keyst('translation.key') in componentstools
UX and UI design conventions for Pimcore Studio - layout, spacing, action labels, writing style, and design principles for consistent extensions
tools
Widget system in Pimcore Studio UI - registering widgets, opening them in layout areas, WidgetManagerTabConfig, and connecting widgets to navigation
tools
How bundles consume the Pimcore Studio UI SDK - plugins, modules, DI, registries, and imports
development
TypeScript coding standards and best practices for Pimcore Studio UI - type safety, null checks, and code quality