Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ESLint rule to prevent usage of the verb 'toggle' in translatable strings #67741

Merged

Conversation

im3dabasia
Copy link
Contributor

@im3dabasia im3dabasia commented Dec 9, 2024

Closes : #66372

What?

As described in the above issue, the Gutenberg repository moved away from using the verb "toggle" because it was causing issues with translations. A new issue was proposed (#66372) to add an ESLint rule to prevent contributors from using this word in translation-ready functions like __().

Why?

The usage of the verb "toggle" was causing incorrect translations in the core, as it does not translate well in many languages.

How?

An ESLint rule was added to prevent the usage of the verb "toggle."

Screenshots or screencast

image
Screen.Recording.2024-12-11.at.11.38.52.AM.mov

@im3dabasia im3dabasia marked this pull request as ready for review December 11, 2024 06:17
Copy link

github-actions bot commented Dec 11, 2024

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: im3dabasia <[email protected]>
Co-authored-by: swissspidy <[email protected]>
Co-authored-by: Mamaduka <[email protected]>
Co-authored-by: afercia <[email protected]>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@im3dabasia
Copy link
Contributor Author

Hey @afercia,
Could you please review this PR when you have a moment?

@Mamaduka
Copy link
Member

@swissspidy, I think you've got the most experience with custom i18n ESLint rules. Do you mind having a look at this?

@swissspidy swissspidy self-requested a review December 16, 2024 15:28
Copy link
Member

@swissspidy swissspidy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO such a rule is too niche/specific for this specific string. If anything, it should use a user-configurable blocklist. It's also very specific for WordPress/Gutenberg, so I don't think eslint-plugin is a good fit for it.

I would just use no-restricted-syntax for this. There's a lot of precedent for this in eslintrc.js:

gutenberg/.eslintrc.js

Lines 86 to 140 in 2ee383c

const restrictedSyntax = [
// NOTE: We can't include the forward slash in our regex or
// we'll get a `SyntaxError` (Invalid regular expression: \ at end of pattern)
// here. That's why we use \\u002F in the regexes below.
{
selector:
'ImportDeclaration[source.value=/^@wordpress\\u002F.+\\u002F/]',
message: 'Path access on WordPress dependencies is not allowed.',
},
{
selector:
'CallExpression[callee.name="deprecated"] Property[key.name="version"][value.value=/' +
majorMinorRegExp +
'/]',
message:
'Deprecated functions must be removed before releasing this version.',
},
{
selector:
'CallExpression[callee.object.name="page"][callee.property.name="waitFor"]',
message:
'This method is deprecated. You should use the more explicit API methods available.',
},
{
selector:
'CallExpression[callee.object.name="page"][callee.property.name="waitForTimeout"]',
message: 'Prefer page.waitForSelector instead.',
},
{
selector: 'JSXAttribute[name.name="id"][value.type="Literal"]',
message:
'Do not use string literals for IDs; use withInstanceId instead.',
},
{
// Discourage the usage of `Math.random()` as it's a code smell
// for UUID generation, for which we already have a higher-order
// component: `withInstanceId`.
selector:
'CallExpression[callee.object.name="Math"][callee.property.name="random"]',
message:
'Do not use Math.random() to generate unique IDs; use withInstanceId instead. (If you’re not generating unique IDs: ignore this message.)',
},
{
selector:
'CallExpression[callee.name="withDispatch"] > :function > BlockStatement > :not(VariableDeclaration,ReturnStatement)',
message:
'withDispatch must return an object with consistent keys. Avoid performing logic in `mapDispatchToProps`.',
},
{
selector:
'LogicalExpression[operator="&&"][left.property.name="length"][right.type="JSXElement"]',
message:
'Avoid truthy checks on length property rendering, as zero length is rendered verbatim.',
},
];

The following rule works just fine:

	{
		selector: 'Literal[value=/^toggle/i]',
		message: "Avoid using the verb 'Toggle' in translatable strings",
	},

@swissspidy swissspidy added Internationalization (i18n) Issues or PRs related to internationalization efforts [Type] Automated Testing Testing infrastructure changes impacting the execution of end-to-end (E2E) and/or unit tests. labels Dec 16, 2024
@im3dabasia
Copy link
Contributor Author

Hi @swissspidy,

I used Literal[value=/^toggle\\b/i] to ensure that the rule only matches the word "toggle" when it appears standalone, excluding cases like "ToggleControl."

This approach helps avoid false positives in translatable strings.

Please take a look at my PR and let me know your feedback.

@swissspidy
Copy link
Member

Looks much better! Now we just need to fix the 20 newly reported errors :)

@im3dabasia
Copy link
Contributor Author

@swissspidy,

Upon reviewing the errors flagged by the current rule, it appears that the rule is raising errors for the word "toggle" whenever it appears in any string literal, including cases where it is not inside a translation function (such as in Examples 11, 12, 13, etc.).

However, our initial goal was to only prevent the use of the word "toggle" in translation-ready functions like __() and similar.

Could you please advise on how to adjust the rule to restrict the error only to those specific translation contexts, and prevent it from flagging usage of "toggle" in other non-translation-related string literals?

Thank you in advance for your guidance!

  1. const toggleHelpText = __(
    'Toggle between using the same value for all screen sizes or using a unique value per screen size.'
    );
  2. describe( 'NavigationMenuSelector', () => {
    describe( 'Toggle', () => {
    it( 'should show dropdown toggle with loading message when menus have not resolved', async () => {
    useNavigationMenu.mockReturnValue( {
    navigationMenus: [],
    isResolvingNavigationMenus: true,
    hasResolvedNavigationMenus: false,
    canSwitchNavigationMenu: true,
    } );
    render( <NavigationMenuSelector /> );
    expect(
    screen.getByRole( 'button', {
    name: /Loading/,
    } )
    ).toBeInTheDocument();
    } );
  3. help={ __(
    'Toggle off to hide the label text, e.g. "Next Page".'
    ) }
  4. help={
    onlyIncludeCurrentPage
    ? __(
    'Only including headings from the current page (if the post is paginated).'
    )
    : __(
    'Toggle to only include headings from the current page (if the post is paginated).'
    )
    }
  5. const toggleButton = screen.getByRole( 'button', {
    name: 'Toggle third item',
    } );
  6. describe( 'toggle', () => {
    it( 'toggles the selection of a date', () => {
  7. await user.click( screen.getByRole( 'button', { name: 'Toggle' } ) );
  8. const opener = screen.getByRole( 'button', {
    name: 'Toggle Modal',
    } );
  9. const opener = screen.getByRole( 'button', {
    name: 'Toggle Modal',
    } );
  10. const opener = screen.getByRole( 'button', {
    name: 'Toggle Modal',
    } );
  11. const id = useInstanceId(
    ToggleGroupControlOptionBase,
    toggleGroupControlContext.baseId || 'toggle-group-control-option-base'
    );
  12. const generatedId = useInstanceId(
    ToggleGroupControlAsButtonGroup,
    'toggle-group-control-as-button-group'
    );
  13. const generatedId = useInstanceId(
    ToggleGroupControlAsRadioGroup,
    'toggle-group-control-as-radio-group'
    );
  14. it( 'does not rerender a nested component that is to be unmounted', () => {
    registry.registerStore( 'toggler', {
    reducer: ( state = false, action ) =>
    action.type === 'TOGGLE' ? ! state : state,
    actions: {
    toggle: () => ( { type: 'TOGGLE' } ),
    },
    selectors: {
    get: ( state ) => state,
    },
    } );
  15. function submitCustomFieldsForm() {
    const customFieldsForm = document.getElementById(
    'toggle-custom-fields-form'
    );
  16. expect( getElementById ).toHaveBeenCalledWith(
    'toggle-custom-fields-form'
    );
  17. const IS_TOGGLE = 'toggle';
    const IS_BUTTON = 'button';
  18. it( 'toggle when post is not (1), (2), (3), the viewport is <= medium, and the publish sidebar is enabled', () => {
    useViewportMatch.mockReturnValue( true );
    useSelect.mockImplementation( () => ( {
    isPublishSidebarEnabled: true,
    } ) );
    render( <PostPublishButtonOrToggle isPublishSidebarEnabled /> );
    expect(
    screen.getByRole( 'button', { name: 'Publish' } )
    ).toBeVisible();
    } );
  19. describe( 'toggle', () => {
    describe( 'without force', () => {
    it( 'toggles off', () => {
    const list = new TokenList( 'abc' );
    const returnValue = list.toggle( 'abc' );
    expect( list.value ).toBe( '' );
    expect( list ).toHaveLength( 0 );
    expect( returnValue ).toBe( false );
    } );

@swissspidy
Copy link
Member

So how about CallExpression[callee.name=/^(__|_x|_n|_nx)$/] > Literal[value=/^toggle\b/i]

@im3dabasia
Copy link
Contributor Author

im3dabasia commented Dec 18, 2024

Thank you very much, @swissspidy. I really appreciate it.

After adopting the new approach, the errors have been reduced to three, which are string literals in the __() functions.

I believe these might have been missed previously.

  1. 'Toggle between using the same value for all screen sizes or using a unique value per screen size.'
  2. 'Toggle off to hide the label text, e.g. "Next Page".'
  3. 'Toggle to only include headings from the current page (if the post is paginated).'
Click to view code

const toggleHelpText = __(
'Toggle between using the same value for all screen sizes or using a unique value per screen size.'
);

help={ __(
'Toggle off to hide the label text, e.g. "Next Page".'
) }

help={
onlyIncludeCurrentPage
? __(
'Only including headings from the current page (if the post is paginated).'
)
: __(
'Toggle to only include headings from the current page (if the post is paginated).'
)
}

@swissspidy
Copy link
Member

So can we fix them? :-)

@Mamaduka
Copy link
Member

The Query Pagination text has a separate issue - #51335. I'm working on a fix - #68105.

@im3dabasia
Copy link
Contributor Author

Could you please suggest which of the following revised strings would be the most appropriate?

  1. Original:
    'Toggle between using the same value for all screen sizes or using a unique value per screen size.'

    Revised Alternatives:

    • 'Choose whether to use the same value for all screen sizes or a unique value for each screen size.'
    • 'Select whether to apply the same value across all screen sizes or a different value for each size.'
  2. Original: (@Mamaduka is working on this)
    'Toggle off to hide the label text, e.g. "Next Page".'

    Revised Alternatives:

    • 'Disable to hide the label text, such as "Next Page".'
    • 'Turn off to hide the label text (e.g., "Next Page").'
  3. Original:
    'Toggle to only include headings from the current page (if the post is paginated).'

    Revised Alternatives:

    • 'Select to include only headings from the current page (if the post is paginated). '
    • 'Enable to include only headings from the current page (if the post is paginated). '

@swissspidy
Copy link
Member

I'd say:

  • 'Choose whether to use the same value for all screen sizes or a unique value for each screen size.'
  • Only include headings from the current page (if the post is paginated).

@swissspidy swissspidy requested review from Mamaduka and afercia and removed request for gziolo, ntwb, ajitbohra and nerrad December 19, 2024 09:33
@swissspidy swissspidy changed the title Add Eslint rules to prevent usage of the verb 'toggle' in translatable strings Add ESLint rule to prevent usage of the verb 'toggle' in translatable strings Dec 19, 2024
@Mamaduka
Copy link
Member

@im3dabasia, the Query Pagination fix has been merged. Rebasing will resolve the failing Static Analysis check.

@im3dabasia
Copy link
Contributor Author

@im3dabasia, the Query Pagination fix has been merged. Rebasing will resolve the failing Static Analysis check.

Thank you for the update. I will go ahead and rebase my branch right away

@im3dabasia im3dabasia force-pushed the try/add-eslint-rules-for-word-toggle branch from a4fb20a to d54337c Compare December 19, 2024 10:50
@Mamaduka Mamaduka merged commit 8a10988 into WordPress:trunk Dec 19, 2024
61 checks passed
@github-actions github-actions bot added this to the Gutenberg 20.0 milestone Dec 19, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Internationalization (i18n) Issues or PRs related to internationalization efforts [Type] Automated Testing Testing infrastructure changes impacting the execution of end-to-end (E2E) and/or unit tests.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add linting rules to prevent the usage of the verb 'toggle' in all translatable strings
3 participants