1313
1414namespace CodeIgniter \Commands \Translation ;
1515
16- use CodeIgniter \CLI \BaseCommand ;
16+ use CodeIgniter \CLI \AbstractCommand ;
17+ use CodeIgniter \CLI \Attributes \Command ;
1718use CodeIgniter \CLI \CLI ;
19+ use CodeIgniter \CLI \Input \Option ;
1820use CodeIgniter \Helpers \Array \ArrayHelper ;
1921use Config \App ;
2022use Locale ;
2325use SplFileInfo ;
2426
2527/**
26- * @see \CodeIgniter\Commands\Translation\LocalizationFinderTest
28+ * Finds and saves available phrases to translate.
2729 */
28- class LocalizationFinder extends BaseCommand
30+ #[Command(
31+ name: 'lang:find ' ,
32+ description: 'Find and save available phrases to translate. ' ,
33+ group: 'Translation ' ,
34+ )]
35+ class LocalizationFinder extends AbstractCommand
2936{
30- protected $ group = 'Translation ' ;
31- protected $ name = 'lang:find ' ;
32- protected $ description = 'Find and save available phrases to translate. ' ;
33- protected $ usage = 'lang:find [options] ' ;
34- protected $ arguments = [];
35- protected $ options = [
36- '--locale ' => 'Specify locale (en, ru, etc.) to save files. ' ,
37- '--dir ' => 'Directory to search for translations relative to APPPATH. ' ,
38- '--show-new ' => 'Show only new translations in table. Does not write to files. ' ,
39- '--verbose ' => 'Output detailed information. ' ,
40- ];
37+ private string $ languagePath ;
4138
42- /**
43- * Flag for output detailed information
44- */
45- private bool $ verbose = false ;
39+ protected function configure (): void
40+ {
41+ $ this
42+ ->addOption (new Option (
43+ name: 'locale ' ,
44+ description: 'Specify locale (en, ru, etc.) to save files. ' ,
45+ requiresValue: true ,
46+ default: '' ,
47+ ))
48+ ->addOption (new Option (
49+ name: 'dir ' ,
50+ description: 'Directory to search for translations relative to APPPATH. ' ,
51+ requiresValue: true ,
52+ default: '' ,
53+ ))
54+ ->addOption (new Option (
55+ name: 'show-new ' ,
56+ description: 'Show only new translations in table. Does not write to files. ' ,
57+ ))
58+ ->addOption (new Option (
59+ name: 'verbose ' ,
60+ description: 'Output detailed information. ' ,
61+ ));
62+ }
4663
47- /**
48- * Flag for showing only translations, without saving
49- */
50- private bool $ showNew = false ;
64+ protected function execute ( array $ arguments , array $ options ): int
65+ {
66+ $ locale = $ options [ ' locale ' ];
67+ assert ( is_string ( $ locale )) ;
5168
52- private string $ languagePath ;
69+ $ dir = $ options ['dir ' ];
70+ assert (is_string ($ dir ));
5371
54- public function run (array $ params )
55- {
56- $ this ->verbose = array_key_exists ('verbose ' , $ params );
57- $ this ->showNew = array_key_exists ('show-new ' , $ params );
58- $ optionLocale = $ params ['locale ' ] ?? null ;
59- $ optionDir = $ params ['dir ' ] ?? null ;
60- $ currentLocale = Locale::getDefault ();
61- $ currentDir = APPPATH ;
62- $ this ->languagePath = $ currentDir . 'Language ' ;
72+ $ currentLocale = Locale::getDefault ();
6373
64- if ( service ( ' environment ' )-> isTesting ()) {
65- $ currentDir = SUPPORTPATH . ' Services ' . DIRECTORY_SEPARATOR ;
66- $ this -> languagePath = SUPPORTPATH . ' Language ' ;
67- }
74+ [ ' currentDir ' => $ currentDir , ' languagePath ' => $ this -> languagePath ] = $ this -> resolvePaths ();
75+
76+ if ( $ locale !== '' ) {
77+ $ supportedLocales = config (App::class)-> supportedLocales ;
6878
69- if (is_string ($ optionLocale )) {
70- if (! in_array ($ optionLocale , config (App::class)->supportedLocales , true )) {
79+ if (! in_array ($ locale , $ supportedLocales , true )) {
7180 CLI ::error (
72- 'Error: " ' . $ optionLocale . '" is not supported. Supported locales: '
73- . implode (', ' , config (App::class)->supportedLocales ),
81+ sprintf (
82+ 'Error: "%s" is not supported. Supported locales: %s ' ,
83+ $ locale ,
84+ implode (', ' , $ supportedLocales ),
85+ ),
86+ 'light_gray ' ,
87+ 'red ' ,
7488 );
7589
7690 return EXIT_USER_INPUT ;
7791 }
7892
79- $ currentLocale = $ optionLocale ;
93+ $ currentLocale = $ locale ;
8094 }
8195
82- if (is_string ( $ optionDir ) ) {
83- $ tempCurrentDir = realpath ($ currentDir . $ optionDir );
96+ if ($ dir !== '' ) {
97+ $ tempCurrentDir = realpath ($ currentDir . $ dir );
8498
8599 if ($ tempCurrentDir === false ) {
86- CLI ::error ('Error: Directory must be located in " ' . $ currentDir . ' " ' );
100+ CLI ::error (sprintf ( 'Error: Directory must be located in "%s" ' , $ currentDir), ' light_gray ' , ' red ' );
87101
88102 return EXIT_USER_INPUT ;
89103 }
90104
91- if ($ this ->isSubDirectory ($ tempCurrentDir , $ this ->languagePath )) {
92- CLI ::error ('Error: Directory " ' . $ this ->languagePath . ' " restricted to scan. ' );
105+ if ($ this ->isSubdirectory ($ tempCurrentDir , $ this ->languagePath )) {
106+ CLI ::error (sprintf ( 'Error: Directory "%s" restricted to scan. ' , $ this ->languagePath ), ' light_gray ' , ' red ' );
93107
94108 return EXIT_USER_INPUT ;
95109 }
@@ -99,18 +113,42 @@ public function run(array $params)
99113
100114 $ this ->process ($ currentDir , $ currentLocale );
101115
102- CLI ::write ('All operations done! ' );
116+ CLI ::write ('All operations done! ' , ' green ' );
103117
104118 return EXIT_SUCCESS ;
105119 }
106120
121+ /**
122+ * Resolves the directory to scan and the directory that holds the language
123+ * files, swapping in the test fixtures under the testing environment.
124+ *
125+ * @return array{currentDir: string, languagePath: string}
126+ */
127+ private function resolvePaths (): array
128+ {
129+ if (service ('environment ' )->isTesting ()) {
130+ return [
131+ 'currentDir ' => SUPPORTPATH . 'Services ' . DIRECTORY_SEPARATOR ,
132+ 'languagePath ' => SUPPORTPATH . 'Language ' ,
133+ ];
134+ }
135+
136+ return [
137+ 'currentDir ' => APPPATH ,
138+ 'languagePath ' => APPPATH . 'Language ' ,
139+ ];
140+ }
141+
107142 private function process (string $ currentDir , string $ currentLocale ): void
108143 {
144+ $ showNew = $ this ->getValidatedOption ('show-new ' ) === true ;
145+ $ verbose = $ this ->getValidatedOption ('verbose ' ) === true ;
146+
109147 $ tableRows = [];
110148 $ countNewKeys = 0 ;
111149
112150 $ iterator = new RecursiveIteratorIterator (new RecursiveDirectoryIterator ($ currentDir ));
113- $ files = iterator_to_array ($ iterator, true );
151+ $ files = iterator_to_array ($ iterator );
114152 ksort ($ files );
115153
116154 [
@@ -121,10 +159,7 @@ private function process(string $currentDir, string $currentLocale): void
121159
122160 ksort ($ foundLanguageKeys );
123161
124- $ languageDiff = [];
125- $ languageFoundGroups = array_unique (array_keys ($ foundLanguageKeys ));
126-
127- foreach ($ languageFoundGroups as $ langFileName ) {
162+ foreach ($ foundLanguageKeys as $ langFileName => $ foundKeys ) {
128163 $ languageStoredKeys = [];
129164 $ languageFilePath = $ this ->languagePath . DIRECTORY_SEPARATOR . $ currentLocale . DIRECTORY_SEPARATOR . $ langFileName . '.php ' ;
130165
@@ -137,38 +172,38 @@ private function process(string $currentDir, string $currentLocale): void
137172 // are not new and must not be re-reported or written.
138173 $ resolvedKeys = $ this ->findResolvedTranslations ($ langFileName , $ currentLocale );
139174
140- $ languageDiff = ArrayHelper::recursiveDiff ($ foundLanguageKeys [ $ langFileName ] , $ resolvedKeys );
175+ $ languageDiff = ArrayHelper::recursiveDiff ($ foundKeys , $ resolvedKeys );
141176 $ countNewKeys += ArrayHelper::recursiveCount ($ languageDiff );
142177
143- if ($ this -> showNew ) {
178+ if ($ showNew ) {
144179 $ tableRows = array_merge ($ this ->arrayToTableRows ($ langFileName , $ languageDiff ), $ tableRows );
145180 } else {
146181 $ newLanguageKeys = array_replace_recursive ($ languageDiff , $ languageStoredKeys );
147182
148183 if ($ languageDiff !== []) {
149184 if (file_put_contents ($ languageFilePath , $ this ->templateFile ($ newLanguageKeys )) === false ) {
150- $ this ->writeIsVerbose ('Lang file ' . $ langFileName . ' (error write). ' , 'red ' );
185+ $ this ->writeIsVerbose (sprintf ( 'Lang file %s (error write). ' , $ langFileName ) , 'red ' );
151186 } else {
152- $ this ->writeIsVerbose ('Lang file " ' . $ langFileName . ' " successful updated! ' , 'green ' );
187+ $ this ->writeIsVerbose (sprintf ( 'Lang file "%s " successful updated! ' , $ langFileName ) , 'green ' );
153188 }
154189 }
155190 }
156191 }
157192
158- if ($ this -> showNew && $ tableRows !== []) {
193+ if ($ showNew && $ tableRows !== []) {
159194 sort ($ tableRows );
160195 CLI ::table ($ tableRows , ['File ' , 'Key ' ]);
161196 }
162197
163- if (! $ this -> showNew && $ countNewKeys > 0 ) {
198+ if (! $ showNew && $ countNewKeys > 0 ) {
164199 CLI ::write ('Note: You need to run your linting tool to fix coding standards issues. ' , 'white ' , 'red ' );
165200 }
166201
167- $ this ->writeIsVerbose ('Files found: ' . $ countFiles );
168- $ this ->writeIsVerbose ('New translates found: ' . $ countNewKeys );
169- $ this ->writeIsVerbose ('Bad translates found: ' . count ($ badLanguageKeys ));
202+ $ this ->writeIsVerbose (sprintf ( 'Files found: %d ' , $ countFiles) );
203+ $ this ->writeIsVerbose (sprintf ( 'New translates found: %d ' , $ countNewKeys) );
204+ $ this ->writeIsVerbose (sprintf ( 'Bad translates found: %d ' , count ($ badLanguageKeys) ));
170205
171- if ($ this -> verbose && $ badLanguageKeys !== []) {
206+ if ($ verbose && $ badLanguageKeys !== []) {
172207 $ tableBadRows = [];
173208
174209 foreach ($ badLanguageKeys as $ value ) {
@@ -211,19 +246,13 @@ private function findResolvedTranslations(string $langFileName, string $currentL
211246 }
212247
213248 /**
214- * @param SplFileInfo|string $file
215- *
216- * @return array<string, array>
249+ * @return array{foundLanguageKeys: array<string, mixed>, badLanguageKeys: list<array{string, string}>}
217250 */
218- private function findTranslationsInFile ($ file ): array
251+ private function findTranslationsInFile (SplFileInfo $ file ): array
219252 {
220253 $ foundLanguageKeys = [];
221254 $ badLanguageKeys = [];
222255
223- if (is_string ($ file ) && is_file ($ file )) {
224- $ file = new SplFileInfo ($ file );
225- }
226-
227256 $ fileContent = file_get_contents ($ file ->getRealPath ());
228257 preg_match_all ('/lang\( \'([._a-z0-9\-]+) \'\)/ui ' , $ fileContent , $ matches );
229258
@@ -266,13 +295,16 @@ private function findTranslationsInFile($file): array
266295
267296 private function isIgnoredFile (SplFileInfo $ file ): bool
268297 {
269- if ($ file ->isDir () || $ this ->isSubDirectory ($ file ->getRealPath (), $ this ->languagePath )) {
298+ if ($ file ->isDir () || $ this ->isSubdirectory ($ file ->getRealPath (), $ this ->languagePath )) {
270299 return true ;
271300 }
272301
273302 return $ file ->getExtension () !== 'php ' ;
274303 }
275304
305+ /**
306+ * @param array<array-key, mixed> $language
307+ */
276308 private function templateFile (array $ language = []): string
277309 {
278310 if ($ language !== []) {
@@ -337,6 +369,10 @@ private function replaceArraySyntax(string $code): string
337369
338370 /**
339371 * Create multidimensional array from another keys
372+ *
373+ * @param list<string> $fromKeys
374+ *
375+ * @return array<string, mixed>
340376 */
341377 private function buildMultiArray (array $ fromKeys , string $ lastArrayValue = '' ): array
342378 {
@@ -356,6 +392,10 @@ private function buildMultiArray(array $fromKeys, string $lastArrayValue = ''):
356392
357393 /**
358394 * Convert multi arrays to specific CLI table rows (flat array)
395+ *
396+ * @param array<array-key, mixed> $array
397+ *
398+ * @return list<array{string, string}>
359399 */
360400 private function arrayToTableRows (string $ langFileName , array $ array ): array
361401 {
@@ -381,12 +421,12 @@ private function arrayToTableRows(string $langFileName, array $array): array
381421 */
382422 private function writeIsVerbose (string $ text = '' , ?string $ foreground = null , ?string $ background = null ): void
383423 {
384- if ($ this ->verbose ) {
424+ if ($ this ->getValidatedOption ( ' verbose ' ) === true ) {
385425 CLI ::write ($ text , $ foreground , $ background );
386426 }
387427 }
388428
389- private function isSubDirectory (string $ directory , string $ rootDirectory ): bool
429+ private function isSubdirectory (string $ directory , string $ rootDirectory ): bool
390430 {
391431 return 0 === strncmp ($ directory , $ rootDirectory , strlen ($ directory ));
392432 }
@@ -407,7 +447,7 @@ private function findLanguageKeysInFiles(array $files): array
407447 continue ;
408448 }
409449
410- $ this ->writeIsVerbose ('File found: ' . mb_substr ($ file ->getRealPath (), mb_strlen (APPPATH )));
450+ $ this ->writeIsVerbose (sprintf ( 'File found: %s ' , mb_substr ($ file ->getRealPath (), mb_strlen (APPPATH ) )));
411451 $ countFiles ++;
412452
413453 $ findInFile = $ this ->findTranslationsInFile ($ file );
0 commit comments