@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at recaptime-dev/main 221 lines 8.4 kB view raw
1<?php 2 3final class PhorgeInternationalizationValidator extends Phobject { 4 private function validateTranslation($locale, $proto, $transl, $types, $n) { 5 $errors = array(); 6 if ($n >= count($types)) { 7 if (is_array($transl)) { 8 $errors[] = pht( 9 'The locale `%s` defines a translation for the key `%s`, '. 10 'which has at least %s level(s) of arrays, '. 11 'however the source message has only %s parameter(s).', 12 $locale, 13 $proto, 14 new PhutilNumber($n + 1), 15 phutil_count($types)); 16 } 17 $data = array(); 18 foreach ($types as $type) { 19 if ($type === 'phutilnumber') { 20 // Make a class that can be converted into a string 21 // (to mimic the conversion pht() will do) 22 // but not to a number (the double-conversion loses data) 23 // See T16454 24 $data[] = new PhorgeStringablePlaceholder(); 25 } else if ($type === 'number') { 26 // no good way to check numbers being converted to strings 27 // without parsing the format specifier ourself 28 // (remember xsprintf can't work with translated strings since 29 // they can use backreferences, format specifiers, etc) 30 $data[] = 3; 31 } else if ($type === null) { 32 // This could either be a string or a number, let PHP type 33 // conversions handle it 34 $data[] = 'abc'; 35 } else { 36 throw new Exception(pht('Bogus type "%s" for "%s"', $type, $proto)); 37 } 38 } 39 try { 40 $parsed = vsprintf($transl, $data); 41 } catch (ValueError $ex) { 42 // In PHP 8 vsprintf throws a ValueError for bad data; 43 // in PHP7 it returns false 44 $parsed = false; 45 } catch (RuntimeException $ex) { 46 // The types of the args don't match (the RuntimeException comes 47 // from PhutilErrorHandler.php throwing what was originally a PHP 48 // warning) 49 $msg = $ex->getMessage(); 50 if ($msg === 'Object of class PhorgeStringablePlaceholder '. 51 'could not be converted to int') { 52 $errors[] = pht( 53 'The locale `%s` defines a translation for the key `%s` which '. 54 'uses %%d to represent a PhutilNumber. This loses data if the '. 55 'number ends up being formatted with thousands specifiers. '. 56 'See T16454', 57 $locale, 58 $proto); 59 return $errors; 60 } 61 // This shouldn't happen, but if something else goes wrong fall 62 // through to the generic `failed to interpolate properly` error 63 $parsed = false; 64 } 65 if ($parsed === false) { 66 $errors[] = pht( 67 'The locale `%s` defines a translation for the key `%s` which '. 68 'failed to interpolate properly. Probably it defines too many '. 69 'parameters.', 70 $locale, 71 $proto); 72 } 73 return $errors; 74 } 75 if (!is_array($transl)) { 76 $transl = array($transl); 77 } 78 $type = $types[$n]; 79 if ($type === null && count($transl) != 1) { 80 $errors[] = pht( 81 'The locale `%s` defines a translation for the key `%s`, which varies '. 82 'on the plurality or gender of parameter %d, however that parameter '. 83 'is not a number or person.', 84 $locale, 85 $proto, 86 $n + 1); 87 } 88 foreach ($transl as $subarray) { 89 $errors = array_merge( 90 $errors, 91 $this->validateTranslation($locale, $proto, $subarray, $types, $n + 1)); 92 } 93 return $errors; 94 } 95 public function validateLibraries($loaded_json) { 96 $errors = array(); 97 $all_translations = PhutilTranslation::getAllTranslations(); 98 $locales = PhutilLocale::loadAllLocales(); 99 $keyed_translations = array(); 100 $override_key = 'translation.override'; 101 try { 102 $trans_override = PhabricatorEnv::getEnvConfig($override_key); 103 $all_translations[$override_key] = $trans_override; 104 } catch (Throwable $ex) { 105 // If Phorge config is hosed then just don't check translation.override 106 } 107 foreach ($all_translations as $locale_code => $translations) { 108 if (!isset($locales[$locale_code]) && $locale_code != $override_key) { 109 $errors[] = pht( 110 'Translations are defined for the locale `%s`, '. 111 'which is not recognized.', 112 $locale_code); 113 } 114 foreach ($translations as $proto => $transl) { 115 if (!isset($keyed_translations[$proto])) { 116 $keyed_translations[$proto] = array(); 117 } 118 $keyed_translations[$proto][$locale_code] = $transl; 119 // Check for unused translations 120 if (!isset($loaded_json[$proto])) { 121 $errors[] = pht( 122 'The locale `%s` defines a translation for the string "%s", '. 123 'however that string does not appear to be referenced '. 124 'by the codebase.', 125 $locale_code, 126 $proto); 127 } 128 } 129 } 130 foreach ($loaded_json as $string => $spec) { 131 // Check for wrong branches by parameter type 132 $translations = idx($keyed_translations, $string, array()); 133 foreach ($translations as $locale => $translation) { 134 $errors = array_merge($errors, $this->validateTranslation( 135 $locale, 136 $string, 137 $translation, 138 $spec['types'], 139 0)); 140 } 141 // Run it on the proto-English as a translation too 142 // since this also does some parameter type checking 143 // (some of which may belong better in a linter than here) 144 $errors = array_merge($errors, $this->validateTranslation( 145 id(new PhutilRawEnglishLocale())->getLocaleCode(), 146 $string, 147 $string, 148 $spec['types'], 149 0)); 150 // Check for missing branches in US english 151 if (str_contains($string, '(s)')) { 152 if (!isset($keyed_translations[$string]['en_US'])) { 153 foreach ($spec['types'] as $type) { 154 if ($type === 'number') { 155 $errors[] = pht( 156 'The string "%s" contains the placeholder "(s)" and '. 157 'a numeric parameter on which to vary the (s) by, '. 158 'however the builtin US English translation does not do so.', 159 $string); 160 } 161 } 162 } 163 } 164 } 165 return $errors; 166 } 167 public function loadExtractions($run_extractor, $quiet = false) { 168 $libraries = PhutilBootloader::getInstance()->getAllLibraries(); 169 $phorge_root = phutil_get_library_root('phorge'); 170 $i18n_bin = Filesystem::resolvePath('../bin/i18n', $phorge_root); 171 $all_json = array(); 172 foreach ($libraries as $lib) { 173 $root = phutil_get_library_root($lib); 174 $json = Filesystem::resolvePath('.cache/i18n_strings.json', $root); 175 if ($run_extractor) { 176 // The command needs to be stated twice to avoid the linter complaining 177 // about the arg not being a scalar string 178 if ($quiet) { 179 execx( 180 '%R extract %s', 181 $i18n_bin, 182 $root); 183 } else { 184 $err = phutil_passthru( 185 '%R extract %s', 186 $i18n_bin, 187 $root); 188 if ($err) { 189 throw new Exception(pht( 190 'Failed to run i18n extractor: %s', 191 $err)); 192 } 193 } 194 } else if (!Filesystem::pathExists($json)) { 195 throw new Exception(pht( 196 'Strings have not yet been extracted for library %s. '. 197 'Run `%s` for that library first to extract them or '. 198 're-run with `%s` to automatically extract missing strngs.', 199 $lib, 200 'bin/i18n extract', 201 '--extract')); 202 } 203 $all_json += phutil_json_decode(Filesystem::readFile($json)); 204 } 205 // Add extra date elements from PhutilTranslator::translateDate 206 // which aren't in a static pht() call 207 $date = array('types' => array()); 208 $all_json['Jan'] = $date; 209 $all_json['Feb'] = $date; 210 $all_json['Mar'] = $date; 211 $all_json['Apr'] = $date; 212 $all_json['Jun'] = $date; 213 $all_json['Jul'] = $date; 214 $all_json['Aug'] = $date; 215 $all_json['Sep'] = $date; 216 $all_json['Oct'] = $date; 217 $all_json['Nov'] = $date; 218 $all_json['Dec'] = $date; 219 return $all_json; 220 } 221}