writing-i18n
npx skills add commercetools/nimbus --skill writing-i18nWriting i18n Skill
You are a Nimbus internationalization specialist. This skill helps you create,
update, or validate i18n files ({component}.i18n.ts) that define translatable
message definitions for accessible, user-facing text.
Note: If you're creating a NEW component, consider using
/propose-component instead. This skill is for:
- Updating existing i18n files
- Adding translations when you know exactly what's needed
- Being invoked by higher-level commands or agents
Critical Requirements
i18n files ensure accessible, translatable UI text. Every component with user-facing text or accessibility labels MUST have properly structured message definitions for Transifex integration.
Mode Detection
Parse the request to determine the operation:
- create - Generate new i18n message definitions
- update - Add new messages, enhance existing definitions
- validate - Check i18n file compliance with guidelines
If no mode is specified, default to create.
Required Research (All Modes)
Before implementation, you MUST research in parallel:
- Read i18n guidelines:
cat docs/file-type-guidelines/i18n.md - Read naming conventions for message IDs:
cat docs/naming-conventions.md
Message ID pattern: Nimbus.{ComponentName}.{messageKey} - Review component implementation to identify translatable text:
cat packages/nimbus/src/components/{component}/{component}.tsx - Check similar i18n files:
ls packages/nimbus/src/components/*/*.i18n.ts
When i18n Is Needed (Decision Flow)
Use this diagram to determine if your component needs i18n:
graph TD
Start[Analyze Component] --> Q1{Sets default<br/>aria-label or<br/>aria-description?}
Q1 -->|Yes| Need1[✅ i18n REQUIRED<br/>Accessibility labels<br/>need translation]
Q1 -->|No| Q2{Has default<br/>placeholder or<br/>helper text?}
Q2 -->|Yes| Need2[✅ i18n REQUIRED<br/>User-facing text<br/>needs translation]
Q2 -->|No| Q3{Has interactive<br/>controls with<br/>default labels?}
Q3 -->|Yes| Need3[✅ i18n REQUIRED<br/>Button/control labels<br/>need translation]
Q3 -->|No| Q4{Pure layout/<br/>styling component?}
Q4 -->|Yes| NotNeeded[❌ i18n NOT NEEDED<br/>Box, Stack, Grid, Card]
Q4 -->|No| ConsumerProvides[❌ i18n NOT NEEDED<br/>Consumer provides all text]
Components That NEED i18n:
Has default accessibility labels:
- Components setting default aria-label
- Components with aria-description
- Interactive elements with screen reader text
Has default user-facing text:
- Default placeholder text
- Button labels (increment, decrement, close, etc.)
- Helper text or instructions
- Tooltips or user-facing messages
Examples:
- NumberInput (increment/decrement labels)
- Alert (dismiss button label)
- DatePicker (input placeholders, button labels)
- PasswordInput (show/hide password label)
Components That DON'T Need i18n:
Pure layout/styling:
- Box, Stack, Grid, Flex
- Card (unless sets default aria-label)
- Divider, Separator
Consumer provides all text:
- Badge (no default aria-label)
- Button (consumer provides label)
- Text, Heading (consumer provides content)
Decision Flow
Does component set default aria-label or aria-description?
├─ YES → i18n REQUIRED
└─ NO → Does component have default placeholder or helper text?
├─ YES → i18n REQUIRED
└─ NO → Does component have interactive controls with labels?
├─ YES → i18n REQUIRED
└─ NO → i18n NOT NEEDED
File Structure
Location
src/components/{component-name}/{component-name}.i18n.ts
Basic Template
export const messages = {
messageKey: {
id: "Nimbus.ComponentName.messageKey",
description:
"Context for translators explaining where and how this is used",
defaultMessage: "English fallback text",
},
// Additional messages...
};
Message ID Convention (CRITICAL)
All message IDs MUST follow this hierarchical pattern:
Nimbus.{ComponentName}.{messageKey}
Standard Examples
Nimbus.Alert.dismissNimbus.DatePicker.clearInputNimbus.NumberInput.incrementNimbus.PasswordInput.show
Nested Components
For complex components with sub-parts:
Nimbus.{ComponentName}.{SubComponent}.{messageKey}
Example: Nimbus.DatePicker.Time.enterTimeHour
Naming Rules
- ComponentName: PascalCase (Alert, NumberInput, DatePicker)
- SubComponent: PascalCase if nested (Time, Calendar)
- messageKey: camelCase describing the message (dismiss, increment, clearInput)
Message Properties (REQUIRED)
Each message MUST include all three properties:
1. id (Required)
Unique identifier following naming convention:
id: "Nimbus.ComponentName.messageKey";
2. description (Required)
Developer-facing context for translators:
description: "aria-label for dismiss button in alert";
description: "Placeholder text for date input field";
description: "Button label to increment numeric value";
Good descriptions:
- Explain WHERE the text appears
- Explain HOW it's used (aria-label, button text, placeholder)
- Provide context about the interaction
Bad descriptions:
- Too vague: "A label"
- Missing context: "Dismiss"
- No usage info: "Text"
3. defaultMessage (Required)
English fallback text:
defaultMessage: "Dismiss";
defaultMessage: "Select date";
defaultMessage: "Increment value";
Requirements:
- Proper grammar and punctuation
- Concise and clear
- User-friendly language
- No technical jargon
Message Patterns by Use Case
Accessibility Labels
export const messages = {
increment: {
id: "Nimbus.NumberInput.increment",
description: "aria-label for increment button in number input",
defaultMessage: "Increment",
},
decrement: {
id: "Nimbus.NumberInput.decrement",
description: "aria-label for decrement button in number input",
defaultMessage: "Decrement",
},
});
Button Labels
export const messages = {
dismiss: {
id: "Nimbus.Alert.dismiss",
description: "Label for dismiss button in alert component",
defaultMessage: "Dismiss",
},
close: {
id: "Nimbus.Dialog.close",
description: "Label for close button in dialog header",
defaultMessage: "Close",
},
});
Placeholder Text
export const messages = {
selectDate: {
id: "Nimbus.DatePicker.selectDate",
description: "Placeholder text for date input field",
defaultMessage: "Select date",
},
enterText: {
id: "Nimbus.TextInput.enterText",
description: "Default placeholder for text input field",
defaultMessage: "Enter text",
},
});
State Indicators
export const messages = {
showPassword: {
id: "Nimbus.PasswordInput.show",
description: "Button label to reveal password text",
defaultMessage: "Show password",
},
hidePassword: {
id: "Nimbus.PasswordInput.hide",
description: "Button label to conceal password text",
defaultMessage: "Hide password",
},
});
Dynamic Messages (with values)
export const messages = {
itemsSelected: {
id: "Nimbus.Select.itemsSelected",
description: "Announces number of selected items to screen readers",
defaultMessage:
"{count, plural, one {# item selected} other {# items selected}}",
},
pageIndicator: {
id: "Nimbus.Pagination.pageIndicator",
description: "Current page indicator for pagination",
defaultMessage: "Page {current} of {total}",
},
});
Component Integration
IMPORTANT: Components do NOT import
.i18n.tsfiles. They import compiled*.messages.tsdictionaries generated by the build pipeline.
Import Pattern
import { useLocalizedStringFormatter } from "@/hooks";
import { {componentName}MessagesStrings } from "./{component-name}.messages";
Usage in Components
Simple usage:
export const Component = (props: ComponentProps) => {
const msg = useLocalizedStringFormatter({componentName}MessagesStrings);
return (
<button aria-label={msg.format("dismiss")}>
<Icons.Close />
</button>
);
};
With dynamic values:
export const Component = (props: ComponentProps) => {
const msg = useLocalizedStringFormatter({componentName}MessagesStrings);
const label = msg.format("itemsSelected", {
count: selectedItems.length,
});
return <div aria-label={label}>{/* ... */}</div>;
};
With conditional messages:
export const PasswordInput = (props: PasswordInputProps) => {
const msg = useLocalizedStringFormatter(passwordInputMessagesStrings);
const [isVisible, setIsVisible] = useState(false);
const toggleLabel = msg.format(
isVisible ? "hidePassword" : "showPassword"
);
return (
<button onClick={() => setIsVisible(!isVisible)} aria-label={toggleLabel}>
{isVisible ? <Icons.VisibilityOff /> : <Icons.Visibility />}
</button>
);
};
React Aria Integration
When using React Aria hooks, pass localized labels:
export const NumberInput = (props: NumberInputProps) => {
const msg = useLocalizedStringFormatter(numberInputMessagesStrings);
const ariaProps = {
...props,
incrementAriaLabel: msg.format("increment"),
decrementAriaLabel: msg.format("decrement"),
};
const { inputProps, incrementButtonProps, decrementButtonProps } =
useNumberField(ariaProps, state, ref);
return (
<div>
<button {...incrementButtonProps} />
<input {...inputProps} />
<button {...decrementButtonProps} />
</div>
);
};
Compound Components (Single i18n File)
Components with multiple parts share a single i18n file:
// alert.i18n.ts - Single file for entire Alert compound component
export const messages = {
dismiss: {
id: "Nimbus.Alert.dismiss",
description: "aria-label for dismiss button in Alert.DismissButton",
defaultMessage: "Dismiss",
},
infoIcon: {
id: "Nimbus.Alert.infoIcon",
description: "aria-label for info icon in Alert.Icon",
defaultMessage: "Information",
},
warningIcon: {
id: "Nimbus.Alert.warningIcon",
description: "aria-label for warning icon in Alert.Icon",
defaultMessage: "Warning",
},
});
Usage in different parts:
// alert.dismiss-button.tsx
import { alertMessagesStrings } from "./alert.messages";
export const AlertDismissButton = () => {
const msg = useLocalizedStringFormatter(alertMessagesStrings);
return (
<button aria-label={msg.format("dismiss")}>
<Icons.Close />
</button>
);
};
// alert.icon.tsx
import { alertMessagesStrings } from "./alert.messages";
export const AlertIcon = ({ severity }: AlertIconProps) => {
const msg = useLocalizedStringFormatter(alertMessagesStrings);
const label = msg.format(
severity === "info" ? "infoIcon" : "warningIcon"
);
return <Icons.Info aria-label={label} />;
};
Supported Locales
Current Nimbus support:
en- English (default)de- Germanes- Spanishfr-FR- French (France)pt-BR- Portuguese (Brazil)
Create Mode
Step 1: Analyze Component for i18n Needs
Determine if component needs i18n:
- Check for default aria-labels
// ✅ Needs i18n <button aria-label="Dismiss">...</button> // ❌ Doesn't need i18n (consumer provides) <button aria-label={props.label}>...</button> - Check for default placeholders
// ✅ Needs i18n <input placeholder="Select date" /> // ❌ Doesn't need i18n <input placeholder={props.placeholder} /> - Check for interactive controls
// ✅ Needs i18n (increment/decrement buttons) <NumberInput /> // ❌ Doesn't need i18n (pure layout) <Box />
Step 2: Identify All Translatable Text
List all user-facing text:
- Accessibility labels
- Button text
- Placeholder text
- Helper text
- Tooltips
- Error messages
Step 3: Create Message Definitions
For each translatable text, create a message:
export const messages = {
// messageKey should be descriptive
actionName: {
id: "Nimbus.ComponentName.actionName",
description: "Clear context for translators",
defaultMessage: "User-friendly text",
},
});
Step 4: Update Component to Use Messages
Replace hardcoded strings with msg.format():
// Before
<button aria-label="Dismiss">Close</button>
// After
import { {componentName}MessagesStrings } from "./{component-name}.messages";
const msg = useLocalizedStringFormatter({componentName}MessagesStrings);
<button aria-label={msg.format("dismiss")}>Close</button>
Update Mode
Process
- You MUST read existing i18n file
- You MUST scan component for new user-facing text
- You SHOULD preserve existing message structure
- You MUST follow naming conventions
- You MUST provide clear descriptions
Common Updates
- Add new message - New feature with text
- Update description - Improve translator context
- Add dynamic values - Support pluralization or variables
- Deprecate message - Mark old message with comment
Validate Mode
Validation Checklist
You MUST validate against these requirements:
File Structure
- i18n file exists with
.tsextension - Named export:
export const messages = {...}) - Correct location:
{component}/{component}.i18n.ts
Message IDs
- All IDs follow pattern:
Nimbus.{Component}.{key} - ComponentName is PascalCase
- messageKey is camelCase
- IDs are unique within file
- IDs match component name exactly
Message Properties
- All messages have
id,description,defaultMessage - Descriptions explain context for translators
- Default messages use proper grammar
- Default messages are user-friendly
- No technical jargon in default messages
Component Assessment
- Component assessed for i18n need
- Has default labels/aria-labels (if yes, needs i18n)
- Component imports messages correctly
- All user-facing text is internationalized
- No hardcoded strings in component
Integration
- Component imports
useLocalizedStringFormatterfrom "@/hooks" - Component imports compiled message strings from
*.messages.ts - Messages used with
msg.format(key)ormsg.format(key, variables) - Dynamic values passed correctly as second parameter
Validation Report Format
## i18n Validation: {ComponentName}
### Status: [✅ PASS | ❌ FAIL | ⚠️ WARNING]
### i18n Assessment: [Required | Not Required]
### Files Reviewed
- i18n file: `{component}.i18n.ts`
- Component: `{component}.tsx`
### ✅ Compliant
[List passing checks]
### ❌ Violations (MUST FIX)
- [Violation with guideline reference and line number]
### ⚠️ Warnings (SHOULD FIX)
- [Non-critical improvements]
### Messages Found
- [List message IDs with their purpose]
### Missing i18n
- [List hardcoded strings that should be internationalized]
### Description Quality
- [Assess description clarity and completeness]
### Recommendations
- [Specific improvements needed]
Best Practices
✅ DO
- Use i18n for ALL user-facing text
- Keep messages concise and clear
- Provide meaningful descriptions
- Use proper grammar and punctuation
- Test with different locales
- Use plural forms for counts
- Design layouts for text length variation
❌ DON'T
- Concatenate translated strings
- Use i18n for debug/internal messages
- Hardcode strings that should be translatable
- Omit descriptions
- Use technical jargon in default messages
- Assume text length stays constant
Common Patterns
Conditional messages:
const message = isVisible
? messages.hide
: messages.show;
return <button aria-label={msg.format(message)} />;
Pluralization:
defaultMessage: "{count, plural, one {# item} other {# items}}";
With variables:
defaultMessage: "Page {current} of {total}";
// Usage
msg.format("pageInfo", { current: 1, total: 10 });
Error Recovery
If validation fails:
- You MUST check message ID format
- You MUST verify all three properties present
- You MUST ensure component imports correctly
- You MUST confirm no hardcoded strings remain
- You SHOULD test with different locales
Reference Examples
You SHOULD reference these i18n files:
- Simple:
packages/nimbus/src/components/alert/alert.i18n.ts - Complex:
packages/nimbus/src/components/number-input/number-input.i18n.ts - Compound:
packages/nimbus/src/components/date-picker/date-picker.i18n.ts
RFC 2119 Key Words
- MUST / REQUIRED / SHALL - Absolute requirement
- MUST NOT / SHALL NOT - Absolute prohibition
- SHOULD / RECOMMENDED - Should do unless valid reason not to
- SHOULD NOT / NOT RECOMMENDED - Should not do unless valid reason
- MAY / OPTIONAL - Truly optional
Execute i18n operation for: $ARGUMENTS