Issues (6)

src/Filesystem/Gettext.php (1 issue)

Labels
Severity
1
<?php
2
declare(strict_types=1);
3
4
/**
5
 * BEdita, API-first content management framework
6
 * Copyright 2023 Atlas Srl, Chialab Srl
7
 *
8
 * This file is part of BEdita: you can redistribute it and/or modify
9
 * it under the terms of the GNU Lesser General Public License as published
10
 * by the Free Software Foundation, either version 3 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * See LICENSE.LGPL or <http://gnu.org/licenses/lgpl-3.0.html> for more details.
14
 */
15
16
namespace BEdita\I18n\Filesystem;
17
18
use Cake\I18n\DateTime;
0 ignored issues
show
The type Cake\I18n\DateTime was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
19
use SplFileInfo;
20
21
/**
22
 * Gettext utilities.
23
 *
24
 * This class contains methods to analyze and create po/pot files.
25
 * It is used by the shell tasks.
26
 */
27
class Gettext
28
{
29
    /**
30
     * Analyze po file and translate it.
31
     * Returns an array with the following keys:
32
     *
33
     * - numItems: number of items
34
     * - numNotTranslated: number of not translated items
35
     * - translated: number of translated items
36
     * - percent: percentage of translated items
37
     *
38
     * @param string $filename The po file name
39
     * @return array
40
     */
41
    public static function analyzePoFile(string $filename): array
42
    {
43
        $lines = file($filename);
44
        $numItems = $numNotTranslated = 0;
45
        foreach ($lines as $k => $l) {
46
            if (strpos($l, 'msgid "') === 0) {
47
                $numItems++;
48
            }
49
            if (strpos($l, 'msgstr ""') === 0 && (!isset($lines[$k + 1]) || strpos($lines[$k + 1], '"') !== 0)) {
50
                $numNotTranslated++;
51
            }
52
        }
53
        $translated = $numItems - $numNotTranslated;
54
        $percent = $numItems === 0 ? 0 : number_format($translated * 100. / $numItems, 1);
55
56
        return compact('numItems', 'numNotTranslated', 'translated', 'percent');
57
    }
58
59
    /**
60
     * Header lines for po/pot file.
61
     * Returns the header string.
62
     *
63
     * @param string $type The file type (can be 'po', 'pot')
64
     * @return string
65
     */
66
    public static function header(string $type = 'po'): string
67
    {
68
        $dateTimeNow = new DateTime('now');
69
        $result = sprintf('msgid ""%smsgstr ""%s', "\n", "\n");
70
        $contents = [
71
            'po' => [
72
                'Project-Id-Version' => 'BEdita 4',
73
                'POT-Creation-Date' => $dateTimeNow->format('Y-m-d H:i:s'),
74
                'PO-Revision-Date' => '',
75
                'Last-Translator' => '',
76
                'Language-Team' => 'BEdita I18N & I10N Team',
77
                'Language' => '',
78
                'MIME-Version' => '1.0',
79
                'Content-Transfer-Encoding' => '8bit',
80
                'Plural-Forms' => 'nplurals=2; plural=(n != 1);',
81
                'Content-Type' => 'text/plain; charset=utf-8',
82
            ],
83
            'pot' => [
84
                'Project-Id-Version' => 'BEdita 4',
85
                'POT-Creation-Date' => $dateTimeNow->format('Y-m-d H:i:s'),
86
                'MIME-Version' => '1.0',
87
                'Content-Transfer-Encoding' => '8bit',
88
                'Language-Team' => 'BEdita I18N & I10N Team',
89
                'Plural-Forms' => 'nplurals=2; plural=(n != 1);',
90
                'Content-Type' => 'text/plain; charset=utf-8',
91
            ],
92
        ];
93
        foreach ($contents[$type] as $k => $v) {
94
            $result .= sprintf('"%s: %s \n"', $k, $v) . "\n";
95
        }
96
97
        return $result;
98
    }
99
100
    /**
101
     * Write `master.pot` file with all translations.
102
     * Returns an array with the following keys:
103
     *
104
     * - info: array of info messages
105
     * - updated: boolean, true if file has been updated
106
     *
107
     * @return array
108
     */
109
    public static function writeMasterPot(string $localePath, array $translations): array
110
    {
111
        $info = [];
112
        $updated = false;
113
114
        foreach ($translations as $domain => $poResult) {
115
            $potFilename = sprintf('%s/%s.pot', $localePath, $domain);
116
            $info[] = sprintf('Writing new .pot file: %s', $potFilename);
117
            $contents = file_exists($potFilename) ? file_get_contents($potFilename) : '';
118
119
            // remove headers from pot file
120
            $contents = preg_replace('/^msgid ""\nmsgstr ""/', '', $contents);
121
            $contents = trim(preg_replace('/^"([^"]*?)"$/m', '', $contents));
122
123
            $lines = [];
124
            ksort($poResult);
125
            foreach ($poResult as $res => $contexts) {
126
                sort($contexts);
127
                foreach ($contexts as $ctx) {
128
                    $msgctxt = sprintf('msgctxt "%s"%smsgid "%s"%smsgstr ""', $ctx, "\n", $res, "\n");
129
                    $msgidstr = sprintf('msgid "%s"%smsgstr ""', $res, "\n");
130
                    $lines[] = !empty($ctx) ? $msgctxt : $msgidstr;
131
                }
132
            }
133
134
            $result = implode("\n\n", $lines);
135
            if ($contents !== $result) {
136
                file_put_contents($potFilename, sprintf("%s\n%s\n", self::header('pot'), $result));
137
                $updated = true;
138
            }
139
        }
140
141
        return compact('info', 'updated');
142
    }
143
144
    /**
145
     * Write `.po` files for each locale.
146
     * Returns an array with the following keys:
147
     *
148
     * - info: array of info messages
149
     *
150
     * @return array
151
     */
152
    public static function writePoFiles(array $locales, string $localePath, array &$translations): array
153
    {
154
        $info = [];
155
        if (empty($locales)) {
156
            $info[] = 'No locales set, .po files generation skipped';
157
158
            return compact('info');
159
        }
160
161
        $header = self::header('po');
162
        foreach ($locales as $loc) {
163
            $potDir = $localePath . DS . $loc;
164
            if (!file_exists($potDir)) {
165
                mkdir($potDir);
166
            }
167
            $info[] = sprintf('Language: %s', $loc);
168
            foreach (array_keys($translations) as $domain) {
169
                $potFilename = sprintf('%s/%s.pot', $localePath, $domain);
170
                $poFile = sprintf('%s/%s.po', $potDir, $domain);
171
                if (!file_exists($poFile)) {
172
                    $newPoFile = new SplFileInfo($poFile);
173
                    $newPoFile->openFile('w')->fwrite($header);
174
                }
175
                $info[] = sprintf('Merging %s', $poFile);
176
                $mergeCmd = sprintf('msgmerge --backup=off -N -U %s %s', $poFile, $potFilename);
177
                exec($mergeCmd);
178
                $analysis = self::analyzePoFile($poFile);
179
                $info[] = sprintf(
180
                    'Translated %d of %d items - %s %%',
181
                    $analysis['translated'],
182
                    $analysis['numItems'],
183
                    $analysis['percent']
184
                );
185
                $info[] = '---------------------';
186
            }
187
        }
188
189
        return compact('info');
190
    }
191
}
192