Skip to content

refactor(Core/Locale): Move acore_string to rows (breaking change) - #28281

Merged
Helias merged 2 commits into
azerothcore:masterfrom
nolt:acore-string-locale-migrate
Oct 31, 2026
Merged

Helias merged 2 commits into
azerothcore:masterfrom
nolt:acore-string-locale-migrate

Conversation

@nolt

@nolt nolt commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Note

Draft. This is the follow-up agreed in PR 28081 and PR 27958: acore_string stores its translations as rows in acore_string_locale, like every other *_locale table, instead of one column per language. It stays a draft until we have the list of modules that still write to the locale_* columns of acore_string (see Known Issues), so they can move to the table together with this change. It is not built or tested in-game yet.

Changes Proposed:

This PR proposes changes to:

  • Core (units, players, creatures, game systems).
  • Scripts (bosses, spell scripts, creature scripts).
  • Database (SAI, creatures, etc).

acore_string was the only localized table with one column per language. After PR 28081 a module that registers a new locale had to add its own locale_<code> column to a core table, and the loader had to look up in information_schema which columns exist before it could build its query. PR 27958 added acore_string_locale (entry, locale, content) next to the columns; this PR finishes the move, as discussed with Helias:

  • The pending world SQL moves the non-empty texts of the eight client locales (koKR, frFR, deDE, zhCN, zhTW, esES, esMX, ruRU) from acore_string to acore_string_locale and drops the eight columns. acore_string keeps entry and content_default (enUS).
  • A non-empty row that is already in acore_string_locale for one of these locales wins over the column, because that is what the loader picked until now. The SQL first copies such rows into the column, then replaces the rows of these locales with the column values. Empty strings and NULL are not copied, the loader skipped them too. Rows of other locales (e.g. a module's plPL) and rows whose entry is not in acore_string are not touched.
  • LoadAcoreStrings() reads enUS from acore_string and every other locale from acore_string_locale. The information_schema lookup and the query built at runtime are gone.

For a server without modules nothing changes in-game: every text the server loaded before the migration is loaded after it, from the table.

From now on a new or changed translation of an acore string is a DELETE + INSERT into acore_string_locale instead of an UPDATE of a locale_* column, the same as for the other *_locale tables.

AI-assisted Pull Requests

Important

Using AI tools to prepare pull requests is allowed, but it must be disclosed and it must follow our AC guidelines for AI Agentic Engineering (link below).

You are expected to fully understand the changes you submit and to be able to explain and justify them when maintainers ask.

  • AI tools (e.g. Claude, ChatGPT, or similar) were used entirely or partially to prepare this pull request. If checked, specify which tools and models below.
    • Tools/models used: Claude Code, Claude Opus 5.5 (claude-opus-5-5) produced the changes and this description. The self-review report will be added in a comment before the PR leaves draft.
  • I have read and understood the AC guidelines for AI Agentic Engineering

Issues Addressed:

  • None, there is no linked issue. Follow-up of PR 28081 and PR 27958, agreed with Helias there and on Discord.

SOURCE:

The changes have been validated through:

  • Live research (checked on live servers, e.g Classic WotLK, Retail, etc.)
  • Sniffs (remember to share them with the open source community!)
  • Video evidence, knowledge databases or other public sources (e.g forums, Wowhead, etc.)
  • The changes promoted by this pull request come partially or entirely from another project (cherry-pick). Cherry-picks must be committed using the proper --author tag in order to be accepted, thus crediting the original authors, unless otherwise unable to be found

None of the above applies: this is a schema change with no game data, and nothing is taken from another project.

Tests Performed:

This PR has been:

  • Tested in-game by the author.
  • Tested in-game by other community members/someone else other than the author/has been live on production servers.
  • This pull request requires further testing and may have edge cases to be tested.

Not built and not tested in-game yet (draft). So far:

  • Both codestyle checks pass (codestyle-sql.py, codestyle-cpp.py).
  • The SQL was run on MySQL 8.4 against the base acore_string plus acore_string_locale, with extra rows for the edge cases: a table row where the column is NULL, a table row next to a different column value, an empty table row next to a column value, an empty column, a row whose entry is not in acore_string, and plPL rows. For every entry and each of the eight locales, the text the loader would pick before the migration (non-empty row, else non-empty column) equals the row after it: 4142 texts, no missing and no extra rows. The orphan row and the plPL rows were left as they were.

How to Test the Changes:

  • This pull request can be tested by following the reproduction steps provided in the linked issue
  • This pull request requires further testing. Provide steps to test your changes. If it requires any specific setup e.g multiple players please specify it as well.
  1. Before applying the SQL, log in with a non-enUS client (e.g. deDE or esES) and note a few acore strings, e.g. .server info and the reply to a GM command run without a target.
  2. Apply the pending SQL and start the server: acore_string has only entry and content_default, the startup log loads the same number of acore strings as before, and Errors.log has no acore_string_locale errors.
  3. With the same client the texts from step 1 are still translated; an enUS client gets the English ones.
  4. Change a row in acore_string_locale for that locale and run .reload acore_string: the new text is shown.

Known Issues and TODO List:

  • Update the modules that write to the locale_* columns of acore_string (translation modules, modules that ship their own acore strings with translations). After this PR their SQL fails on the missing column, and a column that a module added for its own locale (the PR 28081 way) is no longer read. They need to write rows to acore_string_locale instead. The list of affected repositories is in a comment below.
  • Merge only when every module that writes to these columns has its change to acore_string_locale ready (a PR or a branch in each module), so they can be updated at the same time and none of them breaks in between.
  • Build, in-game test and self-review report before the PR leaves draft.
  • The base dump data/sql/base/db_world/acore_string.sql still has the columns; it is not edited by hand, the pending SQL applies on top of it.

How to Test AzerothCore PRs

When a PR is ready to be tested, it will be marked as [WAITING TO BE TESTED].

You can help by testing PRs and writing your feedback here on the PR's page on GitHub. Follow the instructions here:

http://www.azerothcore.org/wiki/How-to-test-a-PR

REMEMBER: when testing a PR that changes something generic (i.e. a part of code that handles more than one specific thing), the tester should not only check that the PR does its job (e.g. fixing spell XXX) but especially check that the PR does not cause any regression (i.e. introducing new bugs).

For example: if a PR fixes spell X by changing a part of code that handles spells X, Y, and Z, we should not only test X, but we should test Y and Z as well.

Move the texts of the eight client locales from the locale_* columns
of acore_string to acore_string_locale and drop the columns, so every
language, including the ones a module registers, is stored the same
way as in the other *_locale tables.

A non-empty row that was already in acore_string_locale is kept over
the column, as the loader did. LoadAcoreStrings() now reads enUS from
acore_string and all other locales from the table, so it no longer
needs to look up the existing columns in information_schema.
@nolt

nolt commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

Repositories of the azerothcore organization that need a change before this PR can be merged (default branches, checked 2026-10-27). They write to the locale_* columns of acore_string, which this PR drops:

  • mod-locale-itIT: adds its own locale_itIT column (data/sql/db_world/base/00_acore_string_itIT.sql) and fills it with UPDATE (data/sql/db_world/updates/rev_1793007124.sql).
  • skeleton-module: data/sql/db-world/skeleton_module_acore_string.sql inserts into the eight columns. It is the template for new modules, so the example should show acore_string_locale.
  • mod-arenaspectator, mod-pvp-quests: a leftover copy of the skeleton file above (data/sql/db-world/base/skeleton_module_acore_string.sql, "Hello World from Skeleton-Module!"); it can be removed.
  • mod-individual-xp: data/sql/db-world/updates/mod_individual_xp_acore_string.sql.
  • mod-congrats-on-level: data/sql/db-world/col_acore_string.sql.
  • mod-notify-muted: data/sql/db-world/mnm_acore_string.sql.
  • Keira3: the acore_string editor reads and writes the eight columns (libs/shared/acore-world-model/src/entities/acore-text.type.ts, libs/features/texts/src/acore-string/).

Fine as they are (only entry and content_default, or only DELETE): mod-azerothshard, mod-chromie-xp, mod-morph-all-players, mod-weekend-xp, mod-ptr-template, mod-transmog. Modules outside the organization were not checked.

If you know of other modules that write to these columns, please mention them here.

@Rorschach91

Copy link
Copy Markdown
Member

mod-locale-itIT: adds its own locale_itIT column (data/sql/db_world/base/00_acore_string_itIT.sql) and fills it with UPDATE (data/sql/db_world/updates/rev_1793007124.sql).

Ready.

@Rorschach91

Rorschach91 commented Oct 27, 2026 •

Copy link
Copy Markdown
Member

skeleton-module: data/sql/db-world/skeleton_module_acore_string.sql inserts into the eight columns. It is the template for new modules, so the example should show acore_string_locale.

mod-arenaspectator, mod-pvp-quests: a leftover copy of the skeleton file above (data/sql/db-world/base/skeleton_module_acore_string.sql, "Hello World from Skeleton-Module!"); it can be removed.

mod-individual-xp: data/sql/db-world/updates/mod_individual_xp_acore_string.sql.

mod-congrats-on-level: data/sql/db-world/col_acore_string.sql.

mod-notify-muted: data/sql/db-world/mnm_acore_string.sql.

Keira3: the acore_string editor reads and writes the eight columns (libs/shared/acore-world-model/src/entities/acore-text.type.ts, libs/features/texts/src/acore-string/).

CC @Helias

@Helias

Helias commented Oct 27, 2026

Copy link
Copy Markdown
Member

Work in progress...

Repo PR What it does
skeleton-module azerothcore/skeleton-module#37 Example string now puts its translations in acore_string_locale
mod-pvp-quests azerothcore/mod-pvp-quests#13 Removes the leftover skeleton SQL file
mod-individual-xp azerothcore/mod-individual-xp#51 Moves translations to rows, fixes the README uninstall query
mod-congrats-on-level azerothcore/mod-congrats-on-level#43 Moves translations to rows
mod-notify-muted azerothcore/mod-notify-muted#13 Moves translations to rows
mod-aoe-loot azerothcore/mod-aoe-loot#71 README only: describes the module_string translations the module actually uses
wiki azerothcore/wiki#1296 New acore_string_locale page (en/es/cn), updated acore_string page
Keira3 azerothcore/Keira3#3939 New Acore String Locale tab, locale fields removed from the Acore Strings editor

@Rorschach91

Copy link
Copy Markdown
Member

Work in progress...

Repo PR What it does
skeleton-module azerothcore/skeleton-module#37 Example string now puts its translations in acore_string_locale
mod-pvp-quests azerothcore/mod-pvp-quests#13 Removes the leftover skeleton SQL file
mod-individual-xp azerothcore/mod-individual-xp#51 Moves translations to rows, fixes the README uninstall query
mod-congrats-on-level azerothcore/mod-congrats-on-level#43 Moves translations to rows
mod-notify-muted azerothcore/mod-notify-muted#13 Moves translations to rows
mod-aoe-loot azerothcore/mod-aoe-loot#71 README only: describes the module_string translations the module actually uses
wiki azerothcore/wiki#1296 New acore_string_locale page (en/es/cn), updated acore_string page
Keira3 azerothcore/Keira3#3939 New Acore String Locale tab, locale fields removed from the Acore Strings editor

@Helias Did you open PRs for all the modules involved? If so, I think this PR could be marked as ready for review.

@Helias

Helias commented Oct 29, 2026

Copy link
Copy Markdown
Member

Work in progress...
Repo PR What it does
skeleton-module azerothcore/skeleton-module#37 Example string now puts its translations in acore_string_locale
mod-pvp-quests azerothcore/mod-pvp-quests#13 Removes the leftover skeleton SQL file
mod-individual-xp azerothcore/mod-individual-xp#51 Moves translations to rows, fixes the README uninstall query
mod-congrats-on-level azerothcore/mod-congrats-on-level#43 Moves translations to rows
mod-notify-muted azerothcore/mod-notify-muted#13 Moves translations to rows
mod-aoe-loot azerothcore/mod-aoe-loot#71 README only: describes the module_string translations the module actually uses
wiki azerothcore/wiki#1296 New acore_string_locale page (en/es/cn), updated acore_string page
Keira3 azerothcore/Keira3#3939 New Acore String Locale tab, locale fields removed from the Acore Strings editor

@Helias Did you open PRs for all the modules involved? If so, I think this PR could be marked as ready for review.

Yes, I think this PR can be set as ready for review

@Rorschach91
Rorschach91 marked this pull request as ready for review October 29, 2026 11:10
@github-actions github-actions Bot added DB related to the SQL database CORE Related to the core file-cpp Used to trigger the matrix build labels Oct 29, 2026
@coderabbitai

coderabbitai Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Walkthrough

The migration transfers non-empty values for eight locales from acore_string columns into acore_string_locale rows, then drops those columns. LoadAcoreStrings now reads entry and content_default from acore_string and continues loading translations from acore_string_locale.

Priority: Low

Merge Risk: Low · up to 1300c

The migration is mergeable with bounded deployment awareness: apply world updates before using the new loader and avoid running older binaries against the migrated database. Whitespace-only translations still need preservation to avoid unintended default-text fallback.

- [ ] Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Note

Follow-up findings are listed below so you can review them separately from inline comments.

Follow-up (1)
🟡 Minor (1)
Minor · Keep the old locale columns until the previous binary is… · rev_1793082529600203763.sql:25

data/sql/updates/pending_db_world/rev_1793082529600203763.sql:25
🩺 Stability & Availability | 🟡 Minor | ⚖️ Poor tradeoff

Keep the old locale columns until the previous binary is retired.

The base LoadAcoreStrings selects locale_koKR through locale_ruRU from acore_string. This migration drops those columns. A restarted or rolled-back old binary can fail that query, so it cannot load acore strings and exits at startup. Redeploying the new binary avoids the failure.


Review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 993b459 and 1300cc8.

Files selected for processing: 2
  • data/sql/updates/pending_db_world/rev_1793082529600203763.sql
  • src/server/game/Globals/ObjectMgr.cpp

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 7 remain after this review.

DELETE FROM `acore_string_locale` WHERE `locale` IN ('koKR', 'frFR', 'deDE', 'zhCN', 'zhTW', 'esES', 'esMX', 'ruRU') AND `entry` IN (SELECT `entry` FROM `acore_string`);
INSERT INTO `acore_string_locale` (`entry`, `locale`, `content`)
SELECT `entry`, 'koKR', `locale_koKR` FROM `acore_string` WHERE `locale_koKR` <> ''
UNION ALL SELECT `entry`, 'frFR', `locale_frFR` FROM `acore_string` WHERE `locale_frFR` <> ''

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor · Data Integrity & Integration · Quick win

Supported by static analysis

Script executed:

rg -n 'CREATE TABLE `acore_string_locale`|`content`|locale_koKR' data/sql/base/db_world/acore_string* data/sql/updates/pending_db_world/rev_1793082529600203763.sql

Repository: azerothcore/azerothcore-wotlk

Length of output: 2616


Script executed:

rg -n -F -e 'acore_string' -e 'LoadAcoreStrings' -e 'GetAcoreString' -- src/server/game/Globals/ObjectMgr.cpp src/server/game/Globals/ObjectMgr.h || test "$?" -eq 1
echo '--- schema acore_string ---'
sed -n '1,40p' data/sql/base/db_world/acore_string.sql
echo '--- schema acore_string_locale ---'
rg -n -F -A12 -e 'CREATE TABLE `acore_string_locale`' -- data/sql/base/db_world/ || test "$?" -eq 1
echo '--- ObjectMgr 40-70 ---'
sed -n '40,70p' src/server/game/Globals/ObjectMgr.cpp
echo '--- ObjectMgr 9570-9620 ---'
sed -n '9570,9620p' src/server/game/Globals/ObjectMgr.cpp

Repository: azerothcore/azerothcore-wotlk

Length of output: 5899


Script executed:

echo '--- head ObjectMgr 9620-9665 ---'
sed -n '9620,9665p' src/server/game/Globals/ObjectMgr.cpp
echo '--- head AddLocaleString ---'
rg -n -F -A12 -e 'AddLocaleString(' -- src/server/game/Globals/ObjectMgr.cpp || test "$?" -eq 1
echo '--- schema acore_string_locale ---'
rg -n -F -A10 -e 'acore_string_locale' -- data/sql/base/ || test "$?" -eq 1
echo '--- base merge ObjectMgr loader ---'
git show 993b45959556302a4eccded066085793b4b55322:src/server/game/Globals/ObjectMgr.cpp | rg -n -F -e 'LoadAcoreStrings' -e 'locale_' -e 'AddLocaleString' || test "$?" -eq 1

Repository: azerothcore/azerothcore-wotlk

Length of output: 16861


Script executed:

echo '--- base LoadAcoreStrings 9580-9645 ---'
git show 993b45959556302a4eccded066085793b4b55322:src/server/game/Globals/ObjectMgr.cpp | sed -n '9580,9645p'
echo '--- collation or column changes to acore_string locale columns in updates ---'
rg -n -i -e 'locale_koKR' -e 'acore_string`' -e 'acore_string ' -- data/sql/updates/ || test "$?" -eq 1

Repository: azerothcore/azerothcore-wotlk

Length of output: 6456


Web query:

MySQL 8.4 PAD SPACE attribute utf8mb4_unicode_ci trailing spaces comparison equal empty string

Result:

<source_evidence>
<source kind="open_page">
<title>MySQL :: MySQL 8.4 Reference Manual :: 12.10.1 Unicode Character Sets</title>
<location>https://dev.mysql.com/doc/refman/8.4/en/charset-unicode-sets.html</location>
<excerpt>citeturn1view0 [wordlim: 200] Crawled: today; Content type: text/html; Source: open({&quot;ref_id&quot;:&quot;turn0search1&quot;,&quot;lineno&quot;:null}); Total lines: 555
L274: MySQL implements the ``xxx`_unicode_ci` collations according to the Unicode Collation Algorithm (UCA) described at 【123†http://www.unicode.org/reports/tr10/†www.unicode.org】. The collation uses the version-4.0.0 UCA weight keys: 【124†http://www.unicode.org/Public/UCA/4.0.0/allkeys-4.0.0.txt†www.unicode.org】. The ``xxx`_unicode_ci` collations have only partial support for the Unicode Collation Algorithm. Some characters are not supported, and combining marks are not fully supported.
L275: This affects languages such as Vietnamese, Yoruba, and Navajo. A combined character is considered different from the same character written with a single unicode character in string comparisons, and the two characters are considered to have a different length (for example, as returned by the 【125†`CHAR_LENGTH()`】 function or in result set metadata).
L276: Unicode collations based on UCA versions higher than 4.0.0 include the version in the collation name. Examples:
L277: 
L278:   * `utf8mb4_unicode_520_ci` is based on UCA 5.2.0 weight keys (【126†http://www.unicode.org/Public/UCA/5.2.0/allkeys.txt†www.unicode.org】),
L279: 
L280:   * `utf8mb4_0900_ai_ci` is based on UCA 9.0.0 weight keys (【127†http://www.unicode.org/Public/UCA/9.0.0/allkeys.txt†www.unicode.org】).
L281: The 【128†`LOWER()`】 and 【129†`UPPER()`】 functions perform case folding according to the collation of their argument. A character that has uppercase and lowercase versions only in a Unicode version higher than 4.0.0 is converted by these functions only if the argument collation uses a high enough UCA version.
L282: #### Collation Pad Attributes
L283: 
L284: Collations based on UCA 9.0.0 and higher are faster than collations based on UCA versions prior to 9.0.0. They also have a pad attribute of `NO PAD`, in contrast to `PAD SPACE` as used in collations based on UCA versions prior to 9.0.0. For comparison of nonbinary strings, `NO PAD` collations treat spaces at the end of strings like any other character (see 【130†Trailing Space Handling in Comparisons】).
L285: To determine the pad attribute for a collation, use the `INFORMATION_SCHEMA` 【131†`COLLATIONS`】 table, which has a `PAD_ATTRIBUTE` column. For example:
L286: 
L287:     `mysql&gt; SELECT COLLATION_NAME, PAD_ATTRIBUTE
L288:            FROM INFORMATION_SCHEMA.COLLATIONS
L289:            WHERE CHARACTER_SET_NAME = &#39;utf8mb4&#39;;
L290:     +----------------------------+---------------+
L291:     | COLLATION_NAME             | PAD_ATTRIBUTE |
L292:     +----------------------------+---------------+
L293:     | utf8mb4_general_ci         | PAD SPACE     |
L294:     | utf8mb4_bin                | PAD SPACE     |
L295:     | utf8mb4_unicode_ci         | PAD SPACE     |
L296:     | utf8mb4_icelandic_ci       | PAD SPACE     |
L297:     ...
L298:     | utf8mb4_0900_ai_ci         | NO PAD        |
L299:     | utf8mb4_de_pb_0900_ai_ci   | NO PAD        |
L300:     | utf8mb4_is_0900_ai_ci      | NO PAD        |
L301:     ...
L302:     | utf8mb4_ja_0900_as_cs      | NO PAD        |
L303:     | utf8mb4_ja_0900_as_cs_ks   | NO PAD        |
L304:     | utf8mb4_0900_as_ci         | NO PAD        |
L305:     | utf8mb4_ru_0900_ai_ci      | NO PAD        |
L306:     | utf8mb4_ru_0900_as_cs      | NO PAD        |
L307:     | utf8mb4_zh_0900_as_cs      | NO PAD        |
L308:     | utf8mb4_0900_bin           | NO PAD        |
L309:     +----------------------------+---------------+`
L310: Comparison of nonbinary string values (`CHAR`, `VARCHAR`, and `TEXT`) that have a `NO PAD` collation differ from other collations with respect to trailing spaces. For example, `&#39;a&#39;` and `&#39;a &#39;` compare as different strings, not the same string. This can be seen using the binary collations for `utf8mb4`. The pad attribute for `utf8mb4_bin` is `PAD SPACE`, whereas for `utf8mb4_0900_bin` it is `NO PAD`.
L311: Consequently, operations involving `utf8mb4_0900_bin` do not add trailing spaces, and comparisons involving strings with trailing spaces may differ for the two collations:
L312: 
L313:     `mysql&gt; CREATE TABLE t1 (c CHAR(10) COLLATE utf8mb4_bin);
L314:     Query OK, 0 rows affected (0.03 sec)
L315: 
L316:     mysql&gt; INSERT INTO t1 VALUES(&#39;a&#39;);
L317:     Query OK, 1 row affected (0.01 sec)
L318: 
L319:     mysql&gt; SELECT * FROM t1 WHERE c = &#39;a &#39;;
L320:     +------+
L321:     | c    |
L322:     +------+
L323:     | a    |
L324:     +------+
L325:     1 row in set (0.00 sec)
L326: 
L327:     mysql&gt; ALTER TABLE t1 MODIFY c CHAR(10) COLLATE utf8mb4_0900_bin;
L328:     Query OK, 0 rows affected (0.02 sec)
L329:     Records: 0  Duplicates: 0  Warnings: 0
L330:     mysql&gt; SELECT * FROM t1 WHERE c = &#39;a &#39;;
L331:     Empty set (0.00 sec)`
L332: #### Language-Specific Collations
L333: MySQL implements language-specific Unicode collations if the ordering based only on the Unicode Collation Algorithm (UCA) does not work well for a language. Language-specific collations are UCA-based, with additional language tailoring rules. Examples of such rules appear later in this section.
L334: For questions about particular language orderings, 【132†http://unicode.org†unicode.org】 provides Common Locale Data Repository (CLDR) collation charts at 【133†http://www.unicode.org/cldr/charts/30/collation/index.html†www.unicode.org】.
L335: For example, the nonlanguage-specific `utf8mb4_0900_ai_ci` and language-specific `utf8mb4_`LOCALE`_0900_ai_ci` Unicode collations each have these characteristics:
L336: 
L337:   * The collation is based on UCA 9.0.0 and CLDR v30, is accent-insensitive and case-insensitive. These characteristics are indicated by `_0900`, `_ai`, and `_ci` in the collation name. Exception: `utf8mb4_la_0900_ai_ci` is not based on CLDR because Classical Latin is not defined in CLDR.
L338:   * The collation works for all characters in the range [U+0, U+10FFFF].
L339: 
L340:   * If the collation is not language specific, it sorts all characters, including supplementary characters, in default order (described following). If the collation is language specific, it sorts characters of the language correctly according to language-specific rules, and characters not in the language in default order.
L341:   * By default, the collation sorts characters having a code point listed in the DUCET table (Default Unicode Collation Element Table) according to the weight value assigned in the table. The collation sorts characters not having a code point listed in the DUCET table using their implicit weight value, which is constructed according to the UCA.
L342:   * For non-language-specific collations, characters in contraction sequences are treated as separate characters. For language-specific collations, contractions might change character sorting order.
L343: 
L344: A collation name that includes a locale code or language name shown in the following table is a language-specific collation. Unicode character sets may include collations for one or more of these languages.
L345: 
L346: Table 12.3 Unicode Collation Language Specifiers
L347: Language  | Language Specifier
L348: --- | ---
L349: Bosnian  | `bs`
L350: Bulgarian  | `bg`
L351: Chinese  | `zh`
L352: Classical Latin  | `la` or `roman`
L353: Croatian  | `hr` or `croatian`
L354: Czech  | `cs` or `czech`
L355: Danish  | `da` or `danish`
L356: Esperanto  | `eo` or `esperanto`
L357: Estonian  | `et` or `estonian`
L358: Galician  | `gl`
L359: German phone book order  | `de_pb` or `german2`
L360: Hungarian  | `hu` or `hungarian`
L361: Icelandic  | `is` or `icelandic`
L362: Japanese  | `ja`
L363: Latvian  | `lv` or `latvian`
L364: Lithuanian  | `lt` or `lithuanian`
L365: Mongolian  | `mn`
L366: Norwegian / Bokmål  | `nb`
L367: Norwegian / Nynorsk  | `nn`
L368: Persian  | `persian`
L369: Polish  | `pl` or `polish`
L370: Romanian  | `ro` or `romanian`
L371: Russian  | `ru`
L372: Serbian  | `sr`
L373: Sinhala  | `sinhala`
L374: Slovak  | `sk` or `slovak`
L375: Slovenian  | `sl` or `slovenian`
L376: Modern Spanish  | `es` or `spanish`
L377: Traditional Spanish  | `es_trad` or `spanish2`
L378: Swedish  | `sv` or `swedish`
L379: Turkish  | `tr` or `turkish`
L380: Vietnamese  | `vi` or `vietnamese`
L381: MySQL provides the Bulgarian collations `utf8mb4_bg_0900_ai_ci` and `utf8mb4_bg_0900_as_cs`.
L382: 
L383: Croatian collations are tailored for these Croatian letters: `Č`, `Ć`, `Dž`, `Đ`, `Lj`, `Nj`, `Š`, `Ž`.
L384: 
L385: MySQL provides the `utf8mb4_sr_latn_0900_ai_ci` and `utf8mb4_sr_latn_0900_as_cs` collations for Serbian and the `utf8mb4_bs_0900_ai_ci` and `utf8mb4_bs_0900_as_cs` collations for Bosnian, when these languages are written with the Latin alphabet.
L400: For example, `ß` is equal to `ss` in German and some other languages. `utf8mb4_unicode_ci` also supports contractions and ignorable characters. `utf8mb4_general_ci` is a legacy collation that does not support expansions, contractions, or ignorable characters. It can make only one-to-one comparisons between characters.
L401: To further illustrate, the following equalities hold in both `utf8mb4_general_ci` and `utf8mb4_unicode_ci` (for the effect of this in comparisons or searches, see 【41†Section 12.8.6, “Examples of the Effect of Collation”】):
L402: 
L403:     `Ä = A
L404:     Ö = O
L405:     Ü = U`
L406: 
L407: A difference between the collations is that this is true for `utf8mb4_general_ci`:
L408: 
L409:     `ß = s`
L410: 
L411: Whereas this is true for `utf8mb4_unicode_ci`, which supports the German DIN-1 ordering (also known as dictionary order):
L412: 
L413:     `ß = ss`
L414: MySQL implements language-specific Unicode collations if the ordering with `utf8mb4_unicode_ci` does not work well for a language. For example, `utf8mb4_unicode_ci` works fine for German dictionary order and French, so there is no need to create special `utf8mb4` collations.
L415: `utf8mb4_general_ci` also is satisfactory for both German and French, except that `ß` is equal to `s`, and not to `ss`. If this is acceptable for your application, you should use `utf8mb4_general_ci` because it is faster. If this is not acceptable (for example, if you require German dictionary order), use `utf8mb4_unicode_ci` because it is more accurate.
L416: If you require German DIN-2 (phone book) ordering, use the `utf8mb4_german2_ci` collation, which compares the following sets of characters equal:
L417: 
L418:     `Ä = Æ = AE
L419:     Ö = Œ = OE
L420:     Ü = UE
L421:     ß = ss`
L422: 
L423: `utf8mb4_german2_ci` is similar to `latin1_german2_ci`, but the latter does not compare `Æ` equal to `AE` or `Œ` equal to `OE`. There is no `utf8mb4_german_ci` corresponding to `latin1_german_ci` for German dictionary order because `utf8mb4_general_ci` suffices.
L424: #### Character Collating Weights
L425: 
L426: A character&#39;s collating weight is determined as follows:
L427: 
L428:   * For all Unicode collations except the `_bin` (binary) collations, MySQL performs a table lookup to find a character&#39;s collating weight.
L429: 
L430:   * For `_bin` collations except `utf8mb4_0900_bin`, the weight is based on the code point, possibly with leading zero bytes added.
L431: 
L432:   * For `utf8mb4_0900_bin`, the weight is the `utf8mb4` encoding bytes. The sort order is the same as for `utf8mb4_bin`, but much faster.
L433: Collating weights can be displayed using the 【134†`WEIGHT_STRING()`】 function. (See 【135†Section 14.8, “String Functions and Operators”】.) If a collation uses a weight lookup table, but a character is not in the table (for example, because it is a “new” character), collating weight determination becomes more complex:
L434: 
L435:   * For BMP characters in general collations (``xxx`_general_ci`), the weight is the code point.
L436:   * For BMP characters in UCA collations (for example, ``xxx`_unicode_ci` and language-specific collations), the following algorithm applies:
L437: 
L438:     `if (code &gt;= 0x3400 &amp;&amp; code &lt;= 0x4DB5)
L439:       base= 0xFB80; /* CJK Ideograph Extension */
L440:     else if (code &gt;= 0x4E00 &amp;&amp; code &lt;= 0x9FA5)
</excerpt>
</source>
<source kind="open_page">
<title>MySQL :: MySQL 8.4 Reference Manual :: 12.8.5 The binary Collation Compared to _bin Collations</title>
<location>https://dev.mysql.com/doc/refman/8.4/en/charset-binary-collations.html</location>
<excerpt>citeturn1view1 [wordlim: 200] Crawled: today; Content type: text/html; Source: open({&quot;ref_id&quot;:&quot;turn0search0&quot;,&quot;lineno&quot;:null}); Total lines: 412
L236: Nonbinary strings (as stored using the 【119†`CHAR`】, 【119†`VARCHAR`】, and 【118†`TEXT`】 data types) have a character set and collation other than `binary`. A given nonbinary character set can have several collations, each of which defines a particular comparison and sort order for the characters in the set. For most character sets, one of these is the binary collation, indicated by a `_bin` suffix in the collation name.
L238: The `binary` collation differs from `_bin` collations in several respects, discussed in the following sections:
L239: 
L240:   * 【120†The Unit for Comparison and Sorting】
L241: 
L242:   * 【121†Character Set Conversion】
L243: 
L244:   * 【122†Lettercase Conversion】
L245: 
L246:   * 【123†Trailing Space Handling in Comparisons】
L247: 
L248:   * 【124†Trailing Space Handling for Inserts and Retrievals】
L250: Binary strings are sequences of bytes. For the `binary` collation, comparison and sorting are based on numeric byte values. Nonbinary strings are sequences of characters, which might be multibyte. Collations for nonbinary strings define an ordering of the character values for comparison and sorting. For `_bin` collations, this ordering is based on numeric character code values, which is similar to ordering for binary strings except that character code values might be multibyte.
L251: #### Character Set Conversion
L252: 
L253: A nonbinary string has a character set and is automatically converted to another character set in many cases, even when the string has a `_bin` collation:
L254: 
L255:   * When assigning column values to another column that has a different character set:
L256: 
L257:     `UPDATE t1 SET utf8mb4_bin_column=latin1_column;
L258:     INSERT INTO t1 (latin1_column) SELECT utf8mb4_bin_column FROM t2;`
L259:   * When assigning column values for 【125†`INSERT`】 or 【126†`UPDATE`】 using a string literal:
L260: 
L261:     `SET NAMES latin1;
L262:     INSERT INTO t1 (utf8mb4_bin_column) VALUES (&#39;string-in-latin1&#39;);`
L263: 
L264:   * When sending results from the server to a client:
L265: 
L266:     `SET NAMES latin1;
L267:     SELECT utf8mb4_bin_column FROM t2;`
L268: 
L269: For binary string columns, no conversion occurs. For cases similar to those preceding, the string value is copied byte-wise.
L270: #### Lettercase Conversion
L271: Collations for nonbinary character sets provide information about lettercase of characters, so characters in a nonbinary string can be converted from one lettercase to another, even for `_bin` collations that ignore lettercase for ordering:
L272: 
L273:     `mysql&gt; SET NAMES utf8mb4 COLLATE utf8mb4_bin;
L274:     mysql&gt; SELECT LOWER(&#39;aA&#39;), UPPER(&#39;zZ&#39;);
L275:     +-------------+-------------+
L276:     | LOWER(&#39;aA&#39;) | UPPER(&#39;zZ&#39;) |
L277:     +-------------+-------------+
L278:     | aa          | ZZ          |
L279:     +-------------+-------------+`
L280: The concept of lettercase does not apply to bytes in a binary string. To perform lettercase conversion, the string must first be converted to a nonbinary string using a character set appropriate for the data stored in the string:
L281: 
L282:     `mysql&gt; SET NAMES binary;
L283:     mysql&gt; SELECT LOWER(&#39;aA&#39;), LOWER(CONVERT(&#39;aA&#39; USING utf8mb4));
L284:     +-------------+------------------------------------+
L285:     | LOWER(&#39;aA&#39;) | LOWER(CONVERT(&#39;aA&#39; USING utf8mb4)) |
L286:     +-------------+------------------------------------+
L287:     | aA          | aa                                 |
L288:     +-------------+------------------------------------+`
L289: #### Trailing Space Handling in Comparisons
L290: 
L291: MySQL collations have a pad attribute, which has a value of `PAD SPACE` or `NO PAD`:
L292: 
L293:   * Most MySQL collations have a pad attribute of `PAD SPACE`.
L294: 
L295:   * The Unicode collations based on UCA 9.0.0 and higher have a pad attribute of `NO PAD`; see 【53†Section 12.10.1, “Unicode Character Sets”】.
L296: For nonbinary strings (`CHAR`, `VARCHAR`, and `TEXT` values), the string collation pad attribute determines treatment in comparisons of trailing spaces at the end of strings:
L297: 
L298:   * For `PAD SPACE` collations, trailing spaces are insignificant in comparisons; strings are compared without regard to trailing spaces.
L299: 
L300:   * `NO PAD` collations treat trailing spaces as significant in comparisons, like any other character.
L301: The differing behaviors can be demonstrated using the two `utf8mb4` binary collations, one of which is `PAD SPACE`, the other of which is `NO PAD`. The example also shows how to use the `INFORMATION_SCHEMA` 【127†`COLLATIONS`】 table to determine the pad attribute for collations.
L302: 
L303:     `mysql&gt; SELECT COLLATION_NAME, PAD_ATTRIBUTE
L304:            FROM INFORMATION_SCHEMA.COLLATIONS
L305:            WHERE COLLATION_NAME LIKE &#39;utf8mb4%bin&#39;;
L306:     +------------------+---------------+
L307:     | COLLATION_NAME   | PAD_ATTRIBUTE |
L308:     +------------------+---------------+
L309:     | utf8mb4_bin      | PAD SPACE     |
L310:     | utf8mb4_0900_bin | NO PAD        |
L311:     +------------------+---------------+
L312:     mysql&gt; SET NAMES utf8mb4 COLLATE utf8mb4_bin;
L313:     mysql&gt; SELECT &#39;a &#39; = &#39;a&#39;;
L314:     +------------+
L315:     | &#39;a &#39; = &#39;a&#39; |
L316:     +------------+
L317:     |          1 |
L318:     +------------+
L319:     mysql&gt; SET NAMES utf8mb4 COLLATE utf8mb4_0900_bin;
L320:     mysql&gt; SELECT &#39;a &#39; = &#39;a&#39;;
L321:     +------------+
L322:     | &#39;a &#39; = &#39;a&#39; |
L323:     +------------+
L324:     |          0 |
L325:     +------------+`
L326: Note
L327: 
L328: “Comparison” in this context does not include the 【128†`LIKE`】 pattern-matching operator, for which trailing spaces are significant, regardless of collation.
L329: 
L330: For binary strings (`BINARY`, `VARBINARY`, and `BLOB` values), all bytes are significant in comparisons, including trailing spaces:
L331: 
L332:     `mysql&gt; SET NAMES binary;
L333:     mysql&gt; SELECT &#39;a &#39; = &#39;a&#39;;
L334:     +------------+
L335:     | &#39;a &#39; = &#39;a&#39; |
L336:     +------------+
L337:     |          0 |
L338:     +------------+`
L339: #### Trailing Space Handling for Inserts and Retrievals
L340: 
L341: `CHAR(`N`)` columns store nonbinary strings `N` characters long. For inserts, values shorter than `N` characters are extended with spaces. For retrievals, trailing spaces are removed.
L342: `BINARY(`N`)` columns store binary strings `N` bytes long. For inserts, values shorter than `N` bytes are extended with `0x00` bytes. For retrievals, nothing is removed; a value of the declared length is always returned.
L343: 
L344:     `mysql&gt; CREATE TABLE t1 (
L345:              a CHAR(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin,
L346:              b BINARY(10)
L347:            );
L348:     mysql&gt; INSERT INTO t1 VALUES (&#39;x&#39;,&#39;x&#39;);
L349:     mysql&gt; INSERT INTO t1 VALUES (&#39;x &#39;,&#39;x &#39;);
L350:     mysql&gt; SELECT a, b, HEX(a), HEX(b) FROM t1;
L351:     +------+------------------------+--------+----------------------+
L352:     | a    | b                      | HEX(a) | HEX(b)               |
L353:     +------+------------------------+--------+----------------------+
L354:     | x    | 0x78000000000000000000 | 78     | 78000000000000000000 |
L355:     | x    | 0x78200000000000000000 | 78     | 78200000000000000000 |
L356:     +------+------------------------+--------+----------------------+`
L357: 【39†PREV】   【129†HOME】   【35†UP】   【41†NEXT】
L358: 
L359: Related Documentation
L360: 
L361: 【104†MySQL 8.4 Release Notes】
L362: 
L363: Download this Manual
L364: 
L365: 【105†PDF (US Ltr)†downloads.mysql.com】 - 40.2Mb
L366: 【106†PDF (A4)†downloads.mysql.com】 - 40.3Mb
L367: 【107†Man Pages (TGZ)†downloads.mysql.com】 - 261.9Kb
L368: 【108†Man Pages (Zip)†downloads.mysql.com】 - 367.5Kb
L369: 【109†Info (Gzip)†downloads.mysql.com】 - 4.0Mb
L370: 【110†Info (Zip)†downloads.mysql.com】 - 4.0Mb
L371: 
L372: Contact MySQL Sales
L373: USA/Canada: +1-866-221-0634   (【130†More Countries »†www.mysql.com】)
L374:   【131†© 2026 Oracle†www.oracle.com】
L375: 
L376:   * 【132†Products†www.mysql.com】
L377:   * 【133†MySQL HeatWave†www.mysql.com】
L378:   * 【134†MySQL Enterprise Edition†www.mysql.com】
L379:   * 【135†MySQL Standard Edition†www.mysql.com】
L380:   * 【136†MySQL Classic Edition†www.mysql.com】
L381:   * 【137†MySQL Cluster CGE†www.mysql.com】
L382:   * 【138†MySQL Embedded (OEM/ISV)†www.mysql.com】
L383: 
L384:   * 【139†Services†www.mysql.com】
L385:   * 【140†Training†www.mysql.com】
L386:   * 【141†Certification†www.mysql.com】
L387:   * 【142†Support†www.mysql.com】
</excerpt>
</source>
<source kind="search">
<title>MySQL :: MySQL 8.4 Reference Manual :: 12.10.1 Unicode Character Sets</title>
<location>https://dev.mysql.com/doc/refman/8.4/en/charset-unicode-sets.html</location>
<excerpt>citeturn0search1 [wordlim: 200] Crawled: today;     | utf8mb4_unicode_ci         | PAD SPACE     | ... Comparison of nonbinary string values (`CHAR`, `VARCHAR`, and `TEXT`) that have a `NO PAD` collation differ from other collations with respect to trailing spaces. ... MySQL implements language-specific Unicode collations if the ordering with `utf8mb4_unicode_ci` does not work well for a language.
</excerpt>
</source>
<source kind="search">
<title>MySQL :: MySQL 8.4 Reference Manual :: 12.8.5 The binary Collation Compared to _bin Collations</title>
<location>https://dev.mysql.com/doc/refman/8.4/en/charset-binary-collations.html</location>
<excerpt>citeturn0search0 [wordlim: 200] Crawled: 6 days ago; For nonbinary strings (`CHAR`, `VARCHAR`, and `TEXT` values), the string collation pad attribute determines treatment in comparisons of trailing spaces at the end of strings: ... The differing behaviors can be demonstrated using the two `utf8mb4` binary collations, one of which is `PAD SPACE`, the other of which is `NO PAD`. ...     mysql&gt; SET NAMES utf8mb4 COLLATE utf8mb4_bin;
</excerpt>
</source>
<source kind="search">
<title>MySQL Globalization</title>
<location>https://downloads.mysql.com/docs/mysql-g11n-excerpt-8.0-en.a4.pdf</location>
<excerpt>citeturn0search12 [wordlim: 200] Published: last year; mysql&gt; SELECT LOWER(&#39;aA&#39;), LOWER(CONVERT(&#39;aA&#39; USING utf8mb4)); ... For nonbinary strings (CHAR, VARCHAR, and TEXT values), the string collation pad attribute determines treatment in comparisons of trailing spaces at the end of strings: ... The differing behaviors can be demonstrated using the two utf8mb4 binary collations, one of which is PAD SPACE, the other of which is NO PAD. ... “Comparison” in this context does not include the LIKE pattern-matching operator, for which trailing spaces are significant, regardless of collation.
</excerpt>
</source>
<omitted_sources count="18">Additional source records were omitted from this bounded result.</omitted_sources>
</source_evidence>

Citations:

- 1: https://dev.mysql.com/doc/refman/8.4/en/charset-unicode-sets.html
- 2: https://dev.mysql.com/doc/refman/8.4/en/charset-unicode-sets.html

Keep whitespace-only locale texts in the migration.

Under PAD SPACE, ' ' <> '' is false, so the migration drops whitespace-only texts. The loader kept them, because AddLocaleString skips only empty strings. Use LENGTH(...) > 0 in the eight UPDATE joins and the UNION ALL filters.


QueryResult result = WorldDatabase.Query(sql);
// 0 1
QueryResult result = WorldDatabase.Query("SELECT entry, content_default FROM acore_string");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor · Functional Correctness · Quick win

Supported by static analysis

Script executed:

rg -n 'LoadAcoreStrings\(|Updates\.Enable|Database.*Update|Apply.*Update' src/server src/common | head -100

Repository: azerothcore/azerothcore-wotlk

Length of output: 6024


Script executed:

set -e
echo "=== HEAD: ObjectMgr.cpp LoadAcoreStrings ==="
nl -ba src/server/game/Globals/ObjectMgr.cpp | sed -n '9575,9640p'
echo "=== BASE: ObjectMgr.cpp LoadAcoreStrings ==="
git show 993b45959556302a4eccded066085793b4b55322:src/server/game/Globals/ObjectMgr.cpp | nl -ba | sed -n '9575,9660p'
echo "=== Migration SQL ==="
nl -ba data/sql/updates/pending_db_world/rev_1793082529600203763.sql | sed -n '1,40p'

Repository: azerothcore/azerothcore-wotlk

Length of output: 9526


Script executed:

echo "=== World.cpp startup ordering ==="
nl -ba src/server/game/World/World.cpp | sed -n '340,365p'
echo "=== Main.cpp DB loader ordering ==="
nl -ba src/server/apps/worldserver/Main.cpp | sed -n '440,505p'
echo "=== DBUpdater UpdateDatabases failure handling ==="
nl -ba src/server/database/Database/DatabaseLoader.cpp | sed -n '195,260p'
echo "=== GetAcoreString ==="
rg -n -F -- 'GetAcoreString' src/server/game/Globals/ObjectMgr.cpp src/server/game/Globals/ObjectMgr.h

Repository: azerothcore/azerothcore-wotlk

Length of output: 6235


Script executed:

set -e
echo "=== DatabaseLoader Load and update gating ==="
nl -ba src/server/database/Database/DatabaseLoader.cpp | sed -n '50,130p'
echo "=== Update flag constants ==="
rg -n -F -- 'DATABASE_MASK_ALL' src/server/database/Database/DatabaseLoader.h || test "$?" -eq 1
rg -n -F -- 'DATABASE_WORLD' src/server/database/Database/DatabaseLoader.h src/server/database/Database/DatabaseLoader.cpp || test "$?" -eq 1
echo "=== GetAcoreString locale fallback ==="
nl -ba src/server/game/Globals/ObjectMgr.cpp | sed -n '9641,9660p'

Repository: azerothcore/azerothcore-wotlk

Length of output: 4939


Keep reading existing locale_<code> columns in LoadAcoreStrings.

With world updates disabled through Updates.EnableDatabases, the pending migration does not run. LoadAcoreStrings then ignores non-empty locale_frFR values, so French clients receive the default text. The previous loader read those columns. Read existing locale_<code> columns again, and let acore_string_locale override them.

@Helias Helias changed the title refactor(Core/Locale): Move acore_string to rows refactor(Core/Locale): Move acore_string to rows (breaking change) Oct 29, 2026
@Helias
Helias merged commit b1c193d into azerothcore:master Oct 31, 2026
25 of 28 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CORE Related to the core DB related to the SQL database file-cpp Used to trigger the matrix build

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants