Skip to content
Back to skills

Orbit I18n

ASecurity

Internationalization (i18n) audit for a WordPress plugin — checks every user-facing string is wrapped in `__()`/`_e()`/etc., text domain matches the plugin slug, POT file is fresh, locale-loading hook is on `init`/`plugins_loaded` (not earlier), placeholders use `%1$s` for translator-friendly word order, and pluralization uses `_n()`. Use when the user says "i18n", "translation", "POT", "text domain", "make plugin translatable", or before a release with new strings.

  • 2 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 9, 2026
testingjavascriptgojavaphpbashgitapi

Works with

  • cli
  • api

Security analysis

A100/100

Scanned September 9, 2026

npx -y skills add adityaarsharma/orbit --skill orbit-i18n --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Orbit I18n?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Orbit I18n
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/adityaarsharma-orbit-i18n/badge)](https://www.skillsdirectory.com/skills/adityaarsharma-orbit-i18n)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: orbit-i18n
description: Internationalization (i18n) audit for a WordPress plugin — checks every user-facing string is wrapped in `__()`/`_e()`/etc., text domain matches the plugin slug, POT file is fresh, locale-loading hook is on `init`/`plugins_loaded` (not earlier), placeholders use `%1$s` for translator-friendly word order, and pluralization uses `_n()`. Use when the user says "i18n", "translation", "POT", "text domain", "make plugin translatable", or before a release with new strings.
---

# 🪐 orbit-i18n — Translation readiness audit

A plugin that ships with hardcoded English strings is broken for 60% of WordPress users. This skill catches every gap.

---

## Quick start

```bash
# Auto-fix path: regenerate the POT file
wp i18n make-pot ~/plugins/my-plugin languages/my-plugin.pot --slug=my-plugin

# Audit path: find every untranslated string
claude "/orbit-i18n Audit ~/plugins/my-plugin — find every user-facing string, verify text domain, check POT freshness."
```

Runs in `/orbit-gauntlet --mode full` (Step 5).

---

## What this skill checks

### 1. Every user-facing string is wrapped

```php
// ❌ BAD
echo 'Settings saved';
echo '<h2>My Plugin Settings</h2>';
return new WP_Error( 'fail', 'Could not save' );

// ✅ GOOD
echo esc_html__( 'Settings saved', 'my-plugin' );
echo '<h2>' . esc_html__( 'My Plugin Settings', 'my-plugin' ) . '</h2>';
return new WP_Error( 'fail', __( 'Could not save', 'my-plugin' ) );
```

Catches strings in: `echo`, `print`, `?>...<?php`, `wp_die()`, `WP_Error` messages, admin notices, `sprintf()` template, JS `wp.i18n.__()`.

### 2. Text domain matches plugin folder name

```php
// Plugin folder: my-plugin/
// Plugin header: Text Domain: my-plugin

// ❌ Mismatch
__( 'Hello', 'myplugin' )      // Wrong (no dash)
__( 'Hello', 'my_plugin' )     // Wrong (underscore)
__( 'Hello', 'MyPlugin' )      // Wrong (camel-case)

// ✅ Exact match
__( 'Hello', 'my-plugin' )
```

WP.org **rejects** plugins with text domain mismatches. This is non-negotiable.

### 3. Locale-loading hook timing

```php
// ❌ Too early — locale isn't ready
load_plugin_textdomain( 'my-plugin', false, basename( __DIR__ ) . '/languages' );

// ❌ Too early in `muplugins_loaded`
add_action( 'muplugins_loaded', 'my_plugin_load_textdomain' );

// ✅ Right hook — `init` (preferred) or `plugins_loaded`
add_action( 'init', 'my_plugin_load_textdomain' );

function my_plugin_load_textdomain() {
    load_plugin_textdomain(
        'my-plugin',
        false,
        dirname( plugin_basename( __FILE__ ) ) . '/languages'
    );
}
```

Note: WP 6.5+ preloads textdomains automatically — the manual call is a fallback for back-compat.

### 4. Translator-friendly placeholders

```php
// ❌ Hard for translators (German word order is different)
sprintf( __( '%s users found in %s seconds', 'my-plugin' ), $count, $time );

// ✅ Numbered placeholders + translator comment
sprintf(
    /* translators: 1: number of users, 2: number of seconds */
    __( '%1$d users found in %2$.2f seconds', 'my-plugin' ),
    $count,
    $time
);
```

### 5. Pluralization uses `_n()`

```php
// ❌ Doesn't work in languages with multiple plural forms (Russian, Polish, Arabic)
echo $count . ' ' . __( 'item' . ( $count === 1 ? '' : 's' ), 'my-plugin' );

// ✅ Correct — handles all 6 plural forms across languages
echo sprintf(
    _n( '%d item', '%d items', $count, 'my-plugin' ),
    $count
);
```

### 6. Context disambiguation with `_x()`

```php
// "Post" can mean noun (blog post) or verb (to post). Translators need context:
echo _x( 'Post', 'noun, blog post', 'my-plugin' );
echo _x( 'Post', 'verb, to publish', 'my-plugin' );
```

### 7. JavaScript translation (Gutenberg-compatible)

```php
// PHP — make strings available to JS
wp_set_script_translations( 'my-plugin-block', 'my-plugin', plugin_dir_path( __FILE__ ) . 'languages' );
```

```js
// JS — use wp.i18n
import { __ } from '@wordpress/i18n';
const label = __( 'Settings', 'my-plugin' );
```

### 8. POT file freshness

```bash
# Regenerate
wp i18n make-pot . languages/my-plugin.pot --slug=my-plugin

# Diff against committed
git diff languages/my-plugin.pot
# → Any non-trivial diff = strings changed since last regen → commit the new POT
```

The skill flags any string in code that is NOT in the POT file.

### 9. RTL readiness

```php
// In WP-Admin, use the dir-aware helpers
echo is_rtl() ? 'right-side' : 'left-side';
```

```css
/* Provide an RTL CSS file or use logical properties */
.my-button {
  margin-inline-start: 8px;  /* Auto-flips for RTL */
  /* NOT: margin-left: 8px; */
}
```

Plus ship `rtl.css` if you have any non-trivial admin styles.

---

## Report format

```markdown
# i18n Audit — [Plugin]

## Summary
- Hardcoded strings (untranslated): 47
- Text-domain mismatches: 3
- Locale-load hook issues: 1
- Placeholder issues: 8
- Pluralization issues: 2
- POT freshness: 14 strings missing from POT
- RTL readiness: ⚠ 2 hardcoded `margin-left` values

## Critical (block release)

### Text domain mismatch
**Files (3):**
- includes/class-admin.php:42 — `__( 'Save', 'myplugin' )`
- includes/class-admin.php:67 — `__( 'Cancel', 'myplugin' )`
- admin/views/settings.php:103 — `_e( 'Settings', 'myplugin' )`

**Fix:** Replace `'myplugin'` with `'my-plugin'` (matches Text Domain: header).

### Hardcoded English strings
**File:** admin/views/settings.php:18-92
**Count:** 23 strings
**Sample:**
- `<h1>My Plugin Settings</h1>`
- `<button>Save</button>`
- `<p>Configure your settings below.</p>`

[Continue for all findings]
```

---

## Common mistakes

| Anti-pattern | Why it breaks | Fix |
|---|---|---|
| `__( $variable, 'my-plugin' )` | Translator can't extract — string must be literal | Use `sprintf( __( 'Hello %s', 'my-plugin' ), $name )` |
| `__( 'a' . ' ' . 'b', 'my-plugin' )` | Concatenation hides string from POT | Single literal: `__( 'a b', 'my-plugin' )` |
| Loading text domain in plugin file root (not in a hook) | Runs before WP locale is ready | Move to `init` hook |
| Using `<a href="...">click here</a>` inside `__()` | Translators may break the HTML | Use `printf( __( 'Visit %s', 'my-plugin' ), '<a href="...">our site</a>' )` |
| Skipping `esc_html__()` and just using `__()` for output | XSS risk | Always escape on output |

---

## Block.json strings

Gutenberg block titles + descriptions are auto-translated if you set `textdomain`:

```json
{
  "apiVersion": 3,
  "name": "my-plugin/example",
  "title": "Example Block",
  "description": "Renders an example.",
  "textdomain": "my-plugin"
}
```

Run `wp i18n make-json` after updating block.json strings.

---

## Output paths

- POT file: `languages/<slug>.pot`
- Block translations: `languages/<slug>-<locale>-<handle>.json`
- RTL stylesheet: `assets/css/<file>-rtl.css` or use logical properties

---

## Test in another locale

```bash
# Switch the wp-env site to French
wp-env run cli wp language core install fr_FR
wp-env run cli wp site switch-language fr_FR
open http://localhost:8881/wp-admin
# Verify your plugin's UI is translated (or at least doesn't crash)
```

---

## Resources

- [WP Plugin Handbook — i18n](https://developer.wordpress.org/plugins/internationalization/)
- [GlotPress translation patterns](https://make.wordpress.org/polyglots/handbook/)
- [10up i18n best practices](https://10up.github.io/Engineering-Best-Practices/php/#i18n)

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…