• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codeigniter4 / CodeIgniter4 / 27466302243

13 Jun 2026 12:06PM UTC coverage: 89.097% (+0.003%) from 89.094%
27466302243

push

github

web-flow
refactor: migrate `lang:*` commands as modern commands (#10306)

109 of 122 new or added lines in 2 files covered. (89.34%)

1 existing line in 1 file now uncovered.

24956 of 28010 relevant lines covered (89.1%)

224.23 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.96
/system/Commands/Translation/LocalizationSync.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\Commands\Translation;
15

16
use CodeIgniter\CLI\AbstractCommand;
17
use CodeIgniter\CLI\Attributes\Command;
18
use CodeIgniter\CLI\CLI;
19
use CodeIgniter\CLI\Input\Option;
20
use CodeIgniter\Exceptions\LogicException;
21
use Config\App;
22
use ErrorException;
23
use FilesystemIterator;
24
use Locale;
25
use RecursiveDirectoryIterator;
26
use RecursiveIteratorIterator;
27
use SplFileInfo;
28

29
/**
30
 * Synchronizes translation files from one language to another.
31
 */
32
#[Command(
33
    name: 'lang:sync',
34
    description: 'Synchronize translation files from one language to another.',
35
    group: 'Translation',
36
)]
37
class LocalizationSync extends AbstractCommand
38
{
39
    private string $languagePath;
40

41
    protected function configure(): void
42
    {
43
        $this
8✔
44
            ->addOption(new Option(
8✔
45
                name: 'locale',
8✔
46
                description: 'The original locale (en, ru, etc.).',
8✔
47
                requiresValue: true,
8✔
48
                default: '',
8✔
49
            ))
8✔
50
            ->addOption(new Option(
8✔
51
                name: 'target',
8✔
52
                description: 'Target locale (en, ru, etc.).',
8✔
53
                requiresValue: true,
8✔
54
                default: '',
8✔
55
            ));
8✔
56
    }
57

58
    protected function execute(array $arguments, array $options): int
59
    {
60
        $locale = $options['locale'];
7✔
61
        assert(is_string($locale));
62

63
        $target = $options['target'];
7✔
64
        assert(is_string($target));
65

66
        $this->languagePath = APPPATH . 'Language';
7✔
67
        $supportedLocales   = config(App::class)->supportedLocales;
7✔
68

69
        if ($locale === '') {
7✔
70
            $locale = Locale::getDefault();
4✔
71
        }
72

73
        if (! in_array($locale, $supportedLocales, true)) {
7✔
74
            return $this->errorUnsupportedLocale($locale);
1✔
75
        }
76

77
        if ($target === '') {
6✔
78
            CLI::error(
×
NEW
79
                sprintf(
×
NEW
80
                    'Error: "--target" is not configured. Supported locales: %s',
×
NEW
81
                    implode(', ', $supportedLocales),
×
NEW
82
                ),
×
NEW
83
                'light_gray',
×
NEW
84
                'red',
×
UNCOV
85
            );
×
86

87
            return EXIT_USER_INPUT;
×
88
        }
89

90
        if (! in_array($target, $supportedLocales, true)) {
6✔
91
            return $this->errorUnsupportedLocale($target);
1✔
92
        }
93

94
        if ($target === $locale) {
5✔
NEW
95
            CLI::error('Error: You cannot have the same values for "--target" and "--locale".', 'light_gray', 'red');
×
96

97
            return EXIT_USER_INPUT;
×
98
        }
99

100
        if (service('environment')->isTesting()) {
5✔
101
            $this->languagePath = SUPPORTPATH . 'Language';
5✔
102
        }
103

104
        if ($this->process($locale, $target) === EXIT_ERROR) {
5✔
105
            return EXIT_ERROR;
×
106
        }
107

108
        CLI::write('All operations done!', 'green');
3✔
109

110
        return EXIT_SUCCESS;
3✔
111
    }
112

113
    /**
114
     * Writes the unsupported-locale error and returns the user-input exit code.
115
     */
116
    private function errorUnsupportedLocale(string $locale): int
117
    {
118
        CLI::error(
2✔
119
            sprintf(
2✔
120
                'Error: "%s" is not supported. Supported locales: %s',
2✔
121
                $locale,
2✔
122
                implode(', ', config(App::class)->supportedLocales),
2✔
123
            ),
2✔
124
            'light_gray',
2✔
125
            'red',
2✔
126
        );
2✔
127

128
        return EXIT_USER_INPUT;
2✔
129
    }
130

131
    private function process(string $originalLocale, string $targetLocale): int
132
    {
133
        $originalLocaleDir = $this->languagePath . DIRECTORY_SEPARATOR . $originalLocale;
6✔
134
        $targetLocaleDir   = $this->languagePath . DIRECTORY_SEPARATOR . $targetLocale;
6✔
135

136
        if (! is_dir($originalLocaleDir)) {
6✔
137
            CLI::error(
1✔
138
                sprintf('Error: The "%s" directory was not found.', clean_path($originalLocaleDir)),
1✔
139
                'light_gray',
1✔
140
                'red',
1✔
141
            );
1✔
142

143
            return EXIT_ERROR;
1✔
144
        }
145

146
        // Unifying the error - mkdir() may cause an exception.
147
        try {
148
            if (! is_dir($targetLocaleDir) && ! mkdir($targetLocaleDir, 0775)) {
6✔
149
                throw new ErrorException();
5✔
150
            }
151
        } catch (ErrorException $e) {
1✔
152
            CLI::error(
1✔
153
                sprintf('Error: The target directory "%s" cannot be accessed.', clean_path($targetLocaleDir)),
1✔
154
                'light_gray',
1✔
155
                'red',
1✔
156
            );
1✔
157

158
            return EXIT_ERROR;
1✔
159
        }
160

161
        $iterator = new RecursiveIteratorIterator(
5✔
162
            new RecursiveDirectoryIterator(
5✔
163
                $originalLocaleDir,
5✔
164
                FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS,
5✔
165
            ),
5✔
166
        );
5✔
167

168
        $files = iterator_to_array($iterator);
5✔
169
        ksort($files);
5✔
170

171
        /** @var SplFileInfo $originalLanguageFile */
172
        foreach ($files as $originalLanguageFile) {
5✔
173
            if ($originalLanguageFile->getExtension() !== 'php') {
5✔
174
                continue;
×
175
            }
176

177
            $targetLanguageFile = $targetLocaleDir . DIRECTORY_SEPARATOR . $originalLanguageFile->getFilename();
5✔
178

179
            $targetLanguageKeys   = [];
5✔
180
            $originalLanguageKeys = include $originalLanguageFile;
5✔
181

182
            if (is_file($targetLanguageFile)) {
5✔
183
                $targetLanguageKeys = include $targetLanguageFile;
1✔
184
            }
185

186
            $targetLanguageKeys = $this->mergeLanguageKeys($originalLanguageKeys, $targetLanguageKeys, $originalLanguageFile->getBasename('.php'));
5✔
187

188
            $content = "<?php\n\nreturn " . var_export($targetLanguageKeys, true) . ";\n";
5✔
189
            file_put_contents($targetLanguageFile, $content);
5✔
190
        }
191

192
        return EXIT_SUCCESS;
3✔
193
    }
194

195
    /**
196
     * @param array<string, array<string,mixed>|string|null> $originalLanguageKeys
197
     * @param array<string, array<string,mixed>|string|null> $targetLanguageKeys
198
     *
199
     * @return array<string, array<string,mixed>|string|null>
200
     */
201
    private function mergeLanguageKeys(array $originalLanguageKeys, array $targetLanguageKeys, string $prefix = ''): array
202
    {
203
        $mergedLanguageKeys = [];
5✔
204

205
        foreach ($originalLanguageKeys as $key => $value) {
5✔
206
            $placeholderValue = $prefix !== '' ? $prefix . '.' . $key : $key;
5✔
207

208
            if (is_string($value)) {
5✔
209
                // Keep the old value
210
                // TODO: The value type may not match the original one
211
                if (array_key_exists($key, $targetLanguageKeys)) {
5✔
212
                    $mergedLanguageKeys[$key] = $targetLanguageKeys[$key];
1✔
213

214
                    continue;
1✔
215
                }
216

217
                // Set new key with placeholder
218
                $mergedLanguageKeys[$key] = $placeholderValue;
5✔
219
            } elseif (is_array($value)) {
5✔
220
                if (! array_key_exists($key, $targetLanguageKeys)) {
5✔
221
                    $mergedLanguageKeys[$key] = $this->mergeLanguageKeys($value, [], $placeholderValue);
5✔
222

223
                    continue;
5✔
224
                }
225

226
                $mergedLanguageKeys[$key] = $this->mergeLanguageKeys($value, $targetLanguageKeys[$key], $placeholderValue);
1✔
227
            } else {
228
                throw new LogicException(sprintf(
2✔
229
                    'Value for the key "%s" is of the wrong type. Only "array" or "string" is allowed.',
2✔
230
                    $placeholderValue,
2✔
231
                ));
2✔
232
            }
233
        }
234

235
        return $mergedLanguageKeys;
5✔
236
    }
237
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc