1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace Longman\LaravelMultiLang; |
6
|
|
|
|
7
|
|
|
use Closure; |
8
|
|
|
use Illuminate\Cache\CacheManager as Cache; |
9
|
|
|
use Illuminate\Database\DatabaseManager as Database; |
10
|
|
|
use Illuminate\Http\Request; |
11
|
|
|
use InvalidArgumentException; |
12
|
|
|
use Symfony\Component\Translation\Loader\ArrayLoader; |
13
|
|
|
use Symfony\Component\Translation\Translator; |
14
|
|
|
|
15
|
|
|
class MultiLang |
16
|
|
|
{ |
17
|
|
|
/** |
18
|
|
|
* Language/Locale. |
19
|
|
|
* |
20
|
|
|
* @var string |
21
|
|
|
*/ |
22
|
|
|
protected $lang; |
23
|
|
|
|
24
|
|
|
/** |
25
|
|
|
* System environment |
26
|
|
|
* |
27
|
|
|
* @var string |
28
|
|
|
*/ |
29
|
|
|
protected $environment; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* Config. |
33
|
|
|
* |
34
|
|
|
* @var \Longman\LaravelMultiLang\Config |
35
|
|
|
*/ |
36
|
|
|
protected $config; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* Repository |
40
|
|
|
* |
41
|
|
|
* @var \Longman\LaravelMultiLang\Repository |
42
|
|
|
*/ |
43
|
|
|
protected $repository; |
44
|
|
|
|
45
|
|
|
/** |
46
|
|
|
* Texts. |
47
|
|
|
* |
48
|
|
|
* @var array |
49
|
|
|
*/ |
50
|
|
|
protected $texts; |
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* Missing texts. |
54
|
|
|
* |
55
|
|
|
* @var array |
56
|
|
|
*/ |
57
|
|
|
protected $new_texts; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* Application scope. |
61
|
|
|
* |
62
|
|
|
* @var string |
63
|
|
|
*/ |
64
|
|
|
protected $scope = 'global'; |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* Translator instance |
68
|
|
|
* |
69
|
|
|
* @var \Symfony\Component\Translation\Translator |
70
|
|
|
*/ |
71
|
|
|
protected $translator; |
72
|
|
|
|
73
|
|
|
/** |
74
|
|
|
* Create a new MultiLang instance. |
75
|
|
|
* |
76
|
|
|
* @param string $environment |
77
|
|
|
* @param array $config |
78
|
|
|
* @param \Illuminate\Cache\CacheManager $cache |
79
|
|
|
* @param \Illuminate\Database\DatabaseManager $db |
80
|
33 |
|
*/ |
81
|
|
|
public function __construct(string $environment, array $config, Cache $cache, Database $db) |
82
|
33 |
|
{ |
83
|
|
|
$this->environment = $environment; |
84
|
33 |
|
|
85
|
|
|
$this->setConfig($config); |
86
|
33 |
|
|
87
|
33 |
|
$this->setRepository(new Repository($this->config, $cache, $db)); |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* Set multilang config |
92
|
|
|
* |
93
|
|
|
* @param array $config |
94
|
|
|
* @return $this |
95
|
33 |
|
*/ |
96
|
|
|
public function setConfig(array $config): MultiLang |
97
|
33 |
|
{ |
98
|
|
|
$this->config = new Config($config); |
99
|
33 |
|
|
100
|
|
|
return $this; |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
/** |
104
|
|
|
* Get multilang config |
105
|
|
|
* |
106
|
|
|
* @return \Longman\LaravelMultiLang\Config |
107
|
1 |
|
*/ |
108
|
|
|
public function getConfig(): Config |
109
|
|
|
{ |
110
|
1 |
|
|
111
|
|
|
return $this->config; |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* Set repository object |
116
|
|
|
* |
117
|
|
|
* @param \Longman\LaravelMultiLang\Repository $repository |
118
|
|
|
* @return $this |
119
|
33 |
|
*/ |
120
|
|
|
public function setRepository(Repository $repository): MultiLang |
121
|
33 |
|
{ |
122
|
|
|
$this->repository = $repository; |
123
|
33 |
|
|
124
|
|
|
return $this; |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
/** |
128
|
|
|
* Get repository object |
129
|
|
|
* |
130
|
|
|
* @return \Longman\LaravelMultiLang\Repository |
131
|
3 |
|
*/ |
132
|
|
|
public function getRepository(): Repository |
133
|
3 |
|
{ |
134
|
|
|
return $this->repository; |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
/** |
138
|
|
|
* Set application scope |
139
|
|
|
* |
140
|
|
|
* @param $scope |
141
|
|
|
* @return $this |
142
|
1 |
|
*/ |
143
|
|
|
public function setScope($scope): MultiLang |
144
|
1 |
|
{ |
145
|
|
|
$this->scope = $scope; |
146
|
1 |
|
|
147
|
|
|
return $this; |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
/** |
151
|
|
|
* Get application scope |
152
|
|
|
* |
153
|
|
|
* @return string |
154
|
1 |
|
*/ |
155
|
|
|
public function getScope(): string |
156
|
1 |
|
{ |
157
|
|
|
return $this->scope; |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
/** |
161
|
|
|
* Set locale |
162
|
|
|
* |
163
|
|
|
* @param string $lang |
164
|
|
|
* @return void |
165
|
|
|
*/ |
166
|
27 |
|
public function setLocale(string $lang) |
167
|
|
|
{ |
168
|
27 |
|
if (! $lang) { |
169
|
1 |
|
throw new InvalidArgumentException('Locale is empty'); |
170
|
|
|
} |
171
|
26 |
|
$this->lang = $lang; |
172
|
|
|
} |
173
|
26 |
|
|
174
|
23 |
|
public function loadTexts(?string $locale = null, ?string $scope = null): array |
175
|
|
|
{ |
176
|
|
|
if (is_null($locale)) { |
177
|
26 |
|
$locale = $this->getLocale(); |
178
|
26 |
|
} |
179
|
|
|
|
180
|
|
|
if (is_null($scope)) { |
181
|
|
|
$scope = $this->getScope(); |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
if ($this->environment !== 'production' || $this->config->get('cache.enabled', true) === false) { |
185
|
|
|
$texts = $this->repository->loadFromDatabase($locale, $scope); |
186
|
|
|
} else { |
187
|
23 |
|
if ($this->repository->existsInCache($locale, $scope)) { |
188
|
|
|
$texts = $this->repository->loadFromCache($locale, $scope); |
189
|
23 |
|
} else { |
190
|
20 |
|
$texts = $this->repository->loadFromDatabase($locale, $scope); |
191
|
|
|
$this->repository->storeInCache($locale, $texts, $scope); |
192
|
20 |
|
} |
193
|
|
|
} |
194
|
|
|
|
195
|
4 |
|
$this->createTranslator($locale, $scope, $texts); |
196
|
1 |
|
|
197
|
|
|
$this->texts = $texts; |
198
|
4 |
|
|
199
|
4 |
|
return $texts; |
200
|
|
|
} |
201
|
|
|
|
202
|
4 |
|
protected function createTranslator(string $locale, string $scope, array $texts): Translator |
203
|
|
|
{ |
204
|
|
|
$this->translator = new Translator($locale); |
205
|
|
|
$this->translator->addLoader('array', new ArrayLoader()); |
206
|
|
|
$this->translator->addResource('array', $texts, $locale, $scope); |
207
|
|
|
|
208
|
|
|
return $this->translator; |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
/** |
212
|
9 |
|
* Get translated text |
213
|
|
|
* |
214
|
|
|
* @param string $key |
215
|
9 |
|
* @param array $replacements |
216
|
1 |
|
* @return string |
217
|
|
|
*/ |
218
|
|
|
public function get(string $key, array $replacements = []): string |
219
|
8 |
|
{ |
220
|
1 |
|
if (! $this->getConfig()->get('use_texts', true)) { |
221
|
|
|
throw new InvalidArgumentException('Using texts from database is disabled in config'); |
222
|
|
|
} |
223
|
7 |
|
|
224
|
4 |
|
if (empty($key)) { |
225
|
|
|
throw new InvalidArgumentException('Text key not provided'); |
226
|
4 |
|
} |
227
|
|
|
|
228
|
|
|
if (! $this->lang) { |
229
|
3 |
|
return $key; |
230
|
|
|
} |
231
|
3 |
|
|
232
|
|
|
if (is_null($this->texts)) { |
233
|
|
|
// Load texts from storage |
234
|
|
|
$this->loadTexts(); |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
if (! isset($this->texts[$key])) { |
238
|
|
|
$this->queueToSave($key); |
239
|
|
|
} |
240
|
|
|
|
241
|
7 |
|
if (! empty($replacements)) { |
242
|
|
|
$keys = array_keys($replacements); |
243
|
7 |
|
$keys = array_map(static function ($v) { |
244
|
6 |
|
return ':' . $v; |
245
|
|
|
}, $keys); |
246
|
|
|
$replacements = array_combine($keys, $replacements); |
247
|
1 |
|
} |
248
|
|
|
|
249
|
|
|
return $this->translator->trans($key, $replacements, $this->getScope()); |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
/** |
253
|
|
|
* Get redirect url in middleware |
254
|
|
|
* |
255
|
|
|
* @param \Illuminate\Http\Request $request |
256
|
|
|
* @return string |
257
|
1 |
|
*/ |
258
|
|
|
public function getRedirectUrl(Request $request): string |
259
|
1 |
|
{ |
260
|
|
|
$exclude_patterns = $this->config->get('exclude_segments', []); |
261
|
1 |
|
if (! empty($exclude_patterns)) { |
262
|
1 |
|
if (call_user_func_array([$request, 'is'], $exclude_patterns)) { |
263
|
1 |
|
return ''; |
264
|
1 |
|
} |
265
|
1 |
|
} |
266
|
|
|
|
267
|
|
|
$locale = $request->segment(1); |
268
|
|
|
$fallback_locale = $this->config->get('default_locale', 'en'); |
269
|
1 |
|
if (! empty($locale) && strlen($locale) === 2) { |
270
|
|
|
$locales = $this->config->get('locales', []); |
271
|
|
|
|
272
|
|
|
if (! isset($locales[$locale])) { |
273
|
|
|
$segments = $request->segments(); |
274
|
|
|
$segments[0] = $fallback_locale; |
275
|
|
|
$url = implode('/', $segments); |
276
|
|
|
if ($query_string = $request->server->get('QUERY_STRING')) { |
277
|
|
|
$url .= '?' . $query_string; |
278
|
|
|
} |
279
|
|
|
|
280
|
1 |
|
return $url; |
281
|
1 |
|
} |
282
|
1 |
|
} else { |
283
|
|
|
$segments = $request->segments(); |
284
|
|
|
$url = $fallback_locale . '/' . implode('/', $segments); |
285
|
|
|
if ($query_string = $request->server->get('QUERY_STRING')) { |
286
|
|
|
$url .= '?' . $query_string; |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
return $url; |
290
|
|
|
} |
291
|
5 |
|
|
292
|
|
|
return ''; |
293
|
5 |
|
} |
294
|
5 |
|
|
295
|
5 |
|
/** |
296
|
5 |
|
* Detect locale based on url segment |
297
|
|
|
* |
298
|
|
|
* @param \Illuminate\Http\Request $request |
299
|
|
|
* @return string |
300
|
5 |
|
*/ |
301
|
4 |
|
public function detectLocale(Request $request): string |
302
|
|
|
{ |
303
|
4 |
|
$locale = $request->segment(1); |
304
|
3 |
|
$locales = $this->config->get('locales'); |
305
|
3 |
|
|
306
|
3 |
|
if (isset($locales[$locale])) { |
307
|
3 |
|
return $locales[$locale]['locale'] ?? $locale; |
308
|
1 |
|
} |
309
|
|
|
|
310
|
|
|
return (string) $this->config->get('default_locale', 'en'); |
311
|
4 |
|
} |
312
|
|
|
|
313
|
|
|
/** |
314
|
1 |
|
* Wrap routes to available languages group |
315
|
1 |
|
* |
316
|
1 |
|
* @param \Closure $callback |
317
|
|
|
* @return void |
318
|
|
|
*/ |
319
|
|
|
public function routeGroup(Closure $callback) |
320
|
1 |
|
{ |
321
|
|
|
$router = app('router'); |
322
|
|
|
|
323
|
1 |
|
$locales = $this->config->get('locales', []); |
324
|
|
|
|
325
|
|
|
foreach ($locales as $locale => $val) { |
326
|
|
|
$router->group([ |
327
|
|
|
'prefix' => $locale, |
328
|
|
|
'as' => $locale . '.', |
329
|
|
|
], $callback); |
330
|
|
|
} |
331
|
|
|
} |
332
|
2 |
|
|
333
|
|
|
/** |
334
|
2 |
|
* Get texts |
335
|
2 |
|
* |
336
|
|
|
* @return array |
337
|
2 |
|
*/ |
338
|
1 |
|
public function getTexts(): array |
339
|
|
|
{ |
340
|
|
|
|
341
|
1 |
|
return $this->texts; |
342
|
|
|
} |
343
|
|
|
|
344
|
|
|
/** |
345
|
|
|
* Get all texts |
346
|
|
|
* |
347
|
|
|
* @param string $lang |
348
|
|
|
* @param string $scope |
349
|
|
|
* @return array |
350
|
|
|
*/ |
351
|
|
|
public function getAllTexts(?string $lang = null, ?string $scope = null): array |
352
|
|
|
{ |
353
|
|
|
return $this->repository->loadAllFromDatabase($lang, $scope); |
354
|
|
|
} |
355
|
|
|
|
356
|
|
|
/** |
357
|
|
|
* Set texts manually |
358
|
|
|
* |
359
|
|
|
* @param array $texts_array |
360
|
|
|
* @return \Longman\LaravelMultiLang\MultiLang |
361
|
|
|
*/ |
362
|
|
|
public function setTexts(array $texts_array): MultiLang |
363
|
|
|
{ |
364
|
|
|
$texts = []; |
365
|
|
|
foreach ($texts_array as $key => $value) { |
366
|
|
|
$texts[$key] = $value; |
367
|
|
|
} |
368
|
|
|
|
369
|
|
|
$this->texts = $texts; |
370
|
|
|
|
371
|
|
|
$this->createTranslator($this->getLocale(), $this->getScope(), $texts); |
372
|
|
|
|
373
|
|
|
return $this; |
374
|
|
|
} |
375
|
|
|
|
376
|
|
|
/** |
377
|
|
|
* Queue missing texts |
378
|
|
|
* |
379
|
|
|
* @param string $key |
380
|
|
|
* @return void |
381
|
|
|
*/ |
382
|
|
|
protected function queueToSave(string $key) |
383
|
|
|
{ |
384
|
|
|
$this->new_texts[$key] = $key; |
385
|
|
|
} |
386
|
|
|
|
387
|
|
|
/** |
388
|
|
|
* Get language prefixed url |
389
|
|
|
* |
390
|
4 |
|
* @param string $path |
391
|
|
|
* @param string $lang |
392
|
|
|
* @return string |
393
|
4 |
|
*/ |
394
|
|
|
public function getUrl(string $path, ?string $lang = null): string |
395
|
|
|
{ |
396
|
|
|
$locale = $lang ?: $this->getLocale(); |
397
|
|
|
if ($locale) { |
398
|
|
|
$path = $locale . '/' . $this->removeLocaleFromPath($path); |
399
|
|
|
} |
400
|
|
|
|
401
|
|
|
return $path; |
402
|
|
|
} |
403
|
1 |
|
|
404
|
|
|
/** |
405
|
1 |
|
* Remove locale from the path |
406
|
|
|
* |
407
|
|
|
* @param string $path |
408
|
|
|
* @return string |
409
|
|
|
*/ |
410
|
|
|
private function removeLocaleFromPath(string $path): string |
411
|
|
|
{ |
412
|
|
|
$lang_path = $path; |
413
|
|
|
|
414
|
4 |
|
// Remove domain from path |
415
|
|
|
$app_url = config('app.url', ''); |
416
|
4 |
|
if (! empty($app_url) && mb_substr($lang_path, 0, mb_strlen($app_url)) === $app_url) { |
417
|
4 |
|
$lang_path = ltrim(str_replace($app_url, '', $lang_path), '/'); |
418
|
4 |
|
} |
419
|
|
|
|
420
|
|
|
$locales = $this->config->get('locales'); |
421
|
4 |
|
$locale = mb_substr($lang_path, 0, 2); |
422
|
|
|
if (isset($locales[$locale])) { |
423
|
4 |
|
return mb_substr($lang_path, 3); |
424
|
|
|
} |
425
|
|
|
|
426
|
|
|
return $path; |
427
|
|
|
} |
428
|
|
|
|
429
|
|
|
/** |
430
|
|
|
* Get language prefixed route |
431
|
|
|
* |
432
|
4 |
|
* @param string $name |
433
|
|
|
* @return string |
434
|
4 |
|
*/ |
435
|
4 |
|
public function getRoute(string $name): string |
436
|
|
|
{ |
437
|
|
|
$locale = $this->getLocale(); |
438
|
|
|
if ($locale) { |
439
|
|
|
$name = $locale . '.' . $name; |
440
|
|
|
} |
441
|
|
|
|
442
|
|
|
return $name; |
443
|
|
|
} |
444
|
4 |
|
|
445
|
|
|
/** |
446
|
4 |
|
* Check if autosave allowed |
447
|
4 |
|
* |
448
|
4 |
|
* @return bool |
449
|
|
|
*/ |
450
|
|
|
public function autoSaveIsAllowed() |
451
|
4 |
|
{ |
452
|
|
|
if ($this->environment === 'local' && $this->config->get('db.autosave', true)) { |
453
|
|
|
return true; |
454
|
|
|
} |
455
|
|
|
|
456
|
|
|
return false; |
457
|
|
|
} |
458
|
|
|
|
459
|
|
|
/** |
460
|
4 |
|
* Get locale |
461
|
|
|
* |
462
|
4 |
|
* @return string |
463
|
4 |
|
*/ |
464
|
4 |
|
public function getLocale() |
465
|
1 |
|
{ |
466
|
|
|
return $this->lang; |
467
|
|
|
} |
468
|
4 |
|
|
469
|
|
|
/** |
470
|
|
|
* Get available locales |
471
|
|
|
* |
472
|
|
|
* @return array |
473
|
|
|
*/ |
474
|
|
|
public function getLocales(): array |
475
|
|
|
{ |
476
|
|
|
return (array) $this->config->get('locales'); |
477
|
2 |
|
} |
478
|
|
|
|
479
|
2 |
|
/** |
480
|
2 |
|
* Save missing texts |
481
|
1 |
|
* |
482
|
|
|
* @return bool |
483
|
|
|
*/ |
484
|
2 |
|
public function saveTexts(): bool |
485
|
|
|
{ |
486
|
|
|
if (empty($this->new_texts)) { |
487
|
|
|
return false; |
488
|
|
|
} |
489
|
|
|
|
490
|
|
|
return $this->repository->save($this->new_texts, $this->scope); |
491
|
|
|
} |
492
|
|
|
} |
493
|
|
|
|