1<?php
2
3function translate_catalog(): array {
4 return array(
5 'en' => array('name' => 'English', 'native' => 'English', 'flag' => 'uk'),
6 'pt_br' => array('name' => 'Portuguese', 'native' => 'Português', 'flag' => 'pt'),
7 'es' => array('name' => 'Spanish', 'native' => 'Español', 'flag' => 'es'),
8 'pl' => array('name' => 'Polish', 'native' => 'Polski', 'flag' => 'pl'),
9 'de' => array('name' => 'German', 'native' => 'Deutsch', 'flag' => 'de')
10 );
11}
12
13function translate_sanitize(string $code): string {
14 $code = strtolower(preg_replace('/[^a-zA-Z_]/', '', $code) ?? '');
15 return isset(translate_catalog()[$code]) ? $code : '';
16}
17
18function translate_default(): string {
19 $code = translate_sanitize((string)(config('language') ?? 'en'));
20 return ($code !== '') ? $code : 'en';
21}
22
23function translate_enabled(): array {
24 $configured = config('languages_enabled');
25
26 if (is_string($configured)) {
27 $decoded = json_decode($configured, true);
28 $configured = is_array($decoded) ? $decoded : explode(',', $configured);
29 }
30
31 $enabled = array();
32
33 if (is_array($configured)) {
34 foreach ($configured as $code) {
35 $clean = translate_sanitize((string)$code);
36 if ($clean !== '' && !in_array($clean, $enabled, true)) {
37 $enabled[] = $clean;
38 }
39 }
40 }
41
42 if (!$enabled) {
43 $enabled = array_keys(translate_catalog());
44 }
45
46 $default = translate_default();
47 if (!in_array($default, $enabled, true)) {
48 array_unshift($enabled, $default);
49 }
50
51 return $enabled;
52}
53
54function translate_from_browser(): string {
55 $header = (string)($_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? '');
56 if ($header === '') {
57 return '';
58 }
59
60 $enabled = translate_enabled();
61 $ranked = array();
62
63 foreach (explode(',', $header) as $part) {
64 $bits = explode(';q=', trim($part));
65 $tag = strtolower(str_replace('-', '_', trim($bits[0])));
66 $q = isset($bits[1]) ? (float)$bits[1] : 1.0;
67
68 if ($tag === '') {
69 continue;
70 }
71
72 $ranked[] = array('tag' => $tag, 'q' => $q);
73 }
74
75 usort($ranked, function ($a, $b) { return $b['q'] <=> $a['q']; });
76
77 foreach ($ranked as $entry) {
78 $tag = $entry['tag'];
79
80 if (in_array($tag, $enabled, true)) {
81 return $tag;
82 }
83
84 $base = explode('_', $tag)[0];
85
86 foreach ($enabled as $code) {
87 if ($code === $base || strpos($code, $base . '_') === 0) {
88 return $code;
89 }
90 }
91 }
92
93 return '';
94}
95
96function translate_active(?string $set = null): string {
97 static $active = null;
98
99 if ($set !== null) {
100 $active = $set;
101 return $active;
102 }
103
104 if ($active !== null) {
105 return $active;
106 }
107
108 $enabled = translate_enabled();
109
110 $fromUrl = translate_sanitize((string)($_GET['lang'] ?? ''));
111 if ($fromUrl !== '' && in_array($fromUrl, $enabled, true)) {
112 translate_remember($fromUrl);
113 return $active = $fromUrl;
114 }
115
116 $prefix = (string)($GLOBALS['config']['session_prefix'] ?? '');
117 $fromStore = translate_sanitize((string)($_SESSION[$prefix . 'language'] ?? $_COOKIE[$prefix . 'language'] ?? ''));
118 if ($fromStore !== '' && in_array($fromStore, $enabled, true)) {
119 return $active = $fromStore;
120 }
121
122 $fromBrowser = translate_from_browser();
123 if ($fromBrowser !== '') {
124 return $active = $fromBrowser;
125 }
126
127 return $active = translate_default();
128}
129
130function translate_remember(string $code): void {
131 $code = translate_sanitize($code);
132 if ($code === '' || !in_array($code, translate_enabled(), true)) {
133 return;
134 }
135
136 $prefix = (string)($GLOBALS['config']['session_prefix'] ?? '');
137
138 if (session_status() === PHP_SESSION_ACTIVE) {
139 $_SESSION[$prefix . 'language'] = $code;
140 }
141
142 if (!headers_sent()) {
143 setcookie($prefix . 'language', $code, array(
144 'expires' => time() + (365 * 86400),
145 'path' => '/',
146 'samesite' => 'Lax'
147 ));
148 }
149
150 $_COOKIE[$prefix . 'language'] = $code;
151}
152
153function translate_load(string $code): array {
154 static $loaded = array();
155
156 $code = translate_sanitize($code);
157 if ($code === '') {
158 return array();
159 }
160
161 if (isset($loaded[$code])) {
162 return $loaded[$code];
163 }
164
165 $core = array();
166
167 $file = __DIR__ . '/../../locale/' . $code . '.php';
168 if (is_file($file)) {
169 $included = include $file;
170 if (is_array($included)) {
171 $core = $included;
172 }
173 }
174
175 return $loaded[$code] = translate_theme_strings($code) + $core + translate_plugin_strings($code);
176}
177
178function translate_theme_strings(string $code): array {
179 if (!function_exists('theme_chain') || !function_exists('theme_root')) {
180 return array();
181 }
182
183 $strings = array();
184
185 foreach (theme_chain() as $theme) {
186 $file = theme_root() . '/' . $theme . '/locale/' . $code . '.php';
187 if (!is_file($file)) {
188 continue;
189 }
190
191 $loaded = include $file;
192 if (is_array($loaded)) {
193 $strings += $loaded;
194 }
195 }
196
197 return $strings;
198}
199
200function translate_plugin_strings(string $code): array {
201 if (!function_exists('znote_plugins')) {
202 return array();
203 }
204
205 $strings = array();
206
207 foreach (znote_plugins() as $name => $plugin) {
208 if (empty($plugin['enabled'])) {
209 continue;
210 }
211
212 $file = rtrim((string)($plugin['path'] ?? ''), '/\\') . '/locale/' . $code . '.php';
213 if (!is_file($file)) {
214 continue;
215 }
216
217 $loaded = include $file;
218 if (is_array($loaded)) {
219 $strings += $loaded;
220 }
221 }
222
223 return $strings;
224}
225
226function t(string $key, array $vars = array()): string {
227 $active = translate_active();
228
229 $strings = translate_load($active);
230 $text = $strings[$key] ?? null;
231
232 if ($text === null && $active !== 'en') {
233 $fallback = translate_load('en');
234 $text = $fallback[$key] ?? null;
235 }
236
237 if ($text === null) {
238 $text = $key;
239 }
240
241 if ($vars) {
242 $replace = array();
243 foreach ($vars as $name => $value) {
244 $replace['{' . $name . '}'] = (string)$value;
245 }
246 $text = strtr($text, $replace);
247 }
248
249 return $text;
250}
251
252function te(string $key, array $vars = array()): void {
253 echo t($key, $vars);
254}
255
256if (!function_exists('h')) {
257 function h($s): string {
258 return htmlspecialchars((string)($s ?? ''), ENT_QUOTES, 'UTF-8');
259 }
260}
261
262function t_default(string $key, string $fallback, array $vars = array()): string {
263 $text = t($key, $vars);
264 return ($text === $key) ? $fallback : $text;
265}
266
267function translate_flag(string $flag): string {
268 if (!preg_match('/^[a-z]{2,3}$/', $flag) || !is_file(__DIR__ . '/../../assets/flags/' . $flag . '.png')) {
269 return '';
270 }
271
272 return '<img src="assets/flags/' . $flag . '.png" alt="" loading="lazy">';
273}
274
275function translate_url(string $code): string {
276 $query = $_GET;
277 $query['lang'] = $code;
278
279 $path = strtok((string)($_SERVER['REQUEST_URI'] ?? '/'), '?');
280
281 return htmlspecialchars(($path === false ? '/' : $path) . '?' . http_build_query($query), ENT_QUOTES, 'UTF-8');
282}
283
284function translate_selector(): string {
285 $enabled = translate_enabled();
286
287 if (count($enabled) < 2) {
288 return '';
289 }
290
291 $catalog = translate_catalog();
292 $active = translate_active();
293 $current = $catalog[$active] ?? $catalog['en'];
294
295 $items = '';
296 foreach ($enabled as $code) {
297 $entry = $catalog[$code] ?? null;
298 if ($entry === null) {
299 continue;
300 }
301
302 $items .= '<a href="' . translate_url($code) . '" hreflang="' . htmlspecialchars($code, ENT_QUOTES, 'UTF-8') . '"'
303 . ($code === $active ? ' class="is-active" aria-current="true"' : '') . '>'
304 . '<i>' . translate_flag($entry['flag']) . '</i>'
305 . '<span>' . htmlspecialchars($entry['native'], ENT_QUOTES, 'UTF-8') . '</span></a>';
306 }
307
308 return '<div class="znote-lang" id="znoteLang">'
309 . '<button type="button" class="znote-lang-current" aria-haspopup="true" aria-expanded="false" aria-label="'
310 . htmlspecialchars(t('language.choose'), ENT_QUOTES, 'UTF-8') . '">'
311 . '<i>' . translate_flag($current['flag']) . '</i>'
312 . '<span>' . htmlspecialchars(strtoupper(explode('_', $active)[0]), ENT_QUOTES, 'UTF-8') . '</span>'
313 . '<em></em></button>'
314 . '<div class="znote-lang-menu">' . $items . '</div></div>';
315}
316
317function translate_selector_assets(): string {
318 return '<style>'
319 . '.znote-lang{position:fixed;top:12px;right:12px;z-index:9999;font:500 13px/1.2 system-ui,-apple-system,Segoe UI,Roboto,sans-serif}'
320 . '.znote-lang *{box-sizing:border-box}'
321 . '.znote-lang i{display:block;width:20px;height:14px;overflow:hidden;border-radius:2px;box-shadow:0 0 0 1px rgba(0,0,0,.25)}'
322 . '.znote-lang i img{display:block;width:100%;height:100%;object-fit:cover}'
323 . '.znote-lang-current{display:flex;align-items:center;gap:7px;padding:6px 9px;border:0;border-radius:6px;cursor:pointer;'
324 . 'background:rgba(20,20,24,.82);color:#f2f2f4;backdrop-filter:blur(6px);box-shadow:0 2px 10px rgba(0,0,0,.3)}'
325 . '.znote-lang-current:hover{background:rgba(20,20,24,.95)}'
326 . '.znote-lang-current em{width:0;height:0;border:4px solid transparent;border-top-color:currentColor;margin-top:2px}'
327 . '.znote-lang-menu{position:absolute;top:calc(100% + 6px);right:0;min-width:170px;padding:5px;border-radius:8px;'
328 . 'background:rgba(20,20,24,.96);box-shadow:0 6px 24px rgba(0,0,0,.35);display:none}'
329 . '.znote-lang.is-open .znote-lang-menu{display:block}'
330 . '.znote-lang-menu a{display:flex;align-items:center;gap:9px;padding:7px 9px;border-radius:5px;color:#e8e8ec;text-decoration:none;white-space:nowrap}'
331 . '.znote-lang-menu a:hover{background:rgba(255,255,255,.1)}'
332 . '.znote-lang-menu a.is-active{background:rgba(255,255,255,.16)}'
333 . '@media(max-width:640px){.znote-lang{top:8px;right:8px}.znote-lang-current span{display:none}}'
334 . '@media print{.znote-lang{display:none}}'
335 . '</style>'
336 . '<script>(function(){var r=document.getElementById("znoteLang");if(!r)return;'
337 . 'var b=r.querySelector(".znote-lang-current");'
338 . 'b.addEventListener("click",function(e){e.stopPropagation();var o=r.classList.toggle("is-open");b.setAttribute("aria-expanded",o?"true":"false");});'
339 . 'document.addEventListener("click",function(){r.classList.remove("is-open");b.setAttribute("aria-expanded","false");});'
340 . 'document.addEventListener("keydown",function(e){if(e.key==="Escape"){r.classList.remove("is-open");b.setAttribute("aria-expanded","false");}});'
341 . '})();</script>';
342}
343
344function translate_selector_rendered(?bool $set = null): bool {
345 static $done = false;
346
347 if ($set !== null) {
348 $done = $set;
349 }
350
351 return $done;
352}
353
354function translate_inject(): string {
355 if (!config('language_selector') || translate_selector_rendered()) {
356 return '';
357 }
358
359 $markup = translate_selector();
360 if ($markup === '') {
361 return '';
362 }
363
364 translate_selector_rendered(true);
365
366 return $markup . translate_selector_assets();
367}
368