close
Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/eslint-plugin/configs/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ module.exports = {
'@wordpress/i18n-ellipsis': 'error',
'@wordpress/i18n-no-flanking-whitespace': 'error',
'@wordpress/i18n-hyphenated-range': 'error',
'@wordpress/no-i18n-in-save': 'error',
},
};
101 changes: 101 additions & 0 deletions packages/eslint-plugin/docs/rules/no-i18n-in-save.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Disallow translation functions in block save methods (no-i18n-in-save)

Translation functions should never be used in block `save` functions or `save.js` files.

## Rule details

This rule aims to prevent the use of i18n translation functions (`__`, `_x`, `_n`, `_nx`) in block save methods.

### Why?

When translation functions are used in save methods:

1. Translation is saved to database: The translated text is stored at the time of saving, not when the content is displayed
Comment thread
mikachan marked this conversation as resolved.
Outdated
2. No dynamic updates: If the site language changes, previously saved content will not update
3. Block validation errors: Switching languages causes validation errors because the saved HTML no longer matches what the save function generates
4. Content locked to language: Content becomes permanently associated with the language active at save time

### What to do instead

- For static text: Use plain English text that PHP can replace during rendering
- For dynamic labels: Use block attributes and let PHP handle translation during rendering
- For user content: Store as attributes and render/translate in PHP

## Examples

### Incorrect

```js
// ❌ Translation in save function
function save() {
return (
<button>
<span>{ __( 'Click me', 'my-plugin' ) }</span>
</button>
);
}
```

```js
// ❌ Translation in arrow function save
const save = () => {
return __( 'Hello World' );
};
```

```js
// ❌ Translation in object method
const settings = {
save() {
return _x( 'Label', 'context' );
},
};
```

### Correct

```js
// ✅ No translation in save, handled by PHP
function save() {
return (
<button>
<span>Click me</span>
</button>
);
}
```

```js
// ✅ Translation in edit function
function edit() {
return (
<button>
<span>{ __( 'Click me', 'my-plugin' ) }</span>
</button>
);
}
```

```js
// ✅ Use attributes and let PHP translate
function save( { attributes } ) {
return (
<button>
<span>{ attributes.label }</span>
</button>
);
}
```

## When not to use

This rule should not be disabled. If you think you need an exception, consider:

1. Using a render callback in PHP to handle translation
2. Storing translatable content in block attributes
3. Using static text that PHP replaces during rendering

## Further Reading

- [Block API Reference](https://developer.wordpress.org/block-editor/reference-guides/block-api/)
- [Internationalization in WordPress](https://developer.wordpress.org/apis/internationalization/)
222 changes: 222 additions & 0 deletions packages/eslint-plugin/rules/__tests__/no-i18n-in-save.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
/**
* External dependencies
*/
import { RuleTester } from 'eslint';

/**
* Internal dependencies
*/
import rule from '../no-i18n-in-save';

const ruleTester = new RuleTester( {
parserOptions: {
ecmaVersion: 6,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
} );

ruleTester.run( 'no-i18n-in-save', rule, {
valid: [
{
code: `
function edit() {
return __( 'Hello World' );
}
`,
},
{
code: `
const edit = () => {
return __( 'Hello World' );
};
`,
},
{
code: `
const settings = {
edit() {
return __( 'Hello World' );
},
};
`,
},
{
code: `
// Translation functions are fine in non-save files
function render() {
return __( 'Hello World' );
}
`,
},
{
code: `
// Translation in edit function
export default function Edit() {
return <div>{ __( 'Hello World' ) }</div>;
}
`,
},
],
invalid: [
{
code: `
Comment thread
mikachan marked this conversation as resolved.
function save() {
return __( 'Hello World' );
}
`,
errors: [
{
messageId: 'noI18nInSave',
type: 'CallExpression',
},
],
},
{
code: `
const save = () => {
return __( 'Hello World' );
};
`,
errors: [
{
messageId: 'noI18nInSave',
type: 'CallExpression',
},
],
},
{
code: `
const save = function() {
return __( 'Hello World' );
};
`,
errors: [
{
messageId: 'noI18nInSave',
type: 'CallExpression',
},
],
},
{
code: `
export default function save() {
return <span>{ __( 'Hello World' ) }</span>;
}
`,
errors: [
{
messageId: 'noI18nInSave',
type: 'CallExpression',
},
],
},
{
code: `
const settings = {
save() {
return __( 'Hello World' );
},
};
`,
errors: [
{
messageId: 'noI18nInSave',
type: 'CallExpression',
},
],
},
{
code: `
const settings = {
save: () => __( 'Hello World' ),
};
`,
errors: [
{
messageId: 'noI18nInSave',
type: 'CallExpression',
},
],
},
{
code: `
function save() {
return _x( 'Hello', 'greeting' );
}
`,
errors: [
{
messageId: 'noI18nInSave',
type: 'CallExpression',
},
],
},
{
code: `
function save() {
const count = 5;
return _n( 'One item', 'Multiple items', count );
}
`,
errors: [
{
messageId: 'noI18nInSave',
type: 'CallExpression',
},
],
},
{
code: `
function save() {
const count = 5;
return _nx( 'One item', 'Multiple items', count, 'context' );
}
`,
errors: [
{
messageId: 'noI18nInSave',
type: 'CallExpression',
},
],
},
{
code: `
function save() {
return (
<button>
<span>{ __( 'Click me' ) }</span>
</button>
);
}
`,
errors: [
{
messageId: 'noI18nInSave',
type: 'CallExpression',
},
],
},
{
code: `
// Multiple translation calls in save
function save() {
const label = __( 'Label' );
return <div title={ _x( 'Title', 'context' ) }>{ label }</div>;
}
`,
errors: [
{
messageId: 'noI18nInSave',
type: 'CallExpression',
},
{
messageId: 'noI18nInSave',
type: 'CallExpression',
},
],
},
],
} );
Loading
Loading