npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@focus-reactive/payload-plugin-translator

v0.1.1

Published

Translation plugin for Payload CMS 3.x. Automatically translate your localized content using any translation provider.

Readme

@focus-reactive/payload-plugin-translator

Translation plugin for Payload CMS 3.x. Automatically translate your localized content using any translation provider.

Features

  • Deep translation — translates all localized fields at any nesting level (groups, arrays, blocks, tabs)
  • Rich text support — full Lexical rich text translation preserving formatting and structure
  • Bulk translation — translate multiple documents at once from collection list view
  • Progress tracking — real-time translation status indicators in admin UI
  • Async processing — queue-based background jobs (Payload Jobs) or synchronous mode
  • Pluggable providers — use OpenAI or create custom translation providers
  • Field exclusion — exclude specific fields from translation via withFieldTranslation
  • Translation strategies — choose between overwrite all or skip existing translations

Installation

# pnpm
pnpm add @focus-reactive/payload-plugin-translator

# npm
npm install @focus-reactive/payload-plugin-translator

# yarn
yarn add @focus-reactive/payload-plugin-translator

Quick Start

import { buildConfig } from 'payload'
import {
  translatorPlugin,
  createOpenAIProvider,
  createPayloadJobsRunner,
} from '@focus-reactive/payload-plugin-translator'
import { Posts } from './collections/Posts'
import { Pages } from './collections/Pages'

export default buildConfig({
  collections: [Posts, Pages],
  plugins: [
    translatorPlugin({
      collections: [Posts, Pages],
      translationProvider: createOpenAIProvider({
        apiKey: process.env.OPENAI_API_KEY,
      }),
      runner: createPayloadJobsRunner(),
    }),
  ],
  localization: {
    locales: ['en', 'de', 'fr'],
    defaultLocale: 'en',
  },
})

Configuration

TranslatorPluginConfig

Configuration for translatorPlugin().

| Property | Type | Required | Default | Description | | --------------------- | --------------------- | -------- | -------------- | ------------------------------------------------------------------------------------------------------------------- | | collections | CollectionConfig[] | Yes | — | Original collection configs to enable translation for. Must be the same objects passed to buildConfig, not slugs. | | translationProvider | TranslationProvider | Yes | — | Translation provider instance (e.g., createOpenAIProvider(...)) | | runner | TaskRunnerProvider | Yes | — | Task runner provider for background processing (e.g., createPayloadJobsRunner()) | | access | AccessGuard | No | undefined | Access control function for translation endpoints | | basePath | string | No | '/translate' | Base path for all API endpoints |

translatorPlugin({
  collections: [Posts, Pages],
  translationProvider: createOpenAIProvider({ apiKey: process.env.OPENAI_API_KEY }),
  runner: createPayloadJobsRunner(),
  access: async ({ req }) => req.user?.role === 'admin',
  basePath: '/translate',
})

OpenAIProviderConfig

Configuration for createOpenAIProvider().

| Property | Type | Required | Default | Description | | -------------- | ------------------------ | -------- | --------------- | ------------------------------------------ | | apiKey | string | Yes | — | OpenAI API key | | model | string | ChatModel | No | 'gpt-4o' | OpenAI model to use for translation | | systemPrompt | SystemPromptBuilder | No | Built-in prompt | Custom function to build the system prompt | | dryRun | boolean | DryRunConfig | No | false | Simulate translations without API calls |

createOpenAIProvider({
  apiKey: process.env.OPENAI_API_KEY,
  model: 'gpt-4o-mini',
  systemPrompt: ({ sourceLang, targetLang, defaultPrompt }) =>
    `${defaultPrompt}\nUse formal language. Keep brand names unchanged.`,
  dryRun: false,
})

SystemPromptBuilder

Function signature for custom system prompt:

type SystemPromptBuilder = (context: SystemPromptContext) => string

type SystemPromptContext = {
  sourceLang: string
  targetLang: string
  defaultPrompt: string
}

DryRunConfig

When dryRun is an object, it allows custom transformation with optional delay:

type DryRunTransformer = (text: string) => string | Promise<string>

type DryRunConfig = {
  transform: DryRunTransformer // Custom transformer function
  timeout?: number // Delay in ms (simulates API latency)
}

PayloadJobsRunnerOptions

Configuration for createPayloadJobsRunner().

| Property | Type | Required | Default | Description | | ----------- | ------------------------- | -------- | ---------------------------------- | ------------------------------------------------------- | | taskName | string | No | 'translate_document' | Task name in Payload jobs collection | | queueName | string | No | 'translations' | Queue name for grouping jobs | | autoRun | false | { cron, limit } | No | { cron: '* * * * *', limit: 50 } | Auto-run config, or false to disable (for serverless) |

createPayloadJobsRunner({
  taskName: 'translate_document',
  queueName: 'translations',
  autoRun: {
    cron: '* * * * *',
    limit: 50,
  },
})

FieldTranslationConfig

Configuration for withFieldTranslation() helper or field.custom.translateKit.

| Property | Type | Required | Default | Description | | --------- | --------- | -------- | ------- | ----------------------------------- | | exclude | boolean | No | false | Exclude this field from translation |

import { withFieldTranslation } from '@focus-reactive/payload-plugin-translator'

withFieldTranslation({ name: 'sku', type: 'text', localized: true }, { exclude: true })

Translation Strategies

When translating, you can choose how to handle existing translations:

| Strategy | Description | | ----------------- | ------------------------------------------------------------------------ | | 'overwrite' | (Default) Replaces all existing translated content with new translations | | 'skip_existing' | Only translates fields that are empty in the target locale |

Important Notes

Explicit localized: true for nested fields

Payload CMS allows wrapper fields (group, array, blocks, tabs) to be marked as localized, which makes nested fields inherit localization without explicit declaration. However, the plugin only translates leaf fields (primitive data fields like text, textarea, richText).

For correct plugin operation, you must explicitly set localized: true on each nested field you want to translate:

// ❌ Won't work — nested title is not explicitly localized
{
  name: 'meta',
  type: 'group',
  localized: true,
  fields: [
    { name: 'title', type: 'text' }
  ]
}

// ✅ Correct — title is explicitly localized
{
  name: 'meta',
  type: 'group',
  localized: true,
  fields: [
    { name: 'title', type: 'text', localized: true }
  ]
}

Excluded fields behavior

Using withFieldTranslation({ ... }, { exclude: true }) does not mean the field will be completely untouched during translation. If the excluded field is empty in the target locale, it will be populated with data from the source locale.

This prevents validation errors when saving translation results (e.g., required fields that should not be translated but must have a value).

Field exclusion priority over strategy

withFieldTranslation exclusion takes priority over the overwrite strategy. Even when using overwrite strategy, excluded fields will:

  • Keep their target value if it exists
  • Copy source value only if target is empty
  • Never be sent to the translation provider

Preserving completed job status

By default, Payload deletes jobs immediately after completion. This means the "Completed" status won't be visible in the UI. To preserve completed jobs and show their status, add this to your Payload config:

export default buildConfig({
  // ... other config
  jobs: {
    deleteJobOnComplete: false,
  },
})

Task Runners

PayloadJobsRunner (Recommended)

Uses Payload's built-in job queue for background processing:

import { createPayloadJobsRunner } from '@focus-reactive/payload-plugin-translator'

const runner = createPayloadJobsRunner({
  taskName: 'translate_document',
  queueName: 'translations',
  autoRun: {
    cron: '* * * * *',
    limit: 50,
  },
})

SyncRunner

Executes translations synchronously (useful for development or small datasets):

import { createSyncRunner } from '@focus-reactive/payload-plugin-translator'

const runner = createSyncRunner()

Translation Providers

OpenAI Provider

Built-in provider using OpenAI's API:

import { createOpenAIProvider } from '@focus-reactive/payload-plugin-translator'

const provider = createOpenAIProvider({
  apiKey: process.env.OPENAI_API_KEY,
  model: 'gpt-4o-mini',
  systemPrompt: ({ defaultPrompt }) => `${defaultPrompt}\nUse formal language.`,
})

Custom Provider

Create your own translation provider by implementing the TranslationProvider interface:

TranslationProvider Interface

| Method | Signature | Description | | ----------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | | translate | (content: TranslationInput, sourceLng: string, targetLng: string) => Promise<TranslationOutput | null> | Translates content from source to target language |

Important: The numeric indices in TranslationInput must be preserved exactly in TranslationOutput. Each index maps to a specific field in the document structure, so the provider must return the same keys with translated values.

Types

// Numeric index representing position in document structure
type TranslationIndex = number

// Input: Map of numeric indices to text strings
type TranslationInput = Record<TranslationIndex, string>

// Output: Same indices with translated values
type TranslationOutput = Record<TranslationIndex, string>

Example Implementation

import type {
  TranslationProvider,
  TranslationInput,
  TranslationOutput,
} from '@focus-reactive/payload-plugin-translator'

class DeepLProvider implements TranslationProvider {
  constructor(private apiKey: string) {}

  async translate(content: TranslationInput, sourceLng: string, targetLng: string): Promise<TranslationOutput | null> {
    try {
      // content example: { "0": "Hello", "1": "World" }
      const texts = Object.values(content)

      const response = await fetch('https://api.deepl.com/v2/translate', {
        method: 'POST',
        headers: {
          Authorization: `DeepL-Auth-Key ${this.apiKey}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          text: texts,
          source_lang: sourceLng.toUpperCase(),
          target_lang: targetLng.toUpperCase(),
        }),
      })

      const data = await response.json()

      // Reconstruct the result with same keys
      const result: TranslationOutput = {}
      Object.keys(content).forEach((key, index) => {
        result[key] = data.translations[index].text
      })

      return result
    } catch {
      return null
    }
  }
}

// Usage
translatorPlugin({
  collections: [Posts],
  translationProvider: new DeepLProvider(process.env.DEEPL_API_KEY),
  runner: createPayloadJobsRunner(),
})

UI Components

The plugin automatically adds:

  1. Document Translation Panel — Available in the document edit view sidebar
  2. Bulk Translation Dashboard — Accessible from collection list view
  3. Translation Progress Indicator — Shows real-time translation status

TypeScript

All types are exported for TypeScript users:

import type {
  // Plugin config
  TranslatorPluginConfig,

  // Provider types
  TranslationProvider,
  TranslationInput,
  TranslationOutput,
  OpenAIProviderConfig,
  DryRunConfig,

  // Runner types
  TaskRunnerProvider,
  PayloadJobsRunnerOptions,

  // Strategy types
  TranslationStrategy,

  // Access control
  AccessGuard,
  AccessGuardRequest,

  // Field config
  FieldTranslationConfig,
} from '@focus-reactive/payload-plugin-translator'

Known Issues

SQLite: Nested JSON queries not supported

SQLite adapter doesn't support nested JSON field queries like { 'input.collection.value': { equals: '5' } }.

Affected databases: SQLite only

Roadmap

Planned features for future releases:

  • Field-level translation — Granular translation control allowing users to translate individual fields rather than entire documents
  • Global translation dashboard — Translate all collections at once from a single interface, with progress tracking across the entire CMS
  • Vercel Cron Jobs runner — Built-in runner for seamless Vercel/serverless deployments without manual API route configuration
  • Auto-translate on source change — Automatically trigger translation when the default locale content is updated