v3.0.0.0
FlatlyPage
- <?php
-
- const FLATLY_LANG_CODE_PATTERN = '/^[a-z]{2,3}(?:-[a-z0-9]{2,8})?$/';
- const FLATLY_LANG_HTACCESS_MARKER = '# FlatlyPage languages';
-
- function flatly_lang_code_valid(string $code): bool
- {
- return preg_match(FLATLY_LANG_CODE_PATTERN, $code) === 1;
- }
-
- function flatly_limit_text(string $text, int $length): string
- {
- return preg_match('/^.{0,' . $length . '}/su', $text, $m) === 1 ? $m[0] : substr($text, 0, $length);
- }
-
- function flatly_main_settings(): array
- {
- static $settings = null;
- if ($settings === null) {
- $loaded = load_page('settings');
- $settings = is_array($loaded) ? $loaded : [];
- }
- return $settings;
- }
-
- function flatly_default_lang(): string
- {
- $code = strtolower(trim((string) (flatly_main_settings()['site_language'] ?? 'en')));
- return flatly_lang_code_valid($code) ? $code : 'en';
- }
-
- function flatly_default_language_name(): string
- {
- $custom = flatly_limit_text(trim((string) (flatly_main_settings()['site_language_name'] ?? '')), 50);
- if ($custom !== '') {
- return $custom;
- }
- $default = flatly_default_lang();
- return flatly_configured_languages()[$default] ?? strtoupper($default);
- }
-
- function flatly_configured_languages(): array
- {
- $list = flatly_main_settings()['languages'] ?? [];
- $languages = [];
- if (!is_array($list)) {
- return $languages;
- }
- foreach ($list as $row) {
- if (!is_array($row)) {
- continue;
- }
- $code = strtolower(trim((string) ($row['code'] ?? '')));
- $name = trim((string) ($row['name'] ?? ''));
- if (!flatly_lang_code_valid($code) || $name === '' || isset($languages[$code])) {
- continue;
- }
- $languages[$code] = flatly_limit_text($name, 50);
- }
- return $languages;
- }
-
- function flatly_extra_languages(): array
- {
- $languages = flatly_configured_languages();
- unset($languages[flatly_default_lang()]);
- return $languages;
- }
-
- function flatly_is_extra_lang(?string $code): bool
- {
- return $code !== null && $code !== '' && isset(flatly_extra_languages()[$code]);
- }
-
- function flatly_sanitize_languages(mixed $raw): array
- {
- if (is_string($raw)) {
- $raw = json_decode($raw, true);
- }
- $clean = [];
- $seen = [];
- if (!is_array($raw)) {
- return $clean;
- }
- foreach ($raw as $row) {
- if (!is_array($row)) {
- continue;
- }
- $code = strtolower(trim((string) ($row['code'] ?? '')));
- $name = flatly_limit_text(trim((string) ($row['name'] ?? '')), 50);
- if ($name === '' || !flatly_lang_code_valid($code) || isset($seen[$code])) {
- continue;
- }
- $seen[$code] = true;
- $clean[] = ['name' => $name, 'code' => $code];
- if (count($clean) >= 30) {
- break;
- }
- }
- return $clean;
- }
-
- function flatly_lang_file_id(string $lang, string $id): string
- {
- return $lang . '-lang-' . $id;
- }
-
- function flatly_split_lang_file_id(string $file_id): array
- {
- if (preg_match('/^([a-z]{2,3}(?:-[a-z0-9]{2,8})?)-lang-(.+)$/', $file_id, $m)) {
- return [$m[1], $m[2]];
- }
- return [null, $file_id];
- }
-
- function load_page_lang(string $id, ?string $lang): ?array
- {
- if ($lang === null || $lang === '') {
- return load_page($id);
- }
- if (!flatly_lang_code_valid($lang)) {
- return null;
- }
- $path = DATA_DIR . flatly_lang_file_id($lang, $id) . '.php';
- if (!is_file($path)) {
- return null;
- }
- $data = include $path;
- return is_array($data) ? $data : null;
- }
-
- function save_page_lang(string $id, string $lang, array $data): bool
- {
- if (!flatly_lang_code_valid($lang)) {
- return false;
- }
- return save_page(flatly_lang_file_id($lang, $id), $data);
- }
-
- function flatly_root_relative_urls(string $html): string
- {
- $fix = static function (string $value): string {
- if ($value === '' || preg_match('#^(?:[a-z][a-z0-9+.\-]*:|//|/|\#|\?)#i', $value)) {
- return $value;
- }
- return '/' . preg_replace('#^(?:\./)+#', '', $value);
- };
-
- $html = preg_replace_callback(
- '/(\s(?:href|src|action|poster|formaction|data-src)\s*=\s*)(["\'])([^"\']*)\2/i',
- static fn(array $m): string => $m[1] . $m[2] . $fix($m[3]) . $m[2],
- $html
- ) ?? $html;
-
- $html = preg_replace_callback(
- '/(\ssrcset\s*=\s*)(["\'])([^"\']*)\2/i',
- static function (array $m) use ($fix): string {
- $parts = array_map(static function (string $candidate) use ($fix): string {
- $pieces = preg_split('/\s+/', trim($candidate), 2);
- $pieces[0] = $fix($pieces[0]);
- return implode(' ', $pieces);
- }, explode(',', $m[3]));
- return $m[1] . $m[2] . implode(', ', $parts) . $m[2];
- },
- $html
- ) ?? $html;
-
- return preg_replace_callback(
- '/url\(\s*(["\']?)([^)"\']*)\1\s*\)/i',
- static fn(array $m): string => 'url(' . $m[1] . $fix($m[2]) . $m[1] . ')',
- $html
- ) ?? $html;
- }
-
- function flatly_start_root_relative_buffer(): void
- {
- static $started = false;
- if ($started) {
- return;
- }
- $started = true;
- ob_start(static function (string $buffer): string {
- foreach (headers_list() as $header) {
- if (stripos($header, 'content-type:') === 0 && stripos($header, 'html') === false) {
- return $buffer;
- }
- }
- return flatly_root_relative_urls($buffer);
- });
- }
-
- function flatly_set_current_lang(?string $code): void
- {
- $GLOBALS['flatly_lang'] = $code;
- unset($GLOBALS['_products_cache']);
-
- if ($code !== null) {
- flatly_start_root_relative_buffer();
- }
- }
-
- function flatly_current_lang(): ?string
- {
- $code = $GLOBALS['flatly_lang'] ?? null;
- return is_string($code) && $code !== '' ? $code : null;
- }
-
- function flatly_init_front_lang(): bool
- {
- if (!isset($_GET['lang']) || !is_string($_GET['lang']) || $_GET['lang'] === '') {
- flatly_set_current_lang(null);
- return true;
- }
- $code = strtolower(trim($_GET['lang']));
- if ($code === flatly_default_lang() && isset(flatly_configured_languages()[$code])) {
- flatly_set_current_lang(null);
- flatly_start_root_relative_buffer();
- return true;
- }
- if (flatly_is_extra_lang($code)) {
- flatly_set_current_lang($code);
- return true;
- }
- flatly_set_current_lang(null);
- return false;
- }
-
- function flatly_lang_prefix_supported(): bool
- {
- static $supported = null;
- if ($supported === null) {
- $file = BASE_DIR . '/.htaccess';
- $supported = is_file($file) && strpos((string) @file_get_contents($file), FLATLY_LANG_HTACCESS_MARKER) !== false;
- }
- return $supported;
- }
-
- function flatly_lang_url(?string $lang, string $path = '/'): string
- {
- $path = '/' . ltrim($path, '/');
- if ($lang === null || $lang === '' || $lang === flatly_default_lang()) {
- return $path;
- }
- if (flatly_lang_prefix_supported()) {
- return '/' . $lang . ($path === '/' ? '/' : $path);
- }
- return $path . '?lang=' . rawurlencode($lang);
- }
-
- function flatly_set_page_ref(string $id): void
- {
- $GLOBALS['flatly_page_ref'] = $id;
- }
-
- function flatly_page_ref(): ?string
- {
- $ref = $GLOBALS['flatly_page_ref'] ?? null;
- return is_string($ref) && $ref !== '' ? $ref : null;
- }
-
- function flatly_lang_page_path(string $lang_code, string $ref): ?string
- {
- if ($ref === 'index') {
- return '/';
- }
- $default = flatly_default_lang();
- $data = $lang_code === $default ? load_page($ref) : load_page_lang($ref, $lang_code);
- if (!is_array($data) || empty($data['slug'])) {
- return null;
- }
- if (function_exists('is_page_published') && !is_page_published($data)) {
- return null;
- }
- return '/' . $data['slug'];
- }
-
- function flatly_language_alternates(): array
- {
- $ref = flatly_page_ref();
- $languages = flatly_configured_languages();
- $default = flatly_default_lang();
- if ($ref === null || !flatly_extra_languages()) {
- return [];
- }
- if (!isset($languages[$default])) {
- $languages = [$default => ''] + $languages;
- }
- $languages[$default] = flatly_default_language_name();
- $alternates = [];
- foreach ($languages as $code => $name) {
- $path = flatly_lang_page_path($code, $ref);
- $alternates[$code] = [
- 'name' => $name,
- 'path' => $path,
- 'url' => flatly_lang_url($code, $path ?? '/'),
- 'exact' => $path !== null,
- ];
- }
- return $alternates;
- }
-
- function flatly_hreflang_links(): string
- {
- $html = '';
- foreach (flatly_language_alternates() as $code => $alt) {
- if (!$alt['exact']) {
- continue;
- }
- $html .= '<link rel="alternate" hreflang="' . htmlspecialchars($code, ENT_QUOTES, 'UTF-8') . '" href="' . htmlspecialchars(SITE_URL . $alt['url'], ENT_QUOTES, 'UTF-8') . '">' . "\n ";
- if ($code === flatly_default_lang()) {
- $html .= '<link rel="alternate" hreflang="x-default" href="' . htmlspecialchars(SITE_URL . $alt['url'], ENT_QUOTES, 'UTF-8') . '">' . "\n ";
- }
- }
- return $html;
- }
-
- function flatly_canonical_link(): string
- {
- $uri = parse_url((string) ($_SERVER['REQUEST_URI'] ?? '/'));
- $path = (string) ($uri['path'] ?? '/');
- if (BASE_URL !== '' && str_starts_with($path, BASE_URL)) {
- $path = substr($path, strlen(BASE_URL));
- }
- $path = '/' . ltrim($path, '/');
-
- $query = '';
- parse_str((string) ($uri['query'] ?? ''), $params);
- if (isset($params['lang']) && is_string($params['lang']) && preg_match(FLATLY_LANG_CODE_PATTERN, $params['lang']) === 1) {
- $query = '?lang=' . rawurlencode($params['lang']);
- }
-
- return '<link rel="canonical" href="' . htmlspecialchars(SITE_URL . $path . $query, ENT_QUOTES, 'UTF-8') . '">' . "\n ";
- }
-
- function flatly_language_autodetect_enabled(): bool
- {
- return !empty(flatly_main_settings()['language_autodetect']);
- }
-
- function flatly_language_detect_script(): string
- {
- if (!flatly_language_autodetect_enabled() || flatly_current_lang() !== null || isset($_GET['lang'])) {
- return '';
- }
-
- $urls = [];
- foreach (flatly_language_alternates() as $code => $alt) {
- if ($alt['exact']) {
- $urls[$code] = $alt['url'];
- }
- }
- if (count($urls) < 2) {
- return '';
- }
-
- $config = json_encode(
- ['primary' => flatly_default_lang(), 'urls' => $urls],
- JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES
- );
-
- $script = <<<'JS'
- (function () {
- var cfg = __CONFIG__;
- try {
- if (window.self !== window.top) return;
- var stored = null;
- try { stored = localStorage.getItem('flatly-lang'); } catch (e) {}
- var target = null;
-
- if (stored) {
- target = cfg.urls[stored] ? stored : null;
- } else {
- if (document.referrer) {
- try { if (new URL(document.referrer).origin === location.origin) return; } catch (e) {}
- }
- var prefs = navigator.languages && navigator.languages.length ? navigator.languages : [navigator.language || ''];
- for (var i = 0; i < prefs.length && !target; i++) {
- var tag = String(prefs[i]).toLowerCase();
- if (cfg.urls[tag]) target = tag;
- else if (cfg.urls[tag.split('-')[0]]) target = tag.split('-')[0];
- }
- }
-
- if (!target || target === cfg.primary) return;
- location.replace((window.FLATLY_BASE || '') + cfg.urls[target]);
- } catch (e) {}
- })();
- JS;
-
- return '<script>' . str_replace('__CONFIG__', (string) $config, $script) . '</script>' . "\n ";
- }
-
- function flatly_home_url(): string
- {
- return flatly_lang_url(flatly_current_lang(), '/');
- }
-
- function render_language_switcher(): string
- {
- $alternates = flatly_language_alternates();
- if (count($alternates) < 2) {
- return '';
- }
- $current = flatly_current_lang() ?? flatly_default_lang();
-
- ob_start(); ?>
- <div class="footer-language" style="margin-left: 10px;">
- <select id="language-select" aria-label="Select language">
- <?php foreach ($alternates as $code => $alt): ?>
- <option value="<?= htmlspecialchars($alt['url'], ENT_QUOTES, 'UTF-8') ?>" lang="<?= htmlspecialchars($code, ENT_QUOTES, 'UTF-8') ?>" data-code="<?= htmlspecialchars($code, ENT_QUOTES, 'UTF-8') ?>" <?= $current === $code ? 'selected' : '' ?>>
- <?= htmlspecialchars($alt['name'], ENT_QUOTES, 'UTF-8') ?>
- </option>
- <?php endforeach; ?>
- </select>
- </div>
- <script>
- (function() {
- 'use strict';
- const languageSelect = document.getElementById('language-select');
- if (languageSelect) {
- languageSelect.addEventListener('change', function() {
- const selectedUrl = this.value;
- <?php if (flatly_language_autodetect_enabled()): ?>
- try { localStorage.setItem('flatly-lang', this.options[this.selectedIndex].getAttribute('data-code')); } catch (e) {}
- <?php endif; ?>
- if (selectedUrl && selectedUrl.startsWith('/')) {
- window.location.href = (window.FLATLY_BASE || '') + selectedUrl;
- }
- });
- }
- })();
- </script>
- <?php return ob_get_clean();
- }
-
- function flatly_translatable_setting_keys(): array
- {
- return ['site_name', 'site_description', 'logo_text', 'nav_links', 'nav_buttons', 'footer'];
- }
-
- function flatly_is_blank(mixed $value): bool
- {
- return $value === null || $value === '' || $value === [];
- }
-
- function flatly_merge_translation(array $base, array $translation): array
- {
- foreach (flatly_translatable_setting_keys() as $key) {
- if (!array_key_exists($key, $translation) || flatly_is_blank($translation[$key])) {
- continue;
- }
- if ($key === 'footer' && is_array($translation[$key]) && is_array($base[$key] ?? null)) {
- foreach ($translation[$key] as $sub => $value) {
- if (!flatly_is_blank($value)) {
- $base[$key][$sub] = $value;
- }
- }
- continue;
- }
- $base[$key] = $translation[$key];
- }
- return $base;
- }
-
- function flatly_block_keys(array $blocks): array
- {
- $keys = [];
- $counters = [];
- foreach ($blocks as $block) {
- $type = is_array($block) ? (string) ($block['type'] ?? '') : '';
- if (is_array($block) && !empty($block['id'])) {
- $keys[] = 'id:' . $block['id'];
- continue;
- }
- $counters[$type] = ($counters[$type] ?? 0) + 1;
- $keys[] = 'type:' . $type . ':' . $counters[$type];
- }
- return $keys;
- }
-
- function flatly_merge_blocks(array $main, array $translated): array
- {
- $result = array_values($translated);
- $result_keys = flatly_block_keys($result);
- $main = array_values($main);
- $main_keys = flatly_block_keys($main);
- $added = 0;
- $previous = null;
-
- foreach ($main as $i => $block) {
- $key = $main_keys[$i];
- if (in_array($key, $result_keys, true)) {
- $previous = $key;
- continue;
- }
- $position = 0;
- if ($previous !== null) {
- $found = array_search($previous, $result_keys, true);
- $position = $found === false ? count($result) : $found + 1;
- }
- array_splice($result, $position, 0, [$block]);
- array_splice($result_keys, $position, 0, [$key]);
- $previous = $key;
- $added++;
- }
-
- return [$result, $added];
- }
-
- function flatly_link_key(array $item, int $index, string $field = 'url'): string
- {
- $value = trim((string) ($item[$field] ?? ''));
- return ($value === '' || $value === '#') ? 'pos:' . $index : $field . ':' . $value;
- }
-
- function flatly_merge_link_list(array $main, array $translated, string $field = 'url'): array
- {
- $result = array_values($translated);
- $existing = [];
- foreach ($result as $i => $item) {
- $existing[flatly_link_key(is_array($item) ? $item : [], $i, $field)] = true;
- }
- $added = 0;
- foreach (array_values($main) as $i => $item) {
- if (!is_array($item)) {
- continue;
- }
- $key = flatly_link_key($item, $i, $field);
- if (isset($existing[$key])) {
- continue;
- }
- $result[] = $item;
- $existing[$key] = true;
- $added++;
- }
- return [$result, $added];
- }
-
- function flatly_sync_settings(array $main, array $translated): array
- {
- $added = 0;
-
- foreach (['site_name', 'site_description', 'logo_text'] as $key) {
- if (flatly_is_blank($translated[$key] ?? null) && !flatly_is_blank($main[$key] ?? null)) {
- $translated[$key] = $main[$key];
- $added++;
- }
- }
-
- foreach (['nav_links', 'nav_buttons'] as $key) {
- [$translated[$key], $count] = flatly_merge_link_list(
- is_array($main[$key] ?? null) ? $main[$key] : [],
- is_array($translated[$key] ?? null) ? $translated[$key] : []
- );
- $added += $count;
- }
-
- $main_footer = is_array($main['footer'] ?? null) ? $main['footer'] : [];
- $footer = is_array($translated['footer'] ?? null) ? $translated['footer'] : [];
-
- foreach (['brand_description', 'copyright'] as $key) {
- if (flatly_is_blank($footer[$key] ?? null) && !flatly_is_blank($main_footer[$key] ?? null)) {
- $footer[$key] = $main_footer[$key];
- $added++;
- }
- }
-
- $columns = is_array($footer['columns'] ?? null) ? array_values($footer['columns']) : [];
- $main_columns = is_array($main_footer['columns'] ?? null) ? array_values($main_footer['columns']) : [];
- for ($i = count($columns); $i < count($main_columns); $i++) {
- $columns[] = $main_columns[$i];
- $added++;
- }
- $footer['columns'] = $columns;
-
- [$footer['bottom_links'], $count] = flatly_merge_link_list(
- is_array($main_footer['bottom_links'] ?? null) ? $main_footer['bottom_links'] : [],
- is_array($footer['bottom_links'] ?? null) ? $footer['bottom_links'] : []
- );
- $added += $count;
-
- [$footer['social_links'], $count] = flatly_merge_link_list(
- is_array($main_footer['social_links'] ?? null) ? $main_footer['social_links'] : [],
- is_array($footer['social_links'] ?? null) ? $footer['social_links'] : [],
- 'platform'
- );
- $added += $count;
-
- $translated['footer'] = $footer;
- return [$translated, $added];
- }
-
- function flatly_sync_page_blocks(array $main_page, ?array $translated_page, string $file_id): array
- {
- if ($translated_page === null) {
- $page = $main_page;
- $page['id'] = $file_id;
- $page['updated_at'] = date('Y-m-d H:i:s');
- $count = count($main_page['blocks'] ?? []);
- return [$page, $count];
- }
- [$blocks, $added] = flatly_merge_blocks(
- is_array($main_page['blocks'] ?? null) ? $main_page['blocks'] : [],
- is_array($translated_page['blocks'] ?? null) ? $translated_page['blocks'] : []
- );
- if ($added > 0) {
- $translated_page['blocks'] = $blocks;
- $translated_page['updated_at'] = date('Y-m-d H:i:s');
- }
- return [$translated_page, $added];
- }
-