c
$npx skills add commercetools/nimbus --skill writing-i18n

Writing 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:

  1. Read i18n guidelines:
    cat docs/file-type-guidelines/i18n.md
    
  2. Read naming conventions for message IDs:
    cat docs/naming-conventions.md
    

    Message ID pattern: Nimbus.{ComponentName}.{messageKey}
  3. Review component implementation to identify translatable text:
    cat packages/nimbus/src/components/{component}/{component}.tsx
    
  4. 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.dismiss
  • Nimbus.DatePicker.clearInput
  • Nimbus.NumberInput.increment
  • Nimbus.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.ts files. They import compiled *.messages.ts dictionaries 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 - German
  • es - Spanish
  • fr-FR - French (France)
  • pt-BR - Portuguese (Brazil)

Create Mode

Step 1: Analyze Component for i18n Needs

Determine if component needs i18n:

  1. Check for default aria-labels
    // ✅ Needs i18n
    <button aria-label="Dismiss">...</button>
    
    // ❌ Doesn't need i18n (consumer provides)
    <button aria-label={props.label}>...</button>
    
  2. Check for default placeholders
    // ✅ Needs i18n
    <input placeholder="Select date" />
    
    // ❌ Doesn't need i18n
    <input placeholder={props.placeholder} />
    
  3. 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

  1. You MUST read existing i18n file
  2. You MUST scan component for new user-facing text
  3. You SHOULD preserve existing message structure
  4. You MUST follow naming conventions
  5. 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 .ts extension
  • 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 useLocalizedStringFormatter from "@/hooks"
  • Component imports compiled message strings from *.messages.ts
  • Messages used with msg.format(key) or msg.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:

  1. You MUST check message ID format
  2. You MUST verify all three properties present
  3. You MUST ensure component imports correctly
  4. You MUST confirm no hardcoded strings remain
  5. 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