Skip to content

vinceau/i18next-auto-keys

Repository files navigation

i18next-auto-keys

npm version Build Status License: MIT

Automatic translation key generation for i18next - No more manual key management!

✨ Features

  • 🔄 Automatic key generation - No manual key management required
  • 📍 Colocated translations - Text strings live next to the components that use them
  • 📦 Framework agnostic - Works with React, Vue, Angular, or vanilla JS
  • 🔒 Typesafe by default - Full TypeScript support with AST transformation
  • Multiple bundler support - Works with Webpack, Vite, and Rollup

🎯 Why this over other i18next libraries?

Traditional i18next libraries like react-i18next force you to:

  • Manually manage translation keys - tedious and error-prone
  • Rewrite your entire codebase to use their APIs (t('some.key') everywhere)
  • Buy into their ecosystem - change how you write and organize code
  • Separate translations from code - hunt through .json files to find text strings

With i18next-auto-keys, you:

  • Keep writing normal TypeScript - no special APIs to learn
  • Stay library-agnostic - developers don't even know what i18n library powers translations
  • Easy migration - switch translation systems with minimal changes to application code

i18next-auto-keys automatically extracts strings from your code and replaces them with i18next.t() calls using auto-generated hash keys at build time.

📋 Requirements

  • Node.js 18+
  • TypeScript 5+ (We use a TypeScript transformer)
  • One of the following bundlers:
    • Webpack 5+ (Webpack loader + plugin)
    • Vite (Rollup plugin, works in dev and build)
    • Rollup 4+ (Rollup plugin)

📦 Installation

npm install --save-dev i18next-auto-keys

# Your project will also need these dependencies if you don't already have them
npm install --save i18next i18next-icu

🚀 Quick Start

1. Write your messages in regular Typescript

You should use the ICU format for plurals and formatting.

// src/components/LoginForm.messages.ts
export const LoginMessages = {
  errorInvalid: (email: string): string => "Invalid email: {email}",

  // ICU pluralization examples
  loginAttempts: (count: number): string =>
    "{count, plural, one {# login attempt} other {# login attempts}} remaining",
};

2. Import and use the Messages

Convert this:

// src/components/LoginForm.tsx
function LoginForm(props) {
  return (
    <div>
      <div>Invalid email: {props.email}</div>
      <div>
      {props.loginAttemptsRemaining} login {props.loginAttemptsRemaining === 1 ? "attempt" : "attempts"} remaining
      </div>
    </div>
  );
}

Into this:

// src/components/LoginForm.tsx
import { LoginMessages } from "./LoginForm.messages.ts";

function LoginForm(props) {
  return (
    <div>
      <div>{LoginMessages.errorInvalid(props.email)}</div>
      <div>{LoginMessages.loginAttempts(props.loginAttemptsRemaining)}</div>
    </div>
  );
}

3. Configure your bundler

→ Full Vite & Rollup documentation

Webpack Configuration
// webpack.config.js
const { I18nextAutoKeyEmitPlugin } = require('i18next-auto-keys');

module.exports = {
  module: {
    rules: [
      {
        test: /\.messages\.(ts|tsx)$/, // Only process message files
        exclude: /node_modules/,
        enforce: 'pre', // Ensure this runs before actual TS -> JS compilation
        use: {
          loader: 'i18next-auto-keys',
          options: {
            setDefaultValue: process.env.NODE_ENV === 'development', // For HMR support
          }
        }
      }
    ]
  },
  plugins: [
    new I18nextAutoKeyEmitPlugin({
      jsonOutputPath: 'locales/en.json', // path to your i18next JSON resources
    })
  ]
};
Vite Configuration
// vite.config.js
import { defineConfig } from 'vite';
import { i18nextAutoKeyRollupPlugin } from 'i18next-auto-keys';

export default defineConfig({
  plugins: [
    i18nextAutoKeyRollupPlugin({
      jsonOutputPath: 'locales/en.json',
      setDefaultValue: process.env.NODE_ENV === 'development',
    }),
  ],
});
Rollup Configuration
// rollup.config.js
import { i18nextAutoKeyRollupPlugin } from 'i18next-auto-keys';
import typescript from '@rollup/plugin-typescript';

export default {
  input: 'src/index.ts',
  output: { dir: 'dist', format: 'esm' },
  plugins: [
    typescript(),
    i18nextAutoKeyRollupPlugin({
      jsonOutputPath: 'locales/en.json',
      setDefaultValue: process.env.NODE_ENV === 'development',
    }),
  ],
};

4. Profit!

The loader automatically transforms your code:

// After transformation
import i18next from "i18next";

export const LoginMessages = {
  errorInvalid: (email: string): string => i18next.t("p6q7r8s9t0", { email }),
  loginAttempts: (count: number): string => i18next.t("u8v9w0x1y2", { count }),
};

And generates translation files:

// dist/locales/en.json
{
  "p6q7r8s9t0": "Invalid email: {email}",
  "u8v9w0x1y2": "{count, plural, one {# login attempt} other {# login attempts}} remaining",
}

🎯 Usage Patterns

Pluralization

Since keys are auto-generated, we can't use i18next's default key-based pluralization (_zero, _one, etc.). Instead we use the ICU message format which handles plurals and inline formatting:

  • Pluralization: {count, plural, one {# item} other {# items}}
  • Number/Date formatting: {price, number, currency}{date, date, short}
  • Conditional text: {status, select, online {Connected} offline {Disconnected}}

Learn more about ICU format →

Parameter Handling

Warning

Do NOT use JavaScript string interpolation (${}) in your message functions!

// ❌ WRONG - Creates unstable hashes
greeting: (name: string): string => `Hello ${name}!`

// ✅ CORRECT - Use ICU format
greeting: (name: string): string => "Hello {name}!"
status: (count: number): string => "{count, plural, one {# item} other {# items}}"

Message Files Organization

Create dedicated message files for better organization:

// src/pages/auth/auth.messages.ts
export const AuthMessages = {
  // Simple messages
  title: (): string => "Authentication",
  subtitle: (): string => "Please sign in to continue",

  // Messages with ICU interpolation
  welcome: (name: string): string => "Welcome back, {name}!",

  // ICU pluralization
  attemptsLeft: (count: number): string =>
    "{count, plural, one {# attempt} other {# attempts}} remaining",
};

Supports Different Function Syntax

Supports arrow functions, function expressions, and method shorthand:

export const Messages = {
  // Arrow functions
  arrowStyle: (name: string): string => "Hello {name}",

  // Function expressions
  functionStyle: function(count: number): string {
    return "{count, plural, one {# item} other {# items}}";
  },

  // Method shorthand
  methodStyle(status: string): string {
    return "Status: {status}";
  },
};

JSDoc Parameter Extraction for Translators

When using the CLI to generate PO files, JSDoc comments are automatically extracted to provide context for translators, especially useful with ICU indexed mode:

/**
 * User account summary message
 * @param userName The user's display name
 * @param accountType Type of account (free, premium, enterprise)
 * @param daysSince Days since account creation
 * @translationContext user-account-summary
 */
userSummary: (userName: string, accountType: string, daysSince: number): string =>
  "User {userName} has {accountType} account (active for {daysSince, number} days)"

Generated PO file with translator context:

#. User account summary message
#. {0} userName: string - The user's display name
#. {1} accountType: string - Type of account (free, premium, enterprise)
#. {2} daysSince: number - Days since account creation
msgctxt "user-account-summary"
msgid "User {userName} has {accountType} account (active for {daysSince, number} days)"
msgstr ""

Parameter Modes

Named (Default): Use parameter names in ICU format

greeting: (name: string): string => "Hello {name}!"
// Transforms to: i18next.t("abc123", { name })

Indexed: Use numbered placeholders

greeting: (name: string): string => "Hello {0}!"
// Transforms to: i18next.t("abc123", { "0": name })

Excluding Messages from Translation

Use JSDoc comments to exclude specific functions:

/** @noTranslate */
debugInfo: (): string => "Debug: Component mounted"

Translation Context for Message Disambiguation

Use @translationContext in JSDoc comments to provide context for identical strings that may need different translations:

export const Messages = {
  /** @translationContext gaming */
  playGame: (): string => "Play",

  /** @translationContext music */
  playMusic: (): string => "Play",
};

This helps translators distinguish between contexts. For example, in Spanish:

  • Gaming context: "Jugar" (to play a game)
  • Music context: "Reproducir" (to play music)

Generated PO file with context:

#. Play button for games
msgctxt "gaming"
msgid "Play"
msgstr ""

#. Play button for music
msgctxt "music"
msgid "Play"
msgstr ""

setDefaultValue Option

Includes original strings as fallback values. Useful for development mode or HMR:

// Source
message: (name: string): string => "Hello {name}"

// Transformed to
message: (name: string): string => i18next.t("abc123def4", { defaultValue: "Hello {name}", name })

Use in development to keep production bundles small, or if you don't want to load your default language from a JSON resource.

Debug Mode

Wrap transformed strings with ~~ markers to easily identify which strings are using the translation system in your running application.

This is particularly useful when gradually migrating an existing codebase to use i18next-auto-keys. The wrapped strings will be visually distinct, making it easy to spot which strings have been migrated vs which are still hardcoded. It also helps developers consider the spacial requirements of other languages, especially more verbose languages which might require more space.

Configuration:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.messages\.(ts|tsx)$/,
        use: {
          loader: 'i18next-auto-keys',
          options: {
            debug: process.env.NODE_ENV === 'development',
          }
        }
      }
    ]
  }
};

Example transformation:

// Source
greeting: (): string => "Hello world"

// Transformed in development with debug: true
greeting: (): string => `~~${i18next.t("abc123def4")}~~`

// Result in browser: "~~Hello world~~"

🛠️ CLI Tools

Extract translation keys from source files:

npx i18next-auto-keys extract --include "**/*.messages.ts" --output ./i18n/messages.pot

Sync existing PO files with new strings:

npx i18next-auto-keys sync --template ./i18n/messages.pot --po-files "./i18n/*.po" --backup

Convert PO files to JSON:

npx i18next-auto-keys convert --input "./i18n/*.po" --output ./public/locales --batch

Status - Show translation progress for PO files:

npx i18next-auto-keys status

Full CLI documentation →

⚙️ Configuration

Project Configuration File

Optional config file for project-wide settings. Supports multiple formats: i18next-auto-keys.config.js, .i18next-auto-keysrc.json, or package.json.

// i18next-auto-keys.config.js
module.exports = {
  poOutputDirectory: "locales",
  poTemplateName: "messages.pot",
  hashLength: 12,
  argMode: "named",
  topLevelKey: "common",
  projectId: "my-app v2.1.0", // Optional: defaults to package.json name + version
  jsonIndentSpaces: 2,
};

Configuration Options:

Option Type Default Description
poOutputDirectory string "i18n" Directory where PO template files are output
poTemplateName string "messages.pot" Name of the PO template file
hashLength number 10 Length of generated hash keys (minimum 10)
argMode 'indexed' | 'named' 'named' How to pass parameters to i18next.t()
topLevelKey string undefined Wrap translations under a top-level key
projectId string package.json name + version (fallback: "app 1.0") Project ID for PO file headers
jsonIndentSpaces number 2 JSON indentation spaces for output files

Webpack Loader Options

These options override configuration file settings when specified:

Option Type Default Description
include RegExp | RegExp[] * Pattern(s) to match files for processing
hashLength number From config Length of generated hash keys (minimum 10)
argMode 'indexed' | 'named' From config How to pass parameters to i18next.t()
sourcemap boolean false Generate source maps
setDefaultValue boolean false Include original strings as defaultValue in i18next calls
debug boolean false Wrap transformed strings with ~~ markers for visual debugging

Webpack Plugin Options

These options override configuration file settings when specified:

Option Type Default Description
jsonOutputPath string Required Path for JSON translation file
topLevelKey string From config Wrap translations under a top-level key

🧪 Development

Setup

git clone https://github.com/username/i18next-auto-keys.git
cd i18next-auto-keys
npm install

Scripts

npm run build    # Build the package
npm test         # Run tests
npm run test:watch # Watch mode testing

📄 License

This project is licensed under the MIT License - see the LICENSE file for details.