Passed
Push — master ( 0e0687...5606fe )
by Dante
01:10
created

Gettext::header()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 31
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

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