feat(localization): complete and correct the Russian locale (#130)

* feat(localization): complete and correct the Russian locale

The Russian locale was 99 keys behind enUS, so a Russian client kept the
English strings for ClassicAPI's own options: the AddOn Options title and
its game-menu button, every page name, header, control label and tooltip,
the AoE Loot binding, and the generic strings the options pages gained
(FOCUS_TOKEN_NOT_FOUND, LUA_ERROR, RELOADUI, PLAYER_LIST_DELIMITER,
SYMBOLIC_NEGATIVE_NUMBER, SYMBOLIC_POSITIVE_NUMBER and the money texture
formats). All 327 keys are now present, in enUS order, so the two files
diff key by key.

Focus follows the client's modern terms — «Взять в фокус», «Сбросить
фокус», «Выбрать фокус» and «Взять цель в фокус» — matching the existing
/фокус and /сбросфокуса aliases. The rest uses the 1.12 Russian client's
vocabulary where it has one (модификация, цель, панель команд, время
восстановления, табличка с именем) and «динамическое выделение» /
«мягкая цель» for action targeting. The money strings keep their |4
plural forms, which GetCoinText expects.

Fixes:

- LARGE_NUMBER_SEPERATOR was "", which turned digit grouping off. It is
  now a space, as in the tooltip's 1 234 567.
- FIRST/SECOND_NUMBER_CAP and their _NO_SPACE twins carry the leading
  space, because Abbreviate concatenates the number and the suffix
  directly.
- EQUIPMENT_MANAGER_ITEMS_MISSING_TOOLTIP printed a slot number where
  the missing count belongs; it now takes the missing count.
- SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL was missing a space before
  «или нажмите».
- SET_FRIENDNOTE_LABEL had a |3-5 declension token, which 1.12 renders
  literally.
- ITEM_SLOTS_IGNORED and ITEMS_NOT_IN_INVENTORY had wrong third plural
  forms, ITEM_MOD_HIT_MELEE_RATING_SHORT had a double space, and the flat
  stat labels (Защита, Уклонение, Парирование, Блок) read wrong after a
  signed value, so they use the same «к …» form as the other ITEM_MOD
  labels.

The TOC gains ## Notes-ruRU next to ## Notes.

* fix(localization): restore retail ruRU focus, number and log strings

Follow-up to the Russian locale completion, which had changed several
strings that the retail ruRU client already translates. Each is now
the retail text, or a correction where the new wording was wrong:

- Focus: the FOCUSTARGET / TARGETFOCUS binding labels were swapped
  (FOCUSTARGET runs FocusUnit("target"), TARGETFOCUS runs
  TargetUnit("focus")). All focus strings use retail ruRU's terms
  again: Запомнить цель, Восстановить цель, Отменить запоминание цели,
  Запомненная цель, <фокус отсутствует>.
- EVENTTRACE_LOG_DISCARD is the message logged after the log is
  discarded; it had become an option-style label. Retail: Журнал
  сброшен. EVENTTRACE_BUTTON_DISCARD_FILTER read "cancel all"; retail:
  Сбросить все.
- FIRST/SECOND_NUMBER_CAP and their _NO_SPACE variants go back to
  retail Т / М. AbbreviateNumbers appends the _NO_SPACE suffix
  directly by design ("1.2K"), so " тыс." widened compact numbers.
- LARGE_NUMBER_SEPERATOR goes back to retail "" (as frFR). A plain
  space also let a number wrap across lines. The separate-large-numbers
  tooltip drops its "1 234 567" example, which no longer applies.
- The loose-target option read "whom the soft target follows". The
  normal target is the one that follows, so it is now "обычная цель",
  and the choices name the soft enemy / soft friend.

* fix(localization): pick language rules by locale name, not 4.3.4's index

1.12.1's locale index (VAR_LOCALE_INDEX, 0xC0E080) is the Wow.ini
Language+Country tag's position in the eight-entry name table at
0x8558A4 (enUS koKR frFR deDE zhCN zhTW esES xxYY), stored once at
0x402562 and 0 when nothing matches. It is never 8. The Russian branches
ported from 4.3.4 switched on 4.3.4's own slot numbers (esMX 7, ruRU 8),
so they could never run: a client whose GetLocale() is "ruRU" fell
through to the default plural rule ("5 предмета") and got no ё/е folding
in bag search.

Localization::Locale::Client() reads the same entry Script_GetLocale
(0x48D8B0) pushes, table[index], and Is(code) compares against it. The
language-rule sites now go through it:

- Text::Grammar's plural index (|4 expansion) keys koKR/frFR/zhCN/zhTW
  and ruRU by name.
- Container::Search picks its accent-folding set by name, as a Folding
  value instead of an index switch.
- Localization::Numbers' ClientIsAsian reuses IsAsianLocale on the name.
- AddOns::TocRewrite drops its private copy of the reader, and the
  slot-8 clamp, which guarded an index that cannot occur.

The DBC column reads keep using the index, which is what it is for.

---------

Co-authored-by: Brues <5278969+brues-code@users.noreply.github.com>
This commit is contained in:
shikulja
2026-11-06 10:25:00 -06:00
committed by GitHub
co-authored by Brues
parent d825bcbdda
commit ebfd9d034d
9 changed files with 271 additions and 101 deletions
+10 -7
View File
@@ -8650,17 +8650,20 @@ enum Offsets {
// unit-event observers. Matches nampower's `CGRaidInfo_m_raidtargets`.
VAR_RAID_TARGET_GUIDS = 0x00B71368,
VAR_LOCALE_INDEX = 0x00C0E080, // 0..8, picks one of the 9 localized strings
// 0..7: start-up stores the Wow.ini Language+Country tag's position in
// VAR_LOCALE_NAME_TABLE (0x402562), 0 when it matches none (enGB).
// Picks a DBC localized-string column. NOT 4.3.4's nine-slot locale
// order — language rules go by name (Localization::Locale).
VAR_LOCALE_INDEX = 0x00C0E080,
// Locale-name string table — a parallel `const char *[8]` indexed by
// VAR_LOCALE_INDEX, holding the exact codes `GetLocale()` returns:
// enUS / koKR / frFR / deDE / zhCN / zhTW / esES / xxYY. This is the
// source `Script_GetLocale` (FUN_0048d8b0) reads:
// enUS / koKR / frFR / deDE / zhCN / zhTW / esES / xxYY on a stock
// client. This is the source `Script_GetLocale` (FUN_0048d8b0) reads:
// lua_pushstring(L, (&PTR_DAT_008558a4)[DAT_00c0e080]);
// Verified by resolving all 8 entries from the binary. Index range is
// 0..7 (VAR_LOCALE_INDEX is documented 0..8, but slot 8 overreads into
// the string pool — clamp to 0..7 before reading). Used by the TOC
// `[TextLocale]` / `[AllowLoadTextLocale]` directives (AddOns::TocRewrite).
// and the list SetAddOnMetadata (0x51D580) matches a TOC key's
// `-xxXX` suffix against. Verified by resolving all 8 entries from the
// binary. Read through Localization::Locale::Client().
VAR_LOCALE_NAME_TABLE = 0x008558A4,
LUA_IS_NUMBER = 0x6F34D0,
+4 -13
View File
@@ -14,6 +14,7 @@
#include "addons/EngineIO.h"
#include "addons/TocRewrite.h"
#include "addons/Toc.h"
#include "localization/Locale.h"
#include "Offsets.h"
#include "turtle/Detect.h"
@@ -34,17 +35,7 @@ using AddOns::Toc::Lower;
bool IsSpace(char c) { return c == ' ' || c == '\t'; }
// The client locale code ("enUS", "frFR", …) — the exact string
// GetLocale() returns (Offsets.h VAR_LOCALE_NAME_TABLE derivation).
const char *ClientLocale() {
uint32_t idx = *reinterpret_cast<const uint32_t *>(
static_cast<uintptr_t>(Offsets::VAR_LOCALE_INDEX));
if (idx > 7) idx = 0; // slot 8 overreads the string pool — clamp
const char *const *table = reinterpret_cast<const char *const *>(
static_cast<uintptr_t>(Offsets::VAR_LOCALE_NAME_TABLE));
const char *s = table[idx];
return (s != nullptr) ? s : "enUS";
}
using Localization::Locale::Client;
// True iff the comma-separated `list` (spaces allowed) contains `token` (CI).
bool ListContains(const char *list, size_t listLen, const char *token) {
@@ -66,7 +57,7 @@ bool ListContains(const char *list, size_t listLen, const char *token) {
const char *VariableValue(const char *head, size_t headLen) {
if (EqCI(head, headLen, "Family")) return "Classic";
if (EqCI(head, headLen, "Game")) return "Vanilla";
if (EqCI(head, headLen, "TextLocale")) return ClientLocale();
if (EqCI(head, headLen, "TextLocale")) return Client();
return nullptr;
}
@@ -158,7 +149,7 @@ bool EvalCondition(const char *head, size_t headLen, const char *args,
if (EqCI(head, headLen, "ExcludeLoadGameType"))
return !ListContainsGameType(args, argsLen);
if (EqCI(head, headLen, "AllowLoadTextLocale"))
return ListContains(args, argsLen, ClientLocale());
return ListContains(args, argsLen, Client());
if (EqCI(head, headLen, "AllowLoad"))
return ListContains(args, argsLen, "game"); // addon files: in-game only
return false;
+31 -13
View File
@@ -55,6 +55,7 @@
#include "item/Location.h"
#include "item/Record.h"
#include "item/TypeInfo.h"
#include "localization/Locale.h"
#include "unit/Identity.h"
#include <algorithm>
@@ -86,12 +87,29 @@ char16_t ToLower(char16_t c) {
return c;
}
// Appends the lowercased `c` with 4.3.4's accent folding for `locale`
// (frFR 2, deDE 3, esES/esMX 6/7, ruRU 8); æ, œ and ß become two letters.
// Returns the new length.
size_t AppendFolded(int locale, char16_t c, char16_t *out, size_t n) {
switch (locale) {
case 2: // frFR
// 4.3.4's accent-folding sets. 4.3.4 picks one by its locale index (frFR 2,
// deDE 3, esES/esMX 6/7, ruRU 8); 1.12's index has other slots, so the set
// is picked by locale name.
enum class Folding { None, French, German, Spanish, Russian };
Folding ClientFolding() {
using Localization::Locale::Is;
if (Is("frFR"))
return Folding::French;
if (Is("deDE"))
return Folding::German;
if (Is("esES") || Is("esMX"))
return Folding::Spanish;
if (Is("ruRU"))
return Folding::Russian;
return Folding::None;
}
// Appends the lowercased `c` with `folding`'s accent folding; æ, œ and ß
// become two letters. Returns the new length.
size_t AppendFolded(Folding folding, char16_t c, char16_t *out, size_t n) {
switch (folding) {
case Folding::French:
switch (c) {
case 0xE0: case 0xE2: case 0xE4: c = u'a'; break;
case 0xE6: out[n++] = u'a'; c = u'e'; break;
@@ -103,14 +121,13 @@ size_t AppendFolded(int locale, char16_t c, char16_t *out, size_t n) {
case 0xF9: case 0xFA: case 0xFB: case 0xFC: c = u'u'; break;
}
break;
case 3: // deDE
case Folding::German:
if (c == 0xDF) {
out[n++] = u's';
c = u's';
}
break;
case 6: // esES
case 7: // esMX
case Folding::Spanish:
switch (c) {
case 0xE1: c = u'a'; break;
case 0xE9: c = u'e'; break;
@@ -120,10 +137,12 @@ size_t AppendFolded(int locale, char16_t c, char16_t *out, size_t n) {
case 0xF1: c = u'n'; break;
}
break;
case 8: // ruRU
case Folding::Russian:
if (c == 0x451)
c = 0x435;
break;
case Folding::None:
break;
}
out[n++] = c;
return n;
@@ -141,11 +160,10 @@ size_t Fold(const char *text, char16_t (&out)[kFoldCap]) {
// One unit short of the buffer: a full conversion stores no terminator.
reinterpret_cast<Convert_t>(Offsets::FUN_SUNI_CONVERT_UTF8_TO_16)(
wide, kFoldCap - 1, text, 0x7FFFFFFF, nullptr, nullptr);
const int locale =
*reinterpret_cast<const int *>(static_cast<uintptr_t>(Offsets::VAR_LOCALE_INDEX));
const Folding folding = ClientFolding();
size_t n = 0;
for (size_t i = 0; wide[i] != 0 && n + 2 < kFoldCap; ++i)
n = AppendFolded(locale, ToLower(wide[i]), out, n);
n = AppendFolded(folding, ToLower(wide[i]), out, n);
out[n] = 0;
return n;
}
@@ -1,6 +1,7 @@
## Interface: 11200
## Title: !!!ClassicAPI
## Notes: Library to emulate aspects of the modern WoW API on 1.12.1 / Lua 5.0.
## Notes-ruRU: Библиотека, эмулирующая аспекты современного API WoW на 1.12.1 / Lua 5.0.
## Version: DEV
## Author: Brues
## X-Website: https://github.com/brues-code/ClassicAPI
+150 -52
View File
@@ -4,17 +4,109 @@ ACTION_BUTTON_USE_KEY_DOWN = "Способности при нажатии кл
ACTION_TARGETING_OPTION = "Разрешить динамическое выделение";
ADDON_MEM_KB_ABBR = "(%.0f КБ) %s";
ADDON_MEM_MB_ABBR = "(%.2f МБ) %s";
ADDON_OPTIONS = "Параметры модификаций";
BINDING_NAME_AOELOOT = "Сбор добычи по области";
BINDING_NAME_FOCUSTARGET = "Запомненная цель";
BINDING_NAME_INTERACTMOUSEOVER = "Взаимодействовать указателем мыши";
BINDING_NAME_INTERACTMOUSEOVER = "Взаимодействовать с объектом под курсором";
BINDING_NAME_INTERACTTARGET = "Взаимодействовать с целью";
BINDING_NAME_TARGETFOCUS = "Восстановить цель";
CLASSICAPI_ACTION_BARS = "Панели команд";
CLASSICAPI_ACTION_TARGETING = "Динамическое выделение";
CLASSICAPI_ADVANCED = "Дополнительно";
CLASSICAPI_ALL_SOFT_TARGET_ICONS = "Значки для всех мягких целей";
CLASSICAPI_AUTO_DISMOUNT = "Автоматически спешиваться";
CLASSICAPI_AUTO_DISMOUNT_TOOLTIP = "Спешиваться при применении заклинания, использовании объекта, сборе добычи, атаке или открытии карты полетов, если вы едете верхом.";
CLASSICAPI_AUTO_STAND = "Автоматически вставать";
CLASSICAPI_AUTO_STAND_TOOLTIP = "Вставать при применении заклинания, сборе добычи или открытии карты полетов, если вы сидите.";
CLASSICAPI_AUTO_UNSHIFT = "Автоматически снимать облик";
CLASSICAPI_AUTO_UNSHIFT_TOOLTIP = "Покидать свой облик при применении заклинания, которое в нем недоступно, разговоре с NPC, использовании объекта или предмета, а также при открытии карты полетов.";
CLASSICAPI_BAGS_AND_ITEMS = "Сумки и предметы";
CLASSICAPI_CASTING_AND_ATTACKING = "Заклинания и атаки";
CLASSICAPI_COMBAT_LOG_RETENTION = "Хранение журнала боя";
CLASSICAPI_COMBAT_LOG_RETENTION_TOOLTIP = "Сколько секунд журнал боя хранит записи для модификаций.";
CLASSICAPI_CURSOR = "Курсор";
CLASSICAPI_DIRECTION_ANY = "Любое направление";
CLASSICAPI_DIRECTION_CONE = "В пределах 30 градусов";
CLASSICAPI_DIRECTION_FRONT = "Прямо перед собой";
CLASSICAPI_ENEMIES = "Противники";
CLASSICAPI_ENEMY_DIRECTION = "Направление на противника";
CLASSICAPI_ENEMY_ICON = "Показывать значок противника";
CLASSICAPI_ENEMY_ICON_TOOLTIP = "Показывает значок над противником, которого выбирает динамическое выделение.";
CLASSICAPI_ENEMY_RANGE = "Дистанция до противников";
CLASSICAPI_EXPANDED_CURSORS = "Показывать расширенные курсоры";
CLASSICAPI_EXPANDED_CURSORS_TOOLTIP = "При наведении на NPC или объект курсор показывает, что с ним можно сделать. Например, наковальню для ремонта, ! или ? для заданий и крючок для вашего поплавка.";
CLASSICAPI_FRIEND_DIRECTION = "Направление на союзника";
CLASSICAPI_FRIEND_ICON = "Показывать значок союзника";
CLASSICAPI_FRIEND_ICON_TOOLTIP = "Показывает значок над дружественной целью, которую выбирает дружественное динамическое выделение.";
CLASSICAPI_FRIEND_RANGE = "Дистанция до союзников";
CLASSICAPI_FRIEND_TARGETING = "Включить дружественное динамическое выделение";
CLASSICAPI_FRIEND_TARGETING_TOOLTIP = "Выбирает ближайшую дружественную цель в поле зрения. Если у вас нет цели, полезные заклинания применяются к ней.";
CLASSICAPI_FRIENDS = "Союзники";
CLASSICAPI_GAME_OBJECT_ICON = "Показывать значок взаимодействия на объектах";
CLASSICAPI_GAME_OBJECT_ICON_TOOLTIP = "Также показывает значок взаимодействия над объектами, которые нельзя выбрать целью, например почтовыми ящиками и сундуками.";
CLASSICAPI_HIDE_EQUIPMENT_SETS = "Скрывать комплекты экипировки на предметах";
CLASSICAPI_HIDE_EQUIPMENT_SETS_TOOLTIP = "Убирает строку с комплектами экипировки из подсказок предметов.";
CLASSICAPI_ICON_SIZE = "Размер значков";
CLASSICAPI_ICON_SIZE_TOOLTIP = "Размер значков взаимодействия, противников и союзников.";
CLASSICAPI_IGNORE_BACKPACK = "Не сортировать рюкзак";
CLASSICAPI_IGNORE_BACKPACK_TOOLTIP = "При сортировке сумок предметы в рюкзаке остаются на своих местах.";
CLASSICAPI_IGNORE_BANK = "Не сортировать основной банк";
CLASSICAPI_IGNORE_BANK_TOOLTIP = "При сортировке банка предметы в основных ячейках банка остаются на своих местах.";
CLASSICAPI_INTERACT_DIRECTION = "Направление взаимодействия";
CLASSICAPI_INTERACT_DIRECTION_TOOLTIP = "Где должен находиться NPC или объект, чтобы его выбрала клавиша взаимодействия. Направление определяется камерой, а не персонажем.";
CLASSICAPI_INTERACT_ICON = "Показывать значок взаимодействия";
CLASSICAPI_INTERACT_ICON_TOOLTIP = "Показывает значок над NPC или объектом, которые использует клавиша взаимодействия.";
CLASSICAPI_INTERACT_KEY = "Клавиша взаимодействия";
CLASSICAPI_INTERACT_RANGE = "Дистанция взаимодействия";
CLASSICAPI_INTERFACE = "Интерфейс";
CLASSICAPI_KEEP_ACTION_TARGETING = "Сохранять динамическое выделение";
CLASSICAPI_KEEP_ACTION_TARGETING_TOOLTIP = "Динамическое выделение продолжает выбирать противников и союзников, пока у вас есть цель, выбранная вручную.";
CLASSICAPI_LOOSE_TARGET_FOLLOWS = "За кем следует обычная цель";
CLASSICAPI_LOOSE_TARGET_FOLLOWS_ENEMY = "За мягкой целью противника";
CLASSICAPI_LOOSE_TARGET_FOLLOWS_FRIEND = "За мягкой целью союзника";
CLASSICAPI_LOOSE_TARGET_FOLLOWS_NONE = "Ни за кем";
CLASSICAPI_LOOSE_TARGET_FOLLOWS_TOOLTIP = "Цель, выбранная динамическим выделением, меняется вместе с мягкой целью противника или союзника. Цель, выбранная вручную, сохраняется.";
CLASSICAPI_LOW_PRIORITY_ICONS = "Показывать значки низкого приоритета";
CLASSICAPI_LOW_PRIORITY_ICONS_TOOLTIP = "Также показывает значок над целью, с которой нельзя ничего сделать, например над членом группы. Значок выглядит как обычный указатель.";
CLASSICAPI_MATCH_INSIDE_NAMES = "Учитывать правки внутри имени";
CLASSICAPI_MATCH_INSIDE_NAMES_TOOLTIP = "Когда вы вводите текст в середине имени, предлагаемые имена также учитывают буквы после курсора.";
CLASSICAPI_MATCH_THE_COMMAND = "Учитывать команду";
CLASSICAPI_MATCH_THE_COMMAND_TOOLTIP = "Предлагает только имена, подходящие для команды, например участников гильдии для /gpromote. Если выключено, предлагаются все известные вам имена.";
CLASSICAPI_MOTION = "Движение";
CLASSICAPI_NAME_SUGGESTIONS = "Подсказки имен";
CLASSICAPI_NAMEPLATES = "Таблички с именами";
CLASSICAPI_NUMBERS = "Числа";
CLASSICAPI_OPTIONS_ABOUT = "Настройки возможностей, которые ClassicAPI добавляет в игру. Каждый параметр меняет одну настройку, и изменение применяется сразу.";
CLASSICAPI_OTHER_NAMEPLATE_ALPHA = "Непрозрачность чужих табличек";
CLASSICAPI_OTHER_NAMEPLATE_ALPHA_TOOLTIP = "Непрозрачность табличек с именами целей, которые не являются вашей целью.";
CLASSICAPI_PREFER_YOUR_TARGET = "Отдавать предпочтение вашей цели";
CLASSICAPI_PREFER_YOUR_TARGET_TOOLTIP = "Если выбранную вами цель можно взять, динамическое выделение выбирает ее. Если параметр выключен, всегда выбирается ближайшая цель.";
CLASSICAPI_RANGE_TOOLTIP = "Максимальное расстояние в ярдах. Оно не может превышать значение параметра targetNearestDistance.";
CLASSICAPI_RECENT_WHISPERS_FIRST = "Сначала недавние шепоты";
CLASSICAPI_RECENT_WHISPERS_FIRST_TOOLTIP = "Предлагаемые имена начинаются с игроков, которым вы шептали совсем недавно.";
CLASSICAPI_REMEMBER_PLAYERS = "Запоминать игроков";
CLASSICAPI_REMEMBER_PLAYERS_TOOLTIP = "Сохраняет имя, класс, расу и пол встречавшихся вам игроков, чтобы модификации могли показывать их в следующих сеансах. Данные общие для всех ваших персонажей на этом сервере.";
CLASSICAPI_SCRIPT_PROFILE = "Профилирование скриптов";
CLASSICAPI_SCRIPT_PROFILE_TOOLTIP = "Измеряет время процессора, которое используют модификации. Изменение вступает в силу после перезагрузки интерфейса.";
CLASSICAPI_SEPARATE_LARGE_NUMBERS = "Разделять большие числа";
CLASSICAPI_SEPARATE_LARGE_NUMBERS_TOOLTIP = "Ставит разделитель между группами из трех цифр, когда модификации форматируют числа с помощью BreakUpLargeNumbers.";
CLASSICAPI_SORT_BAGS_RIGHT_TO_LEFT = "Сортировать сумки справа налево";
CLASSICAPI_SORT_BAGS_RIGHT_TO_LEFT_TOOLTIP = "При сортировке предметы перемещаются в конец последней сумки, а хлам — в начало рюкзака.";
CLASSICAPI_STRICT_INTERACT_RANGE = "Строгая дистанция взаимодействия";
CLASSICAPI_STRICT_INTERACT_RANGE_TOOLTIP = "Никогда не выбирает NPC или объект за пределами дистанции взаимодействия. Если параметр выключен, объект, доступный для использования сейчас, выбирается даже на большем расстоянии.";
CLASSICAPI_TARGET_NAMEPLATE_ALPHA = "Непрозрачность таблички цели";
CLASSICAPI_TARGET_NAMEPLATE_ALPHA_TOOLTIP = "Непрозрачность таблички с именем вашей цели.";
CLASSICAPI_UI_SHAKE_STRENGTH = "Сила дрожания интерфейса";
CLASSICAPI_UI_SHAKE_STRENGTH_TOOLTIP = "Определяет, насколько сильно эффекты дрожания смещают рамки. 0 отключает дрожание.";
CLASSICAPI_UNIT_DIRECTION_TOOLTIP = "Где должна находиться цель, чтобы ее выбрало динамическое выделение. Направление определяется камерой, а не персонажем.";
CLASSICAPI_WHEN_YOU_HAVE_A_TARGET = "Когда у вас есть цель";
COMBAT_LABEL = "Бой";
COUNTDOWN_FOR_COOLDOWNS_TEXT = "Отсчет времени восстановления";
CONFIRM_DELETE_EQUIPMENT_SET = "Вы действительно хотите удалить комплект экипировки \"%s\"?";
CONFIRM_SAVE_EQUIPMENT_SET = "Хотите сохранить комплект экипировки \"%s\"?";
COPPER_AMOUNT = "%d |4медная монета:медные монеты:медных монет;";
COPPER_AMOUNT_SYMBOL = "м";
COPPER_AMOUNT_TEXTURE = "%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:2:0:4:1:2:3:0:1|t";
DEBUFF_SYMBOL_CURSE = "Пр";
DEBUFF_SYMBOL_DISEASE = "Бз";
DEBUFF_SYMBOL_MAGIC = "Мг";
@@ -24,74 +116,66 @@ ENABLE_INTERACT_TEXT = "Включить клавишу взаимодейств
EQUIPMENT_MANAGER = "Комплекты экипировки";
EQUIPMENT_MANAGER_IGNORE_SLOT = "Пропустить ячейку";
EQUIPMENT_MANAGER_IS_DISABLED = "Управление экипировкой отключено.";
EQUIPMENT_MANAGER_ITEMS_MISSING_TOOLTIP = "%d %s |cffff0000(предмет для ячейки \"%d\" отсутствует)|r";
EQUIPMENT_MANAGER_ITEMS_MISSING_TOOLTIP = "%d %s |cffff0000(не хватает: %d)|r";
EQUIPMENT_MANAGER_PLACE_IN_BAGS = "Поместить в сумку";
EQUIPMENT_MANAGER_UNIGNORE_SLOT = "Учитывать ячейку";
EQUIPMENT_SETS = "Комплекты экипировки: |cFFFFFFFF%s|r";
EQUIPMENT_SETS_CANT_RENAME = "Это название уже используется для другого комплекта.";
EQUIPMENT_SETS_TOO_MANY = "Вы не можете создавать новые комплекты экипировки.";
EQUIPMENT_SET_ASSIGN_TO_SPEC = "Выбрать специализацию:";
EQUIPMENT_SET_ASSIGN_TO_SPEC = "Назначить специализации:";
EQUIPMENT_SET_EDIT = "Изменить название/значок";
EQUIPMENT_SET_SETTINGS = "Параметры";
EQUIPSET_EQUIP = "Надеть";
ERR_EQUIPMENT_MANAGER_BAGS_FULL = "Не удалось заменить комплект обмундирования. Инвентарь переполнен.";
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "Некоторые предметы из набора \"%s\" не будут экипированы, поскольку вы участвуете в бою.";
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "Недостает одного или более предметов из набора \"%s\".";
ERR_EQUIPMENT_MANAGER_BAGS_FULL = "Не удалось сменить экипировку. Инвентарь переполнен.";
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "Некоторые предметы из комплекта \"%s\" нельзя сменить, поскольку вы участвуете в бою.";
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "Один или несколько предметов из комплекта \"%s\" отсутствуют и не были надеты.";
EVENTTRACE_APPLY_DEFAULT_FILTER = "Применить фильтр по умолчанию";
EVENTTRACE_ARG_FMT = "Арг. %d:";
EVENTTRACE_BUTTON_DISABLE_FILTERS = "Снять выделение";
EVENTTRACE_ARG_FMT = "Аргумент %d:";
EVENTTRACE_BUTTON_DISABLE_FILTERS = "Снять все";
EVENTTRACE_BUTTON_DISCARD_FILTER = "Сбросить все";
EVENTTRACE_BUTTON_ENABLE_FILTERS = "Выделить все";
EVENTTRACE_BUTTON_ENABLE_FILTERS = "Выбрать все";
EVENTTRACE_BUTTON_MARKER = "Маркер";
EVENTTRACE_BUTTON_PAUSE = "Пауза";
EVENTTRACE_BUTTON_PLAY = "Играть";
EVENTTRACE_BUTTON_PLAY = "Продолжить";
EVENTTRACE_FILTER_HEADER = "Фильтр";
EVENTTRACE_HEADER = "Журнал события";
EVENTTRACE_LOG_CR_EVENTS = "Отображать глобальные события по регистрации";
EVENTTRACE_HEADER = "Журнал событий";
EVENTTRACE_LOG_CR_EVENTS = "Показывать события глобального реестра";
EVENTTRACE_LOG_DISCARD = "Журнал сброшен";
EVENTTRACE_LOG_HEADER = "Журнал";
EVENTTRACE_LOG_PAUSE = "Регистрация участников события приостановлена";
EVENTTRACE_LOG_PAUSE_WHILE_HIDDEN = "Фиксация событий прекращается в свернутом состоянии";
EVENTTRACE_LOG_START = "Регистрация участников события началась";
EVENTTRACE_LOG_WHEN_HIDDEN = "Фиксировать события в свернутом состоянии";
EVENTTRACE_LOG_PAUSE = "Запись событий приостановлена";
EVENTTRACE_LOG_PAUSE_WHILE_HIDDEN = "Запись событий приостановлена, пока окно скрыто";
EVENTTRACE_LOG_START = "Запись событий начата";
EVENTTRACE_LOG_WHEN_HIDDEN = "Записывать события, когда окно скрыто";
EVENTTRACE_MARKER = "Маркер";
EVENTTRACE_MESSAGE_FORMAT = "--- %s ---";
EVENTTRACE_OPTIONS = "Параметры";
EVENTTRACE_RESULTS = "Результаты: %d";
EVENTTRACE_SHOW_ARGUMENTS = "Отображать аргументы события";
EVENTTRACE_SHOW_TIMESTAMP = "Отображать метку времени события";
EVENTTRACE_RESULTS = "Результатов: %d";
EVENTTRACE_SHOW_ARGUMENTS = "Показывать аргументы событий";
EVENTTRACE_SHOW_TIMESTAMP = "Показывать время событий";
EVENTTRACE_TIMESTAMP = "Время:";
CLEAR_FOCUS = "Отменить запоминание цели";
INTERACT_ICONS_DEFAULT = "Только NPC (по умолчанию)";
INTERACT_ICONS_OPTION = "Значки клавиш взаимодействия";
INTERACT_ICONS_SHOW_ALL = "Показать все";
INTERACT_ICONS_OPTION = "Значки клавиши взаимодействия";
INTERACT_ICONS_SHOW_ALL = "Показывать все";
INTERACT_ICONS_SHOW_NONE = "Не показывать";
OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN = "Способности будут задействоваться, когда пользователь нажимает, а не отпускает клавишу.";
OPTION_TOOLTIP_ACTION_TARGETING = "Система захвата целей, динамически выбирающая цели в поле зрения. Работает параллельно со стандартной системой наведения.";
OPTION_TOOLTIP_COUNTDOWN_FOR_COOLDOWNS = "Отображение обратного отсчета для времени восстановления на панели команд.";
OPTION_TOOLTIP_ENABLE_INTERACT = "Позволяет взаимодействовать с NPC и объектами по нажатию клавиши.";
OPTION_TOOLTIP_INTERACT_ICONS = "Определяет, какие значки появляются над NPC и объектами, если включен параметр клавиши взаимодействия.";
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Прекращение автоматической атаки при смене цели.";
OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN = "Способности на панели команд срабатывают при нажатии клавиши, а не при ее отпускании.";
OPTION_TOOLTIP_ACTION_TARGETING = "Система выделения, которая динамически выбирает противников в зависимости от направления взгляда. Работает вместе со стандартной системой выделения.";
OPTION_TOOLTIP_COUNTDOWN_FOR_COOLDOWNS = "Показывать числовой отсчет времени восстановления на панели команд.";
OPTION_TOOLTIP_ENABLE_INTERACT = "Позволяет взаимодействовать с NPC и объектами нажатием клавиши.";
OPTION_TOOLTIP_INTERACT_ICONS = "Определяет, какие значки появляются над NPC и объектами, когда включена клавиша взаимодействия.";
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Прекращать автоатаку при смене цели.";
KEY_BINDING_NAME_AND_KEY = "%s (%s)";
KEY_BINDING_TOOLTIP = "<ПКМ, чтобы отвязать>";
SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL = "Назначьте клавишу \"%s\"или нажмите %s для отмены";
KEY_BINDING_TOOLTIP = "<ПКМ, чтобы снять привязку>";
SETTINGS_BIND_KEY_TO_COMMAND_OR_CANCEL = "Назначьте клавишу для \"%s\" или нажмите %s для отмены";
SET_FOCUS = "Запомнить цель";
FOCUS_TOKEN_NOT_FOUND = "<фокус отсутствует>";
FOCUSTARGET = "Запомнить цель";
ITEMS_VARIABLE_QUANTITY = "%d |4предмет:предмета:предметов;";
ITEMS_EQUIPPED = "%d |4предмет экипирован:предмета экипировано:предметов экипировано;";
ITEMS_IN_INVENTORY = "%d |4предмет:предмета:предметов; в сумках";
ITEM_SLOTS_IGNORED = "%d |4ячейка пропущена:ячейки пропущены:ячейки пропущены;";
ITEM_MISSING = "Предмет для ячейки %s отсутствует";
ITEMS_NOT_IN_INVENTORY = "%d |4предмет отсутствует:предмета отсутствует:предметов отсутствуют;";
STOP_AUTO_ATTACK = "Прекращение автоатаки";
TARGETFOCUS = "Восстановить цель";
-- BINDING_NAME_AOELOOT = "AoE Loot";
COPPER_AMOUNT = "%d |4медная монета:медные монеты:медных монет;";
COPPER_AMOUNT_SYMBOL = "м";
GEARSETS_POPUP_TEXT = "Введите название комплекта (не более 16 символов):";
GENERIC_FRACTION_STRING = "%d/%d";
GOLD_AMOUNT = "%d |4золотая:золотые:золотых;";
GOLD_AMOUNT_SYMBOL = "з";
GOLD_AMOUNT_TEXTURE = "%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:2:0:4:1:0:1:0:1|t";
GOLD_AMOUNT_TEXTURE_STRING = "%s|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:2:0:4:1:0:1:0:1|t";
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
ICON_FILTER_ALL = "Все значки";
ICON_FILTER_ITEM = "Предметы";
@@ -109,17 +193,17 @@ ITEM_MOD_ATTACK_POWER_SHORT = "к силе атаки";
ITEM_MOD_RANGED_ATTACK_POWER_SHORT = "к силе атаки дальнего боя";
ITEM_MOD_CRIT_MELEE_RATING_SHORT = "к показателю критического удара в ближнем бою";
ITEM_MOD_CRIT_RANGED_RATING_SHORT = "к показателю критического удара в дальнем бою";
ITEM_MOD_HIT_MELEE_RATING_SHORT = "к меткости в ближнем бою";
ITEM_MOD_HIT_MELEE_RATING_SHORT = "к меткости в ближнем бою";
ITEM_MOD_HIT_RANGED_RATING_SHORT = "к меткости в дальнем бою";
ITEM_MOD_HIT_SPELL_RATING_SHORT = "к меткости заклинаний";
ITEM_MOD_CRIT_SPELL_RATING_SHORT = "к критическому эффекту заклинаний";
ITEM_MOD_SPELL_DAMAGE_DONE_SHORT = "к дополнительному урону";
ITEM_MOD_SPELL_HEALING_DONE_SHORT = "к дополнительному исцелению";
ITEM_MOD_MANA_REGENERATION_SHORT = "к восполнению маны";
ITEM_MOD_DEFENSE_SKILL_RATING_SHORT = "Защита";
ITEM_MOD_DODGE_RATING_SHORT = "Уклонение";
ITEM_MOD_PARRY_RATING_SHORT = "Парирование";
ITEM_MOD_BLOCK_RATING_SHORT = "Блок";
ITEM_MOD_DEFENSE_SKILL_RATING_SHORT = "к защите";
ITEM_MOD_DODGE_RATING_SHORT = "к уклонению";
ITEM_MOD_PARRY_RATING_SHORT = "к парированию";
ITEM_MOD_BLOCK_RATING_SHORT = "к блокированию";
ITEM_MOD_BLOCK_VALUE_SHORT = "к показателю блокирования";
ITEM_MOD_HOLY_SPELL_DAMAGE_DONE_SHORT = "к урону от магии света";
ITEM_MOD_FIRE_SPELL_DAMAGE_DONE_SHORT = "к урону от магии огня";
@@ -131,6 +215,12 @@ ITEM_MOD_SPELL_PENETRATION_SHORT = "к проникающей способнос
ITEM_MOD_ARMOR_PENETRATION_RATING_SHORT = "к пробиванию брони";
ITEM_MOD_HEALTH_REGEN_SHORT = "к восполнению здоровья";
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "к урону в секунду";
ITEMS_EQUIPPED = "%d |4предмет экипирован:предмета экипировано:предметов экипировано;";
ITEMS_IN_INVENTORY = "%d |4предмет:предмета:предметов; в инвентаре";
ITEMS_NOT_IN_INVENTORY = "%d |4предмет отсутствует:предмета отсутствует:предметов отсутствует;";
ITEMS_VARIABLE_QUANTITY = "%d |4предмет:предмета:предметов;";
ITEM_MISSING = "%s отсутствует";
ITEM_SLOTS_IGNORED = "%d |4ячейка пропущена:ячейки пропущены:ячеек пропущено;";
LARGE_NUMBER_SEPERATOR = "";
DECIMAL_SEPERATOR = ",";
FIRST_NUMBER = "тыс.";
@@ -141,14 +231,18 @@ SECOND_NUMBER_CAP = "М";
SECOND_NUMBER_CAP_NO_SPACE = "М";
THIRD_NUMBER = "млрд";
FOURTH_NUMBER = "трлн";
LUA_ERROR = "Ошибка Lua";
MINUTES_SECONDS = "%.2d:%.2d";
PAPERDOLL_EQUIPMENTMANAGER = "Комплекты экипировки";
PAPERDOLL_NEWEQUIPMENTSET = "Новый комплект";
PERCENTAGE_STRING = "%d%%";
SET_FRIENDNOTE_LABEL = "Заметка о |3-5(%s):";
PLAYER_LIST_DELIMITER = ", ";
RELOADUI = "Перезагрузить интерфейс";
SET_FRIENDNOTE_LABEL = "Задать заметку для %s:";
SET_NOTE = "Добавить заметку";
SILVER_AMOUNT = "%d |4серебряная:серебряные:серебряных;";
SILVER_AMOUNT_SYMBOL = "с";
SILVER_AMOUNT_TEXTURE = "%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:2:0:4:1:1:2:0:1|t";
SLASH_CANCELAURA1 = "/снятьауру";
SLASH_CANCELAURA2 = "/cancelaura";
SLASH_CANCELFORM1 = "/нормоблик";
@@ -222,10 +316,14 @@ SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
SLASH_USE1 = "/use";
SLASH_USE2 = "/использовать";
SOCIAL_LABEL = "Общение";
LINK_TRADESKILL_TOOLTIP = "Щелкните здесь, чтобы создать ссылку на вашу профессию.";
TEXT_TO_SPEECH = "Текст в речь";
TEXT_TO_SPEECH_ADJUST_RATE = "Изменить скорость речи.";
STOP_AUTO_ATTACK = "Прекращение автоатаки";
SYMBOLIC_NEGATIVE_NUMBER = "-%s";
SYMBOLIC_POSITIVE_NUMBER = "+%s";
TARGETFOCUS = "Восстановить цель";
LINK_TRADESKILL_TOOLTIP = "Щелкните здесь, чтобы создать ссылку на свою профессию.";
TEXT_TO_SPEECH = "Синтез речи";
TEXT_TO_SPEECH_ADJUST_RATE = "Изменить скорость речи";
TEXT_TO_SPEECH_ADJUST_VOLUME = "Громкость речи";
TEXT_TO_SPEECH_VOICE_OPTION = "Параметры голоса";
TOTAL_MEM_KB_ABBR = "Память: %.0f КБ";
TOTAL_MEM_MB_ABBR = "Модификации занимают в памяти: %.2f МБ";
TEXT_TO_SPEECH_VOICE_OPTION = "Выбор голоса";
TOTAL_MEM_KB_ABBR = "Память модификаций: %.0f КБ";
TOTAL_MEM_MB_ABBR = "Память модификаций: %.2f МБ";
+33
View File
@@ -0,0 +1,33 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "localization/Locale.h"
#include "Game.h"
#include "Offsets.h"
#include <cstdint>
#include <cstring>
namespace Localization::Locale {
// Script_GetLocale (FUN_0048d8b0): VAR_LOCALE_NAME_TABLE[VAR_LOCALE_INDEX].
const char *Client() {
const uint32_t index = Game::Read<uint32_t>(Offsets::VAR_LOCALE_INDEX);
return reinterpret_cast<const char *const *>(
static_cast<uintptr_t>(Offsets::VAR_LOCALE_NAME_TABLE))[index];
}
bool Is(const char *code) { return std::strcmp(Client(), code) == 0; }
} // namespace Localization::Locale
+32
View File
@@ -0,0 +1,32 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
// The client locale by NAME, as GetLocale() reports it.
//
// Language rules (plurals, accent folding, number abbreviations) must pick
// by this name, never by VAR_LOCALE_INDEX. The 1.12 index is the Wow.ini
// tag's position in a table of eight codes (enUS koKR frFR deDE zhCN zhTW
// esES xxYY), not 4.3.4's nine-slot order, so a 4.3.4 index such as 8 for
// ruRU never occurs here. A Russian client is one whose table holds "ruRU",
// in whatever slot. The index stays right for picking a DBC string column.
namespace Localization::Locale {
// The locale code GetLocale() returns ("enUS", "frFR", ...).
const char *Client();
// Whether the client locale is `code`.
bool Is(const char *code);
} // namespace Localization::Locale
+3 -6
View File
@@ -62,6 +62,7 @@
#include "Game.h"
#include "Offsets.h"
#include "cvar/Factory.h"
#include "localization/Locale.h"
#include <cmath>
#include <cstdio>
@@ -131,17 +132,13 @@ constexpr Scale kScalesAsian[] = {
CVar::Factory::Handle g_breakUpLargeNumbers = nullptr;
// VAR_LOCALE_INDEX order: enUS, koKR, frFR, deDE, zhCN, zhTW, esES, esMX, ruRU.
bool ClientIsAsian() {
const int locale = Game::Read<int>(Offsets::VAR_LOCALE_INDEX);
return locale == 1 || locale == 4 || locale == 5;
}
bool IsAsianLocale(const char *locale) {
return std::strcmp(locale, "koKR") == 0 || std::strcmp(locale, "zhCN") == 0 ||
std::strcmp(locale, "zhTW") == 0;
}
bool ClientIsAsian() { return IsAsianLocale(Localization::Locale::Client()); }
// `_G[name]` when it is a string, else "".
std::string GlobalString(void *L, const char *name) {
Lua::PushString(L, name);
+7 -10
View File
@@ -31,6 +31,7 @@
#include "Game.h"
#include "Offsets.h"
#include "localization/Locale.h"
#include <cstdint>
#include <cstring>
@@ -45,16 +46,13 @@ using Rule_t = char *(__fastcall *)(const char *text);
constexpr size_t kBufferSize = 0x2000;
char g_buffer[kBufferSize];
// FrameScript_GetPluralIndex. The 1.12 locale index has 4.3.4's order for
// 0..7 (enUS, koKR, frFR, deDE, zhCN, zhTW, esES, esMX); 8 is ruRU there.
// FrameScript_GetPluralIndex, keyed by locale name: 4.3.4 switches on its own
// locale index, whose slots 1.12's index does not share (Localization::Locale).
uint32_t PluralIndex(int32_t count) {
switch (Game::Read<int>(Offsets::VAR_LOCALE_INDEX)) {
case 1:
case 2:
case 4:
case 5:
using Localization::Locale::Is;
if (Is("koKR") || Is("frFR") || Is("zhCN") || Is("zhTW"))
return count > 1;
case 8:
if (Is("ruRU")) {
if (count % 100 >= 11 && count % 100 <= 14)
return 2;
if (count % 10 == 1)
@@ -62,9 +60,8 @@ uint32_t PluralIndex(int32_t count) {
if (count % 10 >= 2 && count % 10 <= 4)
return 1;
return 2;
default:
return count != 1;
}
return count != 1;
}
bool IsDigit(char ch) { return ch >= '0' && ch <= '9'; }