Skip to content

Commit 514bb49

Browse files
authored
Added translation helper shell script (#2332)
1 parent b4c70bb commit 514bb49

File tree

1 file changed

+312
-0
lines changed

1 file changed

+312
-0
lines changed

shell/translations.php

Lines changed: 312 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
<?php
2+
/**
3+
* OpenMage
4+
*
5+
* NOTICE OF LICENSE
6+
*
7+
* This source file is subject to the Open Software License (OSL 3.0)
8+
* that is bundled with this package in the file LICENSE.txt.
9+
* It is also available through the world-wide-web at this URL:
10+
* http://opensource.org/licenses/osl-3.0.php
11+
* If you did not receive a copy of the license and are unable to
12+
* obtain it through the world-wide-web, please send an email
13+
* to license@magento.com so we can send you a copy immediately.
14+
*
15+
* @category Mage
16+
* @package Mage_Shell
17+
* @copyright Copyright (c) 2022 The OpenMage Contributors (https://www.openmage.org)
18+
* @license https://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
19+
*/
20+
21+
declare(strict_types=1);
22+
23+
// DO NOT RUN DIRECTLY IN YOUR PRODUCTION ENVIRONMENT!
24+
// This script is distributed in the hope that it will be useful, but without any warranty.
25+
26+
require_once 'abstract.php';
27+
chdir(dirname(__DIR__, 1));
28+
29+
/**
30+
* OpenMage Translation Helper Shell Script
31+
*
32+
* @category Mage
33+
* @package Mage_Shell
34+
* @author The OpenMage Contributors
35+
*/
36+
class Mage_Shell_Translation extends Mage_Shell_Abstract
37+
{
38+
/**
39+
* Remember if we used stdin for file list
40+
*
41+
* @var boolean
42+
*/
43+
protected $_stdin;
44+
45+
/**
46+
* Get a list of files to scan for translated strings
47+
*
48+
* @return array<int, string>
49+
*/
50+
protected function getFiles(): array
51+
{
52+
$files = [];
53+
$fh = fopen('php://stdin', 'r');
54+
55+
if ($fh === false) {
56+
return $files;
57+
}
58+
59+
stream_set_blocking($fh, false);
60+
61+
while (($line = fgets($fh)) !== false) {
62+
$files[] = $line;
63+
}
64+
if (count($files)) {
65+
$this->_stdin = true;
66+
} else {
67+
$files = array_merge(
68+
// Grep for all files that might call the __ function
69+
explode("\n", (string)shell_exec("grep -Frl --exclude-dir='.git' --include=*.php --include=*.phtml '__' .")),
70+
// Grep for all XML files that might use the translate attribute
71+
explode("\n", (string)shell_exec("grep -Frl --exclude-dir='.git' --include=*.xml 'translate=' ."))
72+
);
73+
}
74+
return array_filter(array_map('trim', $files));
75+
}
76+
77+
/**
78+
* Get all defined translation strings per file from app/locale/$CODE/*.csv
79+
*
80+
* @return array<string, array<int, string>>
81+
*/
82+
protected function getDefinedStrings(): array
83+
{
84+
$map = [];
85+
$lang = $this->getArg('lang');
86+
87+
if (!is_string($lang)) {
88+
$lang = 'en_US';
89+
}
90+
91+
$files = glob("app/locale/$lang/*.csv");
92+
if (!is_array($files)) {
93+
return $map;
94+
}
95+
96+
$parser = new Varien_File_Csv();
97+
$parser->setDelimiter(',');
98+
foreach ($files as $file) {
99+
$data = $parser->getDataPairs($file);
100+
$map[$file] = array_keys($data);
101+
}
102+
103+
return $map;
104+
}
105+
106+
/**
107+
* Get all used translation strings per file from all php, phtml, and xml files
108+
*
109+
* @return array<string, array<int, string>>
110+
*/
111+
protected function getUsedStrings(): array
112+
{
113+
$map = [];
114+
$files = $this->getFiles();
115+
foreach ($files as $file) {
116+
// Ignore this file
117+
if ($file === './shell/translations.php') {
118+
continue;
119+
}
120+
121+
$ext = pathinfo($file, PATHINFO_EXTENSION);
122+
$contents = file_get_contents($file);
123+
124+
if ($contents === false) {
125+
echo "ERROR: File not found $file\n";
126+
continue;
127+
}
128+
129+
$matches = [];
130+
131+
if ($ext === 'php' || $ext === 'phtml') {
132+
// Regex to get first argument of __ function
133+
// https://stackoverflow.com/a/5696141
134+
$re_dq = '/__\s*\(\s*"([^"\\\\]*(?:\\\\.[^"\\\\]*)*\s*)"/s';
135+
$re_sq = "/__\s*\(\s*'([^'\\\\]*(?:\\\\.[^'\\\\]*)*\s*)'/s";
136+
137+
if (preg_match_all($re_dq, $contents, $_matches)) {
138+
$matches = array_merge($matches, str_replace('\"', '"', $_matches[1]));
139+
}
140+
if (preg_match_all($re_sq, $contents, $_matches)) {
141+
$matches = array_merge($matches, str_replace("\'", "'", $_matches[1]));
142+
}
143+
} elseif ($ext === 'xml') {
144+
$xml = new SimpleXMLElement($contents);
145+
// Get all nodes with translate="" attribute
146+
$nodes = $xml->xpath('//*[@translate]');
147+
foreach ($nodes as $node) {
148+
// Which children should we translate?
149+
$translateNode = $node['translate'];
150+
if (!$translateNode instanceof SimpleXMLElement) {
151+
continue;
152+
}
153+
$translateChildren = array_map('trim', explode(' ', $translateNode->__toString()));
154+
foreach ($node->children() as $child) {
155+
if (in_array($child->getName(), $translateChildren)) {
156+
$matches[] = $child->__toString();
157+
}
158+
}
159+
}
160+
}
161+
162+
$matches = array_filter(array_unique($matches));
163+
if (count($matches)) {
164+
$map[$file] = $matches;
165+
}
166+
}
167+
return $map;
168+
}
169+
170+
/**
171+
* Find deprecated usage of global __ function
172+
*
173+
* @return void
174+
*/
175+
protected function findDeprecated(): void
176+
{
177+
$files = $this->getFiles();
178+
foreach ($files as $file) {
179+
// Ignore this file
180+
if ($file === './shell/translations.php') {
181+
continue;
182+
}
183+
184+
$ext = pathinfo($file, PATHINFO_EXTENSION);
185+
$contents = file_get_contents($file);
186+
187+
if ($contents === false) {
188+
echo "ERROR: File not found $file\n";
189+
continue;
190+
}
191+
192+
if ($ext === 'php' || $ext === 'phtml') {
193+
// Capture what precedes a __() call
194+
$re = '/(\S*\s*)(__\s*\()/';
195+
196+
$found = false; // If we found deprecated usage in this file
197+
$insert = "Mage::helper('core')->"; // String to insert before global __ usage
198+
$offset = 0; // Keep track of extra offset from adding strings
199+
200+
if (preg_match_all($re, $contents, $matches, PREG_OFFSET_CAPTURE)) {
201+
for ($i = 0; $i < count($matches[0]); $i++) {
202+
$word = trim($matches[1][$i][0]);
203+
if (substr($word, -2) !== '->' && substr($word, -2) !== '::' && $word !== 'function') {
204+
$found = true;
205+
if ($this->getArg('fix')) {
206+
$contents = substr_replace($contents, $insert, $matches[2][$i][1] + $offset, 0);
207+
$offset += strlen($insert);
208+
}
209+
}
210+
}
211+
}
212+
if ($found) {
213+
if ($this->getArg('fix')) {
214+
echo "DEPRECATED: Global __ function fixed in: $file\n";
215+
file_put_contents($file, $contents);
216+
} else {
217+
echo "DEPRECATED: Global __ function found in: $file\n";
218+
}
219+
}
220+
}
221+
}
222+
}
223+
224+
/**
225+
* Run script
226+
*
227+
* @return void
228+
*/
229+
public function run()
230+
{
231+
if ($this->getArg('missing')) {
232+
$definedFileMap = $this->getDefinedStrings();
233+
$usedFileMap = $this->getUsedStrings();
234+
235+
$definedFlat = array_unique(array_merge(...array_values($definedFileMap)));
236+
$usedFlat = array_unique(array_merge(...array_values($usedFileMap)));
237+
238+
if ($this->getArg('verbose')) {
239+
foreach ($usedFileMap as $file => $used) {
240+
$missing = array_diff($used, $definedFlat);
241+
if (count($missing)) {
242+
echo "$file\n " . implode("\n ", $missing) . "\n\n";
243+
}
244+
}
245+
} else {
246+
$missing = array_diff($usedFlat, $definedFlat);
247+
sort($missing);
248+
echo implode("\n", $missing) . "\n";
249+
}
250+
} elseif ($this->getArg('unused')) {
251+
$definedFileMap = $this->getDefinedStrings();
252+
$usedFileMap = $this->getUsedStrings();
253+
254+
$definedFlat = array_unique(array_merge(...array_values($definedFileMap)));
255+
$usedFlat = array_unique(array_merge(...array_values($usedFileMap)));
256+
257+
if ($this->_stdin) {
258+
echo "stdin file list cannot be used with the 'unused' mode.\n";
259+
exit;
260+
}
261+
if ($this->getArg('verbose')) {
262+
foreach ($definedFileMap as $file => $defined) {
263+
$unused = array_diff($defined, $usedFlat);
264+
if (count($unused)) {
265+
echo "$file\n " . implode("\n ", $unused) . "\n\n";
266+
}
267+
}
268+
} else {
269+
$unused = array_diff($definedFlat, $usedFlat);
270+
sort($unused);
271+
echo implode("\n", $unused) . "\n";
272+
}
273+
} elseif ($this->getArg('deprecated')) {
274+
$this->findDeprecated();
275+
} else {
276+
echo $this->usageHelp();
277+
}
278+
}
279+
280+
/**
281+
* Retrieve Usage Help Message
282+
*
283+
* @return string
284+
*/
285+
public function usageHelp()
286+
{
287+
return <<<USAGE
288+
Usage: php -f translations.php -- [options]
289+
php -f translations.php -- missing --verbose
290+
291+
missing Display used translations strings that are missing from csv files
292+
unused Display defined translations strings that are not used in templates
293+
--verbose Include filename with output
294+
--lang <lang> Specify which language pack to check in app/locale, default is en_US
295+
deprecated Find deprecated usage of the global __() function
296+
--fix Overwrite files to fix deprecated usage DO NOT RUN IN PRODUCTION!
297+
help This help
298+
299+
Note: By default, this script will check all files in this repository. However,
300+
you can pipe a list of files to check for missing translations strings.
301+
This is useful for checking a specific commit. For example:
302+
303+
# Check if last two commits may have introduced missing translations
304+
git diff --name-only HEAD~2 | php -f translations.php -- missing
305+
306+
307+
USAGE;
308+
}
309+
}
310+
311+
$shell = new Mage_Shell_Translation();
312+
$shell->run();

0 commit comments

Comments
 (0)