| Total Complexity | 61 |
| Total Lines | 278 |
| Duplicated Lines | 0 % |
| Changes | 1 | ||
| Bugs | 0 | Features | 0 |
Complex classes like Translation often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Translation, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 10 | class Translation |
||
| 11 | { |
||
| 12 | public const CONTEXT_ADMIN_KEY = 'admin-text'; |
||
| 13 | public const SEARCH_THRESHOLD = 3; |
||
| 14 | |||
| 15 | protected array $entries = []; |
||
| 16 | |||
| 17 | protected array $results = []; |
||
| 18 | |||
| 19 | /** |
||
| 20 | * Returns all saved custom strings with translation context. |
||
| 21 | */ |
||
| 22 | public function all(): array |
||
| 23 | { |
||
| 24 | $strings = $this->strings(); |
||
| 25 | $entries = $this->filter($strings, $this->entries())->results(); |
||
| 26 | array_walk($strings, function (&$entry) use ($entries) { |
||
| 27 | $entry['desc'] = array_key_exists($entry['id'], $entries) |
||
| 28 | ? $this->getEntryString($entries[$entry['id']], 'msgctxt') |
||
| 29 | : ''; |
||
| 30 | }); |
||
| 31 | return $strings; |
||
| 32 | } |
||
| 33 | |||
| 34 | public function entries(): array |
||
| 35 | { |
||
| 36 | if (empty($this->entries)) { |
||
| 37 | $potFile = glsr()->path(glsr()->languages.'/'.glsr()->id.'.pot'); |
||
| 38 | $entries = $this->extractEntriesFromPotFile($potFile, glsr()->id); |
||
| 39 | $entries = glsr()->filterArray('translation/entries', $entries); |
||
| 40 | $this->entries = $entries; |
||
| 41 | } |
||
| 42 | return $this->entries; |
||
| 43 | } |
||
| 44 | |||
| 45 | /** |
||
| 46 | * @return static |
||
| 47 | */ |
||
| 48 | public function exclude(?array $entriesToExclude = null, ?array $entries = null) |
||
| 51 | } |
||
| 52 | |||
| 53 | public function extractEntriesFromPotFile(string $potFile, string $domain, array $entries = []): array |
||
| 54 | { |
||
| 55 | try { |
||
| 56 | $potEntries = $this->normalize(Parser::parseFile($potFile)->getEntries()); |
||
| 57 | foreach ($potEntries as $key => $entry) { |
||
| 58 | if (str_contains(Arr::get($entry, 'msgctxt'), static::CONTEXT_ADMIN_KEY)) { |
||
| 59 | continue; |
||
| 60 | } |
||
| 61 | $entry['domain'] = $domain; // the text-domain of the entry |
||
| 62 | $entries[html_entity_decode($key, ENT_COMPAT, 'UTF-8')] = $entry; |
||
| 63 | } |
||
| 64 | } catch (\Exception $e) { |
||
| 65 | glsr_log()->error($e->getMessage()); |
||
| 66 | } |
||
| 67 | return $entries; |
||
| 68 | } |
||
| 69 | |||
| 70 | /** |
||
| 71 | * @return static |
||
| 72 | */ |
||
| 73 | public function filter(?array $filterWith = null, ?array $entries = null, bool $intersect = true) |
||
| 74 | { |
||
| 75 | if (!is_array($entries)) { |
||
| 76 | $entries = $this->results; |
||
| 77 | } |
||
| 78 | if (!is_array($filterWith)) { |
||
| 79 | $filterWith = $this->strings(); |
||
| 80 | } |
||
| 81 | $keys = array_flip(wp_list_pluck($filterWith, 'id')); |
||
| 82 | $this->results = $intersect |
||
| 83 | ? array_intersect_key($entries, $keys) |
||
| 84 | : array_diff_key($entries, $keys); |
||
| 85 | return $this; |
||
| 86 | } |
||
| 87 | |||
| 88 | public function isInvalid(array $entry): bool |
||
| 89 | { |
||
| 90 | $s1 = $entry['s1'] ?? ''; |
||
| 91 | $s2 = $entry['s2'] ?? ''; |
||
| 92 | $p1 = $entry['p1'] ?? ''; |
||
| 93 | $p2 = $entry['p2'] ?? ''; |
||
| 94 | if ($s1 === $s2 && $p1 === $p2) { |
||
| 95 | return false; |
||
| 96 | } |
||
| 97 | if ($this->placeholders($s1) !== $this->placeholders($s2)) { |
||
| 98 | return true; |
||
| 99 | } |
||
| 100 | if ($this->placeholders($p1) !== $this->placeholders($p2)) { |
||
| 101 | return true; |
||
| 102 | } |
||
| 103 | return false; |
||
| 104 | } |
||
| 105 | |||
| 106 | public function isMissing(array $entry): bool |
||
| 107 | { |
||
| 108 | if (empty($entry['s1'])) { |
||
| 109 | return false; |
||
| 110 | } |
||
| 111 | $s1 = $entry['s1']; |
||
| 112 | return false === Arr::searchByKey($s1, $this->entries(), 'msgid') |
||
| 113 | && false === Arr::searchByKey(htmlentities2($s1), $this->entries(), 'msgid'); |
||
| 114 | } |
||
| 115 | |||
| 116 | public function placeholders(string $format): int |
||
| 145 | } |
||
| 146 | |||
| 147 | public function render(string $template, array $entry): string |
||
| 148 | { |
||
| 149 | $data = array_combine(array_map(fn ($key) => "data.{$key}", array_keys($entry)), $entry); |
||
| 150 | $data['data.class'] = ''; |
||
| 151 | $data['data.error'] = ''; |
||
| 152 | if ($this->isMissing($entry)) { |
||
| 153 | $data['data.class'] = 'is-invalid'; |
||
| 154 | $data['data.error'] = _x('This custom text is invalid because the original text has been changed or removed.', 'admin-text', 'site-reviews'); |
||
| 155 | } elseif ($this->isInvalid($entry)) { |
||
| 156 | $data['data.class'] = 'is-invalid'; |
||
| 157 | $data['data.error'] = _x('The placeholder tags are missing in your custom text.', 'admin-text', 'site-reviews'); |
||
| 158 | } |
||
| 159 | return glsr(Template::class)->build("partials/strings/{$template}", [ |
||
| 160 | 'context' => array_map('esc_html', $data), |
||
| 161 | ]); |
||
| 162 | } |
||
| 163 | |||
| 164 | /** |
||
| 165 | * Returns a rendered string of all saved custom strings with translation context. |
||
| 166 | */ |
||
| 167 | public function renderAll(): string |
||
| 168 | { |
||
| 169 | $rendered = ''; |
||
| 170 | foreach ($this->all() as $index => $entry) { |
||
| 171 | $entry['index'] = $index; |
||
| 172 | $entry['prefix'] = OptionManager::databaseKey(); |
||
| 173 | $rendered .= $this->render($entry['type'], $entry); |
||
| 174 | } |
||
| 175 | return $rendered; |
||
| 176 | } |
||
| 177 | |||
| 178 | public function renderResults(bool $resetAfterRender = true): string |
||
| 179 | { |
||
| 180 | $rendered = ''; |
||
| 181 | foreach ($this->results as $id => $entry) { |
||
| 182 | $data = [ |
||
| 183 | 'desc' => $this->getEntryString($entry, 'msgctxt'), |
||
| 184 | 'id' => $id, |
||
| 185 | 'p1' => $this->getEntryString($entry, 'msgid_plural'), |
||
| 186 | 's1' => $this->getEntryString($entry, 'msgid'), |
||
| 187 | ]; |
||
| 188 | $text = !empty($data['p1']) |
||
| 189 | ? sprintf('%s | %s', $data['s1'], $data['p1']) |
||
| 190 | : $data['s1']; |
||
| 191 | $rendered .= $this->render('result', [ |
||
| 192 | 'domain' => $this->getEntryString($entry, 'domain'), |
||
| 193 | 'entry' => wp_json_encode($data, JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_TAG | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), |
||
| 194 | 'text' => wp_strip_all_tags($text), |
||
| 195 | ]); |
||
| 196 | } |
||
| 197 | if ($resetAfterRender) { |
||
| 198 | $this->reset(); |
||
| 199 | } |
||
| 200 | return $rendered; |
||
| 201 | } |
||
| 202 | |||
| 203 | public function reset(): void |
||
| 204 | { |
||
| 205 | $this->results = []; |
||
| 206 | } |
||
| 207 | |||
| 208 | public function results(): array |
||
| 209 | { |
||
| 210 | $results = $this->results; |
||
| 211 | $this->reset(); |
||
| 212 | return $results; |
||
| 213 | } |
||
| 214 | |||
| 215 | /** |
||
| 216 | * @return static |
||
| 217 | */ |
||
| 218 | public function search(string $needle = '') |
||
| 234 | } |
||
| 235 | |||
| 236 | /** |
||
| 237 | * Store the strings to avoid unnecessary loops. |
||
| 238 | */ |
||
| 239 | public function strings(): array |
||
| 240 | { |
||
| 241 | static $strings; |
||
| 242 | if (empty($strings)) { |
||
| 243 | // we need to bypass the filter hooks because this is run before the settings are initiated |
||
| 244 | $settings = get_option(OptionManager::databaseKey()); |
||
| 245 | $strings = Arr::getAs('array', $settings, 'settings.strings'); |
||
| 246 | $strings = $this->normalizeStrings($strings); |
||
| 247 | } |
||
| 248 | return $strings; |
||
| 249 | } |
||
| 250 | |||
| 251 | protected function getEntryString(array $entry, string $key): string |
||
| 252 | { |
||
| 253 | return isset($entry[$key]) |
||
| 254 | ? implode('', (array) $entry[$key]) |
||
| 255 | : ''; |
||
| 256 | } |
||
| 257 | |||
| 258 | protected function normalize(array $entries): array |
||
| 259 | { |
||
| 260 | $keys = [ |
||
| 261 | 'msgctxt', 'msgid', 'msgid_plural', 'msgstr', 'msgstr[0]', 'msgstr[1]', |
||
| 262 | ]; |
||
| 263 | array_walk($entries, function (&$entry) use ($keys) { |
||
| 264 | foreach ($keys as $key) { |
||
| 265 | $entry = $this->normalizeEntryString($entry, $key); |
||
| 266 | } |
||
| 267 | }); |
||
| 268 | return $entries; |
||
| 269 | } |
||
| 270 | |||
| 271 | protected function normalizeEntryString(array $entry, string $key): array |
||
| 277 | } |
||
| 278 | |||
| 279 | protected function normalizeStrings(array $strings): array |
||
| 280 | { |
||
| 281 | $defaultString = array_fill_keys(['id', 's1', 's2', 'p1', 'p2'], ''); |
||
| 282 | $strings = array_filter($strings, 'is_array'); |
||
| 283 | foreach ($strings as &$string) { |
||
| 284 | $string['type'] = isset($string['p1']) ? 'plural' : 'single'; |
||
| 288 | } |
||
| 289 | } |
||
| 290 |