Issues (6)

src/View/Helper/I18nHelper.php (1 issue)

Labels
Severity
1
<?php
2
declare(strict_types=1);
3
4
/**
5
 * BEdita, API-first content management framework
6
 * Copyright 2019 ChannelWeb 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
namespace BEdita\I18n\View\Helper;
16
17
use BEdita\I18n\Core\I18nTrait;
18
use Cake\Core\Configure;
19
use Cake\Routing\Router;
20
use Cake\Utility\Hash;
21
use Cake\View\Helper;
22
23
/**
24
 * Helper to handle i18n things in view.
25
 *
26
 * @property \Cake\View\Helper\HtmlHelper $Html The HtmlHelper
27
 * @property \Cake\View\Helper\UrlHelper $Url The UrlHelper
28
 */
29
class I18nHelper extends Helper
30
{
31
    use I18nTrait;
32
33
    /**
34
     * @inheritDoc
35
     */
36
    public array $helpers = ['Html', 'Url'];
37
38
    /**
39
     * Translation data per object and lang (internal cache).
40
     * If `null` no cache has been created, if empty array no translations
41
     * have been found.
42
     *
43
     * Structure:
44
     *
45
     *   translation[<object ID>][<lang>][<field>] = <value>.
46
     *
47
     * @var array|null
48
     */
49
    protected ?array $translation = null;
50
51
    /**
52
     * Return the current URL replacing current lang with new lang passed.
53
     *
54
     * @param string $newLang The new lang you want in URL.
55
     * @param string|null $switchUrl The switch lang URL defined for this app, if any.
56
     * @return string
57
     */
58
    public function changeUrlLang(string $newLang, ?string $switchUrl = null): string
59
    {
60
        $request = Router::getRequest();
61
        if (empty($request)) {
62
            return '';
63
        }
64
        $path = $request->getUri()->getPath();
65
        $query = $request->getUri()->getQuery();
66
67
        $newLangUrl = $this->newLangUrl($newLang, $path, $query);
68
        if ($newLangUrl !== null) {
69
            return $newLangUrl;
70
        }
71
72
        if (!empty($switchUrl)) {
73
            return sprintf('%s?new=%s', $switchUrl, $newLang);
74
        }
75
76
        if (!empty($query)) {
77
            $path .= sprintf('?%s', $query);
78
        }
79
80
        return $path;
81
    }
82
83
    /**
84
     * Try to create a new language URL from current path using lang prefix.
85
     *
86
     * @param string $newLang The new lang you want in URL.
87
     * @param string $path The current URL path.
88
     * @param string $query The current URL query.
89
     * @return string|null The new lang url or null if no lang prefix was found
90
     */
91
    protected function newLangUrl(string $newLang, string $path, string $query): ?string
92
    {
93
        if (!$this->isI18nPath($path)) {
94
            return null;
95
        }
96
97
        $prefix = sprintf('/%s', $this->getLang());
98
        $url = sprintf('/%s', $newLang) . substr($path, strlen($prefix));
99
        if ($query) {
100
            $url .= '?' . $query;
101
        }
102
103
        return $url;
104
    }
105
106
    /**
107
     * Return true if an URL path has I18n structure i.e. /:lang/other/path or /:lang
108
     *
109
     * @param string $path The path to check.
110
     * @return bool
111
     */
112
    protected function isI18nPath(string $path): bool
113
    {
114
        $prefix = sprintf('/%s', $this->getLang());
115
116
        return stripos($path, $prefix . '/') === 0 || $path === $prefix;
117
    }
118
119
    /**
120
     * Create hreflang meta tags for available languages.
121
     * The meta will be created only if a recognizable i18n path was found on current URL.
122
     *
123
     * @return string
124
     */
125
    public function metaHreflang(): string
126
    {
127
        $request = Router::getRequest();
128
        if ($request === null) {
129
            return '';
130
        }
131
132
        $path = $request->getUri()->getPath();
133
        if (!$this->isI18nPath($path)) {
134
            return '';
135
        }
136
137
        $query = $request->getUri()->getQuery();
138
        $meta = '';
139
        foreach (array_keys($this->getLanguages()) as $code) {
140
            $url = Router::url($this->newLangUrl($code, $path, $query), true);
141
            $meta .= $this->Html->meta([
142
                'rel' => 'alternate',
143
                'hreflang' => $code,
144
                'link' => $url,
145
            ]);
146
        }
147
148
        return $meta;
149
    }
150
151
    /**
152
     * Translate object field
153
     * Return translation (by response object and included data, field and language)
154
     *
155
     * @param array $object The object to translate
156
     * @param string $attribute The attribute name
157
     * @param string|null $lang The lang (2 chars string)
158
     * @param bool $defaultNull Pass true when you want null as default, on missing translation
159
     * @param array $included The included translations data
160
     * @return string|null
161
     */
162
    public function field(
163
        array $object,
164
        string $attribute,
165
        ?string $lang = null,
166
        bool $defaultNull = false,
167
        array $included = []
168
    ): ?string {
169
        $defaultValue = null;
170
        if (!$defaultNull) {
171
            $defaultValue = Hash::get(
172
                $object,
173
                sprintf('attributes.%s', $attribute),
174
                Hash::get($object, sprintf('%s', $attribute))
175
            );
176
        }
177
        if (empty($included) && !empty($this->_View->get('included'))) {
178
            $included = $this->_View->get('included');
179
        }
180
        if (empty($lang)) {
181
            $lang = Configure::read('I18n.lang', '');
182
        }
183
        $returnValue = $this->getTranslatedField($object, $attribute, $lang, $included);
184
        if ($returnValue === null) {
185
            return $defaultValue;
186
        }
187
188
        return $returnValue;
189
    }
190
191
    /**
192
     * Verify that object has translation for the specified attribute and lang
193
     *
194
     * @param array $object The object to translate
195
     * @param string $attribute The attribute name
196
     * @param string|null $lang The lang (2 chars string
197
     * @param array $included The included translations data)
198
     * @return bool
199
     */
200
    public function exists(array $object, string $attribute, ?string $lang = null, array &$included = []): bool
201
    {
202
        if (empty($included) && !empty($this->_View->get('included'))) {
203
            $included = $this->_View->get('included');
204
        }
205
        if (empty($lang)) {
206
            $lang = Configure::read('I18n.lang', '');
207
        }
208
        $val = $this->getTranslatedField($object, $attribute, $lang, $included);
209
210
        return $val !== null;
211
    }
212
213
    /**
214
     * Reset internal translation cache.
215
     * To use when `included` array has changed.
216
     *
217
     * @return void
218
     */
219
    public function reset(): void
220
    {
221
        $this->translation = null;
222
    }
223
224
    /**
225
     * Return translated field per response object and included, attribute and lang. Null on missing translation.
226
     * First time that it's called per response object and included, it fills $this->translation data.
227
     * I.e.:
228
     *
229
     *     $this->translation[100]['en'] = ['title' => 'Example', 'description' => 'This is an example']
230
     *     $this->translation[100]['it'] = ['title' => 'Esempio', 'description' => 'Questo è un esempio']
231
     *     $this->translation[100]['sp'] = ['title' => 'Ejemplo', 'description' => 'Este es un ejemplo']
232
     *
233
     * @param array $object The object to translate
234
     * @param string $attribute The attribute name
235
     * @param string $lang The lang (2 chars string)
236
     * @param array $included The included translations data
237
     * @return string|null The translation of attribute field per object response and lang
238
     */
239
    private function getTranslatedField(array $object, string $attribute, string $lang, array &$included): ?string
240
    {
241
        // first look if embedded relationships are set
242
        if (Hash::check($object, 'relationships.translations.data.0.attributes')) {
243
            $translations = Hash::combine(
244
                $object['relationships']['translations']['data'],
245
                '{n}.attributes.lang',
246
                '{n}.attributes.translated_fields'
247
            );
248
249
            return Hash::get($translations, sprintf('%s.%s', $lang, $attribute));
250
        }
251
252
        if (empty($object['id'])) {
253
            return null;
254
        }
255
256
        $id = $object['id'];
257
258
        if ($this->translation === null) {
259
            $translations = Hash::combine($included, '{n}.id', '{n}.attributes', '{n}.type');
260
            $this->translation = Hash::combine(
261
                $translations,
262
                'translations.{n}.lang',
263
                'translations.{n}.translated_fields',
264
                'translations.{n}.object_id'
265
            );
266
        }
267
268
        $path = sprintf('%s.%s.%s', $id, $lang, $attribute);
269
270
        return Hash::get($this->translation, $path);
0 ignored issues
show
It seems like $this->translation can also be of type null; however, parameter $data of Cake\Utility\Hash::get() does only seem to accept ArrayAccess|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

270
        return Hash::get(/** @scrutinizer ignore-type */ $this->translation, $path);
Loading history...
271
    }
272
273
    /**
274
     * Build a language URL using lang prefix.
275
     *
276
     * @param array|string $path The current URL path. MUST be an absolute path, starting wih `/`
277
     * @param array $options Array of options.
278
     * @return string Full I18n URL.
279
     */
280
    public function buildUrl(mixed $path, array $options = []): string
281
    {
282
        if (is_string($path) && !$this->isI18nPath($path)) {
283
            $path = sprintf('/%s%s', $this->getLang(), $path);
284
        }
285
286
        return $this->Url->build($path, $options);
287
    }
288
}
289