Skip to content

#35144 - Compare term names by a case- and accent-folded sort key - #13586

Open
skikken wants to merge 2 commits into
WordPress:trunkfrom
skikken:fix/35144-sorting-greek-characters
Open

skikken wants to merge 2 commits into
WordPress:trunkfrom
skikken:fix/35144-sorting-greek-characters

Conversation

@skikken

@skikken skikken commented Sep 17, 2026

Copy link
Copy Markdown

Greek terms in a tag cloud come out in five groups instead of two. The reporter described it as Latin Aa-Zz, then Greek Α-Ω, then Greek α-ω, and later refined it to five groups: Latin case-insensitive, uppercase accented Greek, uppercase non-accented Greek, lowercase accented Greek and lowercase non-accented Greek.

wp_generate_tag_cloud() re-sorts the terms in PHP after the database has already sorted them. That second sort goes through _wp_object_name_sort_cb(), which compared with strnatcasecmp(). That function folds case for ASCII only and ignores accents entirely, so for anything non-ASCII the comparison is decided by raw byte value, and one Greek alphabet becomes four byte ranges:

uppercase accented   U+0386-U+038F   CE86-CE8F
uppercase plain      U+0391-U+03A9   CE91-CE9F
lowercase accented   U+03AC-U+03CE   CEAC-CEAF
lowercase plain      U+03B1-U+03C9   CEB1-CF89

Those four ranges stay distinct and sort in exactly that order, which is the reported symptom. It also explains the part of the report that was hardest to account for: the same terms sorted correctly in phpMyAdmin, because the database collation is doing the right thing and the PHP re-sort is throwing that ordering away.

The fix builds a sort key by folding the name to lowercase and stripping combining marks, so case and accent variants of the same letter share a key and sort together. Two details worth calling out:

  • The key is deliberately left in decomposed form. Recomposing after stripping the marks puts the accents straight back, so the fold is undone.
  • A deterministic strnatcmp() tie-break on the original name runs when the folded keys are equal. uasort() is not stable, so without a tie-break the relative order of variants would be undefined and untestable.

mbstring and intl are not required by WordPress and neither is polyfilled for these functions, so both folds apply only when the extension is present, following the function_exists() pattern already used in wp-includes/class-wp-query.php:

$strtolower = function_exists( 'mb_strtolower' ) ? 'mb_strtolower' : 'strtolower';

With neither extension available the ordering is no worse than before: the plain strtolower() fallback still merges uppercase and lowercase forms, covering the larger half of the report.

Two additional notes:

  • The callback also returned the result of strnatcasecmp() directly. It now returns an int. This is the same class of issue that was fixed for an equivalent comparison callback in wp-includes/post.php, where returning a bool triggered a deprecation notice on PHP 8. It is mentioned here because the touched function is the one in the fix, not as a claim about any other callback.
  • No change was made to remove_accents(). It has no Greek entries in its character map and no el_GR branch in its locale rules, so it is the wrong tool for this and adding a locale branch there would be a much larger change than this ticket needs.

Tests were added to the existing tag cloud test file rather than a new one, since that file is where wp_generate_tag_cloud() coverage already lives. They drive the public API, and the ASCII test passes both with and without the fix, so the change is pinned not to alter existing ordering.

Trac ticket: https://core.trac.wordpress.org/ticket/35144

Use of AI Tools

AI assistance: Yes
Tool(s): Claude
Model(s): Sonnet
Used for: test cases, code review


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

wp_generate_tag_cloud() re-sorts the terms in PHP after the database has
already returned them in collation order. The comparison callback
_wp_object_name_sort_cb() used strnatcasecmp(), which folds case only for
ASCII and ignores accents entirely, so non-ASCII terms were ordered by raw
byte value.

For Greek that split one alphabet into four consecutive groups: uppercase
accented, uppercase non-accented, lowercase accented and lowercase
non-accented. With the Latin terms on top, a tag cloud showed five groups
instead of the expected two.

Fold the name to lowercase and strip combining marks to build the sort key,
so case and accent variants of the same letter sort together and match the
ordering the database collation already provides. The sort key is left
decomposed because recomposing would restore the accents. A deterministic
strnatcmp() tie-break on the original name keeps the order well defined
when the folded keys are equal, and the callback result is cast to an int,
which is what comparison callbacks are expected to return.

mbstring and intl are not required by WordPress, so both folds are applied
only when the extension is available. The plain strtolower() fallback and
the unfolded fallback still keep case variants adjacent, which is the
larger part of the reported problem. Plain ASCII ordering is byte for byte
unchanged.

Props beerallica.
Fixes #35144.
Add coverage to the existing tag cloud test file for the name ordering in
wp_generate_tag_cloud().

Greek terms that differ only by case or by accents must stay in a single
group and must not be split apart by the comparison, and Latin terms must
still sort before Greek ones. A further test pins the ASCII ordering so the
change to the sort key is proven not to alter it, and an additional test
asserts the variants of a single letter keep a deterministic order.

The accent expectations require the intl extension, so those tests are
skipped when Normalizer is unavailable.

Fixes #35144.
@github-actions

Copy link
Copy Markdown

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.

Unlinked Accounts

The following contributors have not linked their GitHub and WordPress.org accounts: @skikken.

Contributors, please read how to link your accounts to ensure your work is properly credited in WordPress releases.

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

@github-actions

Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant