Completed
Push — master ( 756ca5...b4e4e7 )
by Stefano
13s queued 10s
created

I18nHelper::isI18nPath()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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