json().'.php')) die('File for locale '.$loc->name.' not found.'); else require 'localization/locale_'.$loc->json().'.php'; foreach ($lang as $k => $v) self::$$k = $v; // *cough* .. reuse-hacks (because copy-pastaing text for 5 locales sucks) self::$item['cat'][2][1] = self::$spell['weaponSubClass']; self::$item['cat'][2][1][14] .= ' ('.self::$item['cat'][2][0].')'; self::$main['moreTitles']['privilege'] = self::$privileges['_privileges']; self::$locale = $loc; } public static function getLocale() : Locale { return self::$locale; } public static function __callStatic(string $prop, ?array $args = []) : string|array|null { $vspfArgs = []; foreach ($args as $i => $arg) { if (!is_array($arg)) continue; $vspfArgs = $arg; unset($args[$i]); } if (($x = self::exist($prop, ...$args)) !== null) return self::vspf($x, $vspfArgs); $dbt = debug_backtrace()[0]; $file = explode(DIRECTORY_SEPARATOR, $dbt['file']); trigger_error('Lang - undefined property Lang::$'.$prop.'[\''.implode('\'][\'', $args).'\'], called in '.array_pop($file).':'.$dbt['line'], E_USER_WARNING); return null; } public static function exist(string $prop, string ...$args) : string|array|null { if (!isset(self::$$prop)) return null; $ref = self::$$prop; foreach ($args as $a) { if (!isset($ref[$a])) return null; $ref = $ref[$a]; } return $ref; } public static function concat(array $args, int $concat = self::CONCAT_AND, ?callable $callback = null) : string { $buff = ''; $callback ??= fn($x) => $x; reset($args); if (count($args) < 2) return $callback(current($args), key($args)); do { $item = $callback(current($args), key($args)); $arg = next($args); if ($arg !== false || $concat == self::CONCAT_NONE) $buff .= ', '.$item; else if ($concat == self::CONCAT_AND) $buff .= self::main('and').$item; else $buff .= self::main('or').$item; } while ($arg !== false); return substr($buff, 2); } // truncate string after X chars. If X is inside a word truncate behind it. public static function trimTextClean(string $text, int $len = 100) : string { // remove line breaks $text = strtr($text, ["\n" => ' ', "\r" => ' ']); // limit whitespaces to one at a time $text = preg_replace('/\s+/', ' ', trim($text)); if ($len <= 0 || mb_strlen($text) <= $len) return $text; $n = 0; $b = []; $parts = explode(' ', $text); while ($n < $len && $parts) { $_ = array_shift($parts); $n += mb_strlen($_); $b[] = $_; } return implode(' ', $b).'…'; } // add line breaks to string after X chars. If X is inside a word break behind it. public static function breakTextClean(string $text, int $len = 30, int $fmt = self::FMT_HTML) : string { // remove line breaks $text = strtr($text, ["\n" => ' ', "\r" => ' ']); // limit whitespaces to one at a time $text = preg_replace('/\s+/', ' ', trim($text)); if ($len <= 0 || mb_strlen($text) <= $len) return $text; $row = []; $i = 0; $n = 0; foreach (explode(' ', $text) as $p) { $row[$i][] = $p; $n += (mb_strlen($p) + 1); if ($n < $len) continue; $n = 0; $i++; } foreach ($row as &$r) $r = implode(' ', $r); $separator = match ($fmt) { self::FMT_HTML => '
', self::FMT_MARKUP => '[br]', self::FMT_RAW => "\n", default => "\n" }; return implode($separator, $row); } public static function sort(string $prop, string $group, int $method = SORT_NATURAL) : void { if (!isset(self::$$prop)) { trigger_error('Lang::sort - tried to use undefined property Lang::$'.$prop, E_USER_WARNING); return; } $var = &self::$$prop; if (!isset($var[$group])) { trigger_error('Lang::sort - tried to use undefined property Lang::$'.$prop.'[\''.$group.'\']', E_USER_WARNING); return; } asort($var[$group], $method); } // todo: expand public static function getInfoBoxForFlags(int $cuFlags) : array { $tmp = []; if ($cuFlags & CUSTOM_DISABLED) $tmp[] = '[tooltip name=disabledHint]'.Util::jsEscape(self::main('disabledHint')).'[/tooltip][span class=tip tooltip=disabledHint]'.Util::jsEscape(self::main('disabled')).'[/span]'; if ($cuFlags & CUSTOM_SERVERSIDE) $tmp[] = '[tooltip name=serversideHint]'.Util::jsEscape(self::main('serversideHint')).'[/tooltip][span class=tip tooltip=serversideHint]'.Util::jsEscape(self::main('serverside')).'[/span]'; if ($cuFlags & CUSTOM_UNAVAILABLE) $tmp[] = self::main('unavailable'); if ($cuFlags & CUSTOM_EXCLUDE_FOR_LISTVIEW && User::isInGroup(U_GROUP_STAFF)) $tmp[] = '[tooltip name=excludedHint]This entry is excluded from lists and is not searchable.[/tooltip][span tooltip=excludedHint class="tip q10"]Hidden[/span]'; return $tmp; } public static function getLocks(int $lockId, ?array &$ids = [], bool $interactive = false, int $fmt = self::FMT_HTML) : array { $locks = []; $ids = []; $lock = DB::Aowow()->selectRow('SELECT * FROM ?_lock WHERE `id` = ?d', $lockId); if (!$lock) return $locks; for ($i = 1; $i <= 5; $i++) { $prop = $lock['properties'.$i]; $rank = $lock['reqSkill'.$i]; $name = ''; switch ($lock['type'.$i]) { case LOCK_TYPE_ITEM: $name = ItemList::getName($prop); if (!$name) continue 2; if ($fmt == self::FMT_HTML) $name = $interactive ? ''.$name.'' : ''.$name.''; else if ($interactive && $fmt == self::FMT_MARKUP) { $name = '[item='.$prop.']'; $ids[Type::ITEM][] = $prop; } break; case LOCK_TYPE_SKILL: $name = self::spell('lockType', $prop); if (!$name) continue 2; // skills if (in_array($prop, [1, 2, 3, 20])) { $skills = array( 1 => SKILL_LOCKPICKING, 2 => SKILL_HERBALISM, 3 => SKILL_MINING, 20 => SKILL_INSCRIPTION ); if ($fmt == self::FMT_HTML) $name = $interactive ? ''.$name.'' : ''.$name.''; else if ($interactive && $fmt == self::FMT_MARKUP) { $name = '[skill='.$skills[$prop].']'; $ids[Type::SKILL][] = $skills[$prop]; } else $name = SkillList::getName($prop); if ($rank > 0) $name .= ' ('.$rank.')'; } // Lockpicking else if ($prop == 4) { if ($fmt == self::FMT_HTML) $name = $interactive ? ''.$name.'' : ''.$name.''; else if ($interactive && $fmt == self::FMT_MARKUP) { $name = '[spell=1842]'; $ids[Type::SPELL][] = 1842; } } // exclude unusual stuff else if (User::isInGroup(U_GROUP_STAFF)) { if ($rank > 0) $name .= ' ('.$rank.')'; } else continue 2; break; case LOCK_TYPE_SPELL: $name = SpellList::getName($prop); if (!$name) continue 2; if ($fmt == self::FMT_HTML) $name = $interactive ? ''.$name.'' : ''.$name.''; else if ($interactive && $fmt == self::FMT_MARKUP) { $name = '[spell='.$prop.']'; $ids[Type::SPELL][] = $prop; } break; default: continue 2; } $locks[$lock['type'.$i] == LOCK_TYPE_ITEM ? $prop : -$prop] = $name; } return $locks; } public static function getReputationLevelForPoints(int $pts) : string { return self::game('rep', Game::getReputationLevelForPoints($pts)); } public static function getRequiredItems(int $class, int $mask, bool $short = true) : string { if (!in_array($class, [ITEM_CLASS_MISC, ITEM_CLASS_ARMOR, ITEM_CLASS_WEAPON])) return ''; // not checking weapon / armor here. It's highly unlikely that they overlap if ($short) { // misc - Mounts if ($class == ITEM_CLASS_MISC) return ''; // all basic armor classes if ($class == ITEM_CLASS_ARMOR && ($mask & 0x1E) == 0x1E) return ''; // all weapon classes if ($class == ITEM_CLASS_WEAPON && ($mask & 0x1DE5FF) == 0x1DE5FF) return ''; foreach (self::spell('subClassMasks') as $m => $str) if ($mask == $m) return $str; } if ($class == ITEM_CLASS_MISC) // yeah hardcoded.. sue me! return self::spell('cat', -5, 0); $tmp = []; $strs = self::spell($class == ITEM_CLASS_ARMOR ? 'armorSubClass' : 'weaponSubClass'); foreach ($strs as $k => $str) if ($mask & (1 << $k) && $str) $tmp[] = $str; if (!$tmp && $class == ITEM_CLASS_ARMOR) return self::spell('cat', -11, 8); else if (!$tmp && $class == ITEM_CLASS_WEAPON) return self::spell('cat', -11, 6); else return implode(', ', $tmp); } public static function getStances(int $stanceMask) : string { $stanceMask &= 0xFF37F6FF; // clamp to available stances/forms.. $tmp = []; $i = 1; while ($stanceMask) { if ($stanceMask & (1 << ($i - 1))) { $tmp[] = self::game('st', $i); $stanceMask &= ~(1 << ($i - 1)); } $i++; } return implode(', ', $tmp); } public static function getMagicSchools(int $schoolMask, bool $short = false) : string { $schoolMask &= SPELL_ALL_SCHOOLS; // clamp to available schools.. $tmp = []; $i = 0; if ($short && $schoolMask == SPELL_ALL_SCHOOLS) return self::main('all'); if ($short && $schoolMask == SPELL_MAGIC_SCHOOLS) return self::main('all').' ('.self::game('dt', 1).')'; while ($schoolMask) { if ($schoolMask & (1 << $i)) { $tmp[] = self::game('sc', $i); $schoolMask &= ~(1 << $i); } $i++; } return implode(', ', $tmp); } public static function getClassString(int $classMask, ?array &$ids = [], int $fmt = self::FMT_HTML) : string { $classMask &= ChrClass::MASK_ALL; // clamp to available classes.. if (!$classMask || $classMask == ChrClass::MASK_ALL)// available to all classes return ''; [$base, $br] = match ($fmt) { self::FMT_HTML => ['%2$s', ''], self::FMT_MARKUP => ['[class=%1$d]', '[br]'], self::FMT_RAW => ['%2$s', ''], default => ['%2$s', ''] }; $tmp = []; foreach (ChrClass::fromMask($classMask) as $c) $tmp[$c] = (!fMod(count($tmp) + 1, 3) ? $br : '').sprintf($base, $c, self::game('cl', $c)); $ids = array_keys($tmp); return implode(', ', $tmp); } public static function getRaceString(int $raceMask, ?array &$ids = [], int $fmt = self::FMT_HTML) : string { $raceMask &= ChrRace::MASK_ALL; // clamp to available races.. if (!$raceMask || $raceMask == ChrRace::MASK_ALL) // available to all races (we don't display 'both factions') return ''; if ($raceMask == ChrRace::MASK_HORDE) return self::game('ra', -2); if ($raceMask == ChrRace::MASK_ALLIANCE) return self::game('ra', -1); [$base, $br] = match ($fmt) { self::FMT_HTML => ['%2$s', ''], self::FMT_MARKUP => ['[race=%1$d]', '[br]'], self::FMT_RAW => ['%2$s', ''], default => ['%2$s', ''] }; $tmp = []; foreach (ChrRace::fromMask($raceMask) as $r) $tmp[$r] = (!fMod(count($tmp) + 1, 3) ? $br : '').sprintf($base, $r, self::game('ra', $r)); $ids = array_keys($tmp); return implode(', ', $tmp); } public static function formatSkillBreakpoints(array $bp, int $fmt = self::FMT_MARKUP) : string { $tmp = self::game('difficulty'); $base = match ($fmt) { self::FMT_HTML => '%2$s ', self::FMT_MARKUP => '[color=r%1$d]%2$s[/color] ', self::FMT_RAW => '%2$s ', default => '%2$s ' }; for ($i = 0; $i < 4; $i++) if (!empty($bp[$i])) $tmp .= sprintf($base, $i + 1, $bp[$i]); return trim($tmp); } public static function nf(float $number, int $decimals = 0, bool $no1k = false) : string { return number_format($number, $decimals, self::main('nfSeparators', 1), $no1k ? '' : self::main('nfSeparators', 0)); } public static function typeName(int $type) : string { return Util::ucFirst(self::game(Type::getFileString($type))); } public static function formatTime(int $msec, string $prop = 'game', string $src = 'timeAbbrev', bool $concat = false) : string { if ($msec < 0) $msec = 0; $time = DateTime::parse($msec); // [$ms, $s, $m, $h, $d] $mult = [0, 1000, 60, 60, 24]; $total = 0; $ref = []; $result = []; if (is_array(self::$$prop[$src])) $ref = &self::$$prop[$src]; else { trigger_error('Lang::formatTime - tried to access undefined property Lang::$'.$prop, E_USER_WARNING); return ''; } if (!$msec) return self::vspf($ref[0], [0]); for ($i = 4; $i > 0; $i--) { $total += $time[$i]; if (isset($ref[$i]) && ($total || ($i == 1 && !$result))) { if (!$concat) return self::vspf($ref[$i], [$total + ($time[$i-1] ?? 0) / $mult[$i]]); $result[] = self::vspf($ref[$i], [$total]); $total = 0; } else $total *= $mult[$i]; } return implode(', ', $result); } private static function vspf(null|array|string $var, array $args = []) : null|array|string { if (is_array($var)) { foreach ($var as &$v) $v = self::vspf($v, $args); return $var; } if (!$var) // may be null or empty. Handled differently depending on context return $var; $var = Cfg::applyToString($var); if ($args) $var = vsprintf($var, $args); return self::unescapeUISequences($var); } /* Quoted from WoWWiki - UI Escape Sequences (https://wowwiki-archive.fandom.com/wiki/UI_escape_sequences) * number |1singular;plural; Will choose a word depending on whether the digit preceding it is 0/1 or not (i.e. 1,11,21 return the first string, as will 0,10,40). Note that unlike |4 singular and plural forms are separated by semi-colon. * |2text Before vowels outputs d' (with apostrophe) and removes any leading spaces from text, otherwise outputs de (with trailing space) * |3-formid(text) Displays text declined to the specified form (index ranges from 1 to GetNumDeclensionSets()). * number |4singular:plural; -or- number |4singular:plural1:plural2; Will choose a form based on the number preceding it. More than two forms (separated by colons) may be required by locale 8 (ruRU). **/ public static function unescapeUISequences(?string $var, int $fmt = -1) : string { if (!$var) return ''; if (strpos($var, '|') === false) return $var; // line break |n $var = preg_replace_callback('/\|n/i', function ($m) use ($fmt) { switch ($fmt) { case -1: // default Lang::vspf case case self::FMT_HTML: return '
'; case self::FMT_MARKUP: return '[br]'; case self::FMT_RAW: default: return ''; } }, $var); // color |c|r $var = preg_replace_callback('/\|c([[:xdigit:]]{2})([[:xdigit:]]{6})(.+?)\|r/is', function ($m) use ($fmt) { [$_, $a, $rgb, $text] = $m; switch ($fmt) { case -1: // default Lang::vspf case case self::FMT_HTML: return sprintf('%3$s', $rgb, $a, $text); case self::FMT_MARKUP: return sprintf('[span color=#%1$s]%3$s[/span]', $rgb, $a, $text); // doesn't support alpha case self::FMT_RAW: default: return $text; } }, $var); // icon |T:0:0:0:-1|t $var = preg_replace_callback('/\|T([\w]+\\\)*([^\.:]+)(?:\.[bB][lL][pP])?:([^\|]+)\|t/', function ($m) use ($fmt) { /* iconParam - size1, size2, xoffset, yoffset size1 == 0; size2 omitted: Width = Height = TextHeight (always square!) size1 > 0; size2 omitted: Width = Height = size1 (always square!) size1 == 0; size2 == 0 : Width = Height = TextHeight (always square!) size1 > 0; size2 == 0 : Width = TextHeight; Height = size1 (size1 is height!!!) size1 == 0; size2 > 0 : Width = size2 * TextHeight; Height = TextHeight (size2 is an aspect ratio and defines width!!!) size1 > 0; size2 > 0 : Width = size1; Height = size2 */ [$_, $iconPath, $iconName, $iconParam] = $m; switch ($fmt) { case self::FMT_HTML: return ''; case self::FMT_MARKUP: return '[icon name='.Util::lower($iconName).']'; case self::FMT_RAW: default: return ''; } }, $var); // hyperlink |H|h|h $var = preg_replace_callback('/\|H([^:]+):([^\|]+)\|h([^\|]+)\|h/i', function ($m) use ($fmt) { /* type Params |Hchannel channelName, channelname == CHANNEL ? channelNr : null |Hachievement AchievementID, PlayerGUID, isComplete, Month, Day, Year, criteriaMask1, criteriaMask2, criteriaMask3, criteriaMask4 - 32bit masks of Achievement_criteria.dbc/UIOrder only for achievements that display a todo list |Hquest QuestID, QuestLevel |Hitem itemId enchantId gemId1 gemId2 gemId3 gemId4 suffixId uniqueId linkLevel |Henchant SpellID (from craftwindow) |Htalent TalentID, TalentRank |Hspell SpellID, PlayerLevel? |Htrade SpellID, curSkill, maxSkill, PlayerGUID, base64_encode(known recipes bitmask) |Hplayer Name |Hunit GUID ? - combatlog |Hicon ? "source"|"dest" - combatlog |Haction ? - combatlog */ [$_, $linkType, $linkVars, $text] = $m; $linkVars = explode(':', $linkVars); $spfVars = ['', $linkVars[0], $text]; switch ($linkType) { case 'trade': case 'enchant': $linkType = 'spell'; case 'achievement': // markdown COULD implement completed status case 'quest': case 'item': // markdown COULD implement enchantments/gems case 'spell': $spfVars[0] = $linkType; break; case 'talent': if ($spell = DB::Aowow()->selectCell('SELECT `spell` FROM ?_talents WHERE `id` = ?d AND `rank` = ?d', $linkVars[0], $linkVars[1])) { $spfVars[0] = 'spell'; $spfVars[1] = $spell; break; } default: return ''; } switch ($fmt) { case self::FMT_HTML: return sprintf('%s', $spfVars); case self::FMT_MARKUP: return sprintf('[%s=%d]', $spfVars); case self::FMT_RAW: default: return sprintf('(%s #%d) %s', $spfVars); } }, $var); // |1 - digit singular/plural |1; $var = preg_replace_callback('/(\d+)\s*\|1([^;]+);([^;]+);/is', function ($m) { [$_, $num, $singular, $plural] = $m; switch ($num[-1]) { case 0: case 1: return $num . ' ' . $singular; default: return $num . ' ' . $plural; } }, $var); // |2 - frFR preposition: de |2 $var = preg_replace_callback('/\|2\s?(.)/i', function ($m) { [$_, $char] = $m; switch (strtolower($char)) { case 'h': if (self::$locale != Locale::FR) return 'de ' . $char; case 'a': case 'e': case 'i': case 'o': case 'u': return "d'" . $char; default: return 'de ' . $char; } }, $var); // |3 - ruRU declinations |3-() $var = preg_replace_callback('/\|3-(\d+)\(([^\)]+)\)/iu', function ($m) { [$_, $caseIdx, $word] = $m; if ($caseIdx > 11 || $caseIdx < 1) // max caseIdx seen in DeclinedWordCases.dbc return $word; if (preg_match('/\P{Cyrillic}/iu', $word)) // not in cyrillic script return $word; if ($declWord = DB::Aowow()->selectCell('SELECT dwc.`word` FROM ?_declinedwordcases dwc JOIN ?_declinedword dc ON dwc.`wordId` = dc.`id` WHERE dwc.`caseIdx` = ?d AND dc.`word` = ?', $caseIdx, $word)) return $declWord; return $word; }, $var); // |4 - numeric switch |4:[:]; $var = preg_replace_callback('/([\d\.\,]+)([^\d]*)\|4([^:]*):([^:;]+)(?::([^;]+))?;/is', function ($m) { [$_, $num, $pad, $singular, $plural1, $plural2] = array_pad($m, 6, null); if (self::$locale != Locale::RU || !$plural2) return $num . $pad . ($num == 1 ? $singular : $plural1); // singular - ends in 1, but not teen number if ($num[-1] == 1 && $num != 11) return $num . $pad . $singular; // genitive singular - ends in 2, 3, 4, but not teen number if (($num[-1] == 2 && $num != 12) || ($num[-1] == 3 && $num != 13) || ($num[-1] == 4 && $num != 14)) return $num . $pad . $plural1; // genitive plural - everything else return $num . $pad . $plural2; }, $var); return $var; } } ?>