UIStrings in Payload CMS: Building a Robust Translation Key Management System

26 novembre 2024 di Moritz Thomas

In any internationalization system, managing translation keys is a critical foundation. This article explores how we implemented a UIStrings collection in Payload CMS that bridges the gap between frontend translation needs and content management, while providing a smooth editing experience for translators and maintaining strict data integrity.

Core Concepts

At its heart, our UIStrings collection manages key-value pairs where:

  • The key is a unique identifier used in the frontend (e.g., comments-textfield-label)

  • The value is the translated text in various languages

  • Additional metadata provides context for translators

These translation keys are used in the frontend through React's internationalization utilities:

// Using react-intl directly
intl.formatMessage({ id: "comments-textfield-label" })

// Using the FormattedMessage component
<FormattedMessage id="comments-submit-button" />

Collection Implementation

Let's break down the key components of our UIStrings collection:

const UIStrings: CollectionConfig = {
  slug: "ui-strings",
  labels: {
    singular: "UI String",
    plural: "UI Strings",
  },
  versions: true,
  // ... fields configuration
};

Custom ID Field

One of our key design decisions was to use the translation key itself as the document ID, a feature called Custom ID in Payload. This approach has several benefits:

{
  name: "id",
  type: "text",
  validate: validateAlphaNumeric("ID"),
}

Why Custom IDs?

  1. Eliminates redundant identification (no separate Payload ID and translation key)

  2. Makes API responses cleaner and more intuitive

  3. Ensures direct correlation between frontend usage and database records

Important Considerations:

  • IDs cannot contain dots or slashes due to Payload's URL routing
  • Once created, IDs cannot be changed (matching their hardcoded nature in the frontend)
  • Alphanumeric validation prevents problematic characters

Enhanced Translation Experience

To help translators work effectively, we implemented a custom "Default Text" field that shows the reference text from the default language:

{
  name: "defaultText",
  type: "ui",
  admin: {
    components: {
      Field: DefaultTextField,
      Cell: DefaultTextCell,
    },
  },
}

The DefaultTextField component provides a clear reference:

export function DefaultTextField({ label }: { label: string }) {
  const { id } = useDocumentInfo();

  return (
    id && (
      <div className="default-text-field">
        <Label label={`${label} (${defaultLocale})`} />
        <div>
          <DefaultTextCell rowData={{ id: id.toString() }} />
        </div>
      </div>
    )
  );
}

This component fetches and displays the default language text using SWR for efficient data loading, used in the edit form as well as the list overview:

export function DefaultTextCell({ rowData }: { rowData: Pick<UiString, "id"> }) {
  const { data, error } = useSWR(
    `/api/${slug}/${rowData.id}?locale=${defaultLocale}`, 
    fetcher
  );

  if (error) return <span>Error loading text</span>;
  if (!data) return <span>Loading...</span>;

  return <span>{data.text || "<No Default Text>"}</span>;
}

Translation Field

The main translation field is implemented with localization support:

{
  name: "text",
  type: "text",
  label: "Text",
  localized: true,
  admin: {
    components: {
      Field: (props) => (
        <InputField {...props} minVariations={3} maxVariations={5} />
      ),
    },
  },
}

Notable features:

  • Localized field enabling translations for all supported languages

  • Custom InputField component supporting AI-powered translation suggestions

  • Configurable minimum and maximum variation counts for suggestions

Access Control

Our access control implementation follows clear security principles and uses the rbacHas function which was discussed in the RBAC (Role Based Access Control) article:

access: {
  read: () => true, // Public access for frontend use
  create: rbacHas(ROLE_ADMIN), // Only admins can create new keys
  update: rbacHas([ROLE_TRANSLATOR, ROLE_EDITOR]), // Translators can modify text
  delete: rbacHas(ROLE_ADMIN), // Only admins can remove keys
  readVersions: () => true,
}

This pattern:

  • Restricts structural changes (create/delete) to administrators

  • Allows translators and editors to update translations

  • Keeps translations and version history publicly accessible

Data Cleanup

To maintain clean data and ensure proper filtering in the admin UI, we implemented a beforeChange hook:

hooks: {
  beforeChange: [
    ({ data }) => {
      // Remove empty text fields for proper admin UI filtering
      if (data.text === "") {
        delete data.text;
      }
      return data;
    },
  ],
}

This is important when leveraging the filtering of Payload CMS. This way in the list vew under "Filters" you can select "Text" -> "exists" -> "false" and will be shown all keys which do not have a text set for the current locale.

Version Control

Our implementation includes comprehensive version control through Payload's versioning system:

const UIStrings: CollectionConfig = {
  // ... other configuration
  versions: true,
};

Version Management Features

The versioning system provides several key benefits for translation management:

  1. Change History

    • Every update to a translation is automatically versioned

    • Editors can view who made each change and when

    • Complete history of translations in all languages is preserved

  2. Rollback Capabilities

    • Editors can restore previous versions if needed

    • Useful for correcting mistakes or reverting controversial changes

    • Maintains all metadata when rolling back

  3. Audit Trail

    • Track when and why translations were modified

    • Identify patterns in translation updates

    • Support quality control processes

Version Interface

Payload CMS provides a built-in version interface that shows:

  • A timeline of all changes

  • The user who made each change

  • Timestamps for all modifications

  • Diff views comparing versions

  • One-click restore functionality

This interface is particularly valuable for:

  • Resolving disputes about translation changes
  • Training new translators by showing historical decisions
  • Understanding the evolution of particular translations
  • Maintaining accountability in the translation process

API Response Format

The REST API provides clean, focused responses:

{
  "id": "comments-textfield-label",
  "createdAt": "2024-09-12T07:35:42.638Z",
  "updatedAt": "2024-09-12T07:35:42.638Z",
  "text": "Kommentar",
  "drafts": []
}

Styling Considerations

The default text reference is styled for clarity:

.default-text-field {
  margin-bottom: var(--spacing-field);

  & > div {
    background-color: #eee;
    padding: calc(var(--base) / 4);
    padding-left: calc(var(--base) / 2);
  }
}

This styling:

  • Clearly separates the reference text visually

  • Maintains consistent spacing with other fields

  • Uses subdued colors to avoid confusion with editable fields

Conclusion

Our UIStrings collection implementation provides a robust foundation for translation management while addressing several key challenges:

  • Efficient key-value storage and retrieval

  • Clean API responses

  • Strong access control

  • Enhanced translator experience

  • Data integrity protection

The system balances flexibility with necessary constraints, ensuring that translation keys remain stable while allowing efficient translation workflows. Future articles will explore additional features like AI-powered translation suggestions and advanced collaboration tools.

Commenti

Ancora nessun commento, sii il primo:

© 2024 da Moritz Thomas