self::mode(), 'language' => self::current(), 'automatic' => self::resolve(get_user_locale()), 'siteLanguage' => self::resolve(self::site_locale()), 'hasPreference' => self::valid_mode($stored), ]; } /** Per user and per site; no change to WordPress, another user, or page content. */ public static function save($mode, bool $migrate = false) { if (!get_current_user_id() || !PostTypes::can_edit()) { return new \WP_Error('digibl_language_forbidden', self::text('Access denied.'), ['status' => 403]); } if (!self::valid_mode($mode)) { return new \WP_Error('digibl_language_invalid', self::text('Choose a supported interface language.'), ['status' => 400]); } $lockKey = 'digibl_language_lock_' . get_current_user_id(); $lock = \Digiter\Diagnostics\ProtocolV1::lock($lockKey, 30); if (!$lock) { return new \WP_Error('digibl_language_busy', self::text('A language change is already being saved.'), ['status' => 409]); } try { // Refresh after taking the lock: another browser may have saved a choice. wp_cache_delete(get_current_user_id(), 'user_meta'); if ($migrate && self::state()['hasPreference']) { return self::state(); } if (!update_user_option(get_current_user_id(), self::OPTION, $mode, false) && get_user_option(self::OPTION) !== $mode) { return new \WP_Error('digibl_language_storage', self::text('Language could not be saved. Please try again.'), ['status' => 500]); } return self::state(); } finally { \Digiter\Diagnostics\ProtocolV1::unlock($lockKey, $lock); } } public static function routes(): void { register_rest_route('digiter-builder/v1', '/interface-language', [ 'methods' => 'POST', 'permission_callback' => static fn() => get_current_user_id() && PostTypes::can_edit(), 'callback' => static function ($request) { $result = self::save($request->get_param('mode'), $request->get_param('migrate') === true); if (is_wp_error($result)) { return $result; } $response = new \WP_REST_Response($result); $response->header('Cache-Control', 'no-store, private'); return $response; }, ]); } public static function menu(): void { add_submenu_page('digiter-builder', self::text('Interface language'), self::text('Interface language'), PostTypes::menu_capability(), 'digibl-interface-language', [self::class, 'screen']); } public static function screen(): void { if (!PostTypes::can_edit()) { wp_die(esc_html(self::text('Access denied.'))); } echo '

' . esc_html(self::text('Interface language')) . '

'; if (isset($_GET['language_saved'])) { echo '

' . esc_html(self::text('Interface language saved.')) . '

'; } echo '

' . esc_html(self::text('This choice is saved to your account on this site and applies on every device.')) . '

'; echo '
'; wp_nonce_field('digibl_interface_language'); echo '

' . esc_html(self::text('Automatic follows your WordPress profile language, or the site language when your profile uses the site default.')) . '

'; echo '

' . esc_html(self::text('Follow site language uses the site setting regardless of your profile language.')) . '

'; echo '

' . esc_html(self::text('Page content and field values are not translated.')) . '

'; submit_button(self::text('Save language')); echo '
'; } public static function save_form(): void { if (!PostTypes::can_edit()) { wp_die(esc_html(self::text('Access denied.')), '', ['response' => 403]); } check_admin_referer('digibl_interface_language'); $result = self::save(isset($_POST['mode']) && is_string($_POST['mode']) ? wp_unslash($_POST['mode']) : null); if (is_wp_error($result)) { wp_die(esc_html($result->get_error_message()), '', ['response' => (int) $result->get_error_data()['status']]); } wp_safe_redirect(add_query_arg('language_saved', '1', admin_url('admin.php?page=digibl-interface-language'))); exit; } public static function catalog(string $language): array { $language = self::resolve($language); if (!isset(self::$catalogs[$language])) { $path = DIGIBL_PATH . 'languages/' . $language . '.json'; $value = is_readable($path) ? json_decode(file_get_contents($path), true) : []; self::$catalogs[$language] = is_array($value) ? $value : []; } return self::$catalogs[$language]; } public static function text(string $message): string { $language = self::current(); $own = self::catalog($language); if (isset($own[$message]) && is_string($own[$message]) && $own[$message] !== '') { return $own[$message]; } return self::catalog('en')[$message] ?? $message; } public static function gettext(string $translation, string $message, string $domain): string { return self::text($message); } /** Only Builder error messages, never successful data or authored documents. */ public static function translate_error($response, $server, $request) { if (!str_starts_with($request->get_route(), '/digiter-builder/v1/') || $response->get_status() < 400) { return $response; } $data = $response->get_data(); if (is_array($data) && isset($data['message']) && is_string($data['message'])) { $data['message'] = self::text($data['message']); $response->set_data($data); } return $response; } public static function wordpress_locale(): string { $language = self::current(); if ($language === 'en') { return 'en_US'; } foreach (array_unique(array_merge([get_user_locale(), self::site_locale()], get_available_languages())) as $locale) { if (self::resolve($locale) === $language) { return $locale; } } return 'en_US'; } public static function enqueue(): void { wp_enqueue_script('digibl-i18n', DIGIBL_URL . 'assets/i18n.js', [], DIGIBL_VERSION, true); wp_enqueue_script('digibl-language', DIGIBL_URL . 'assets/language.js', ['digibl-i18n'], DIGIBL_VERSION, true); $state = self::state(); wp_add_inline_script('digibl-language', 'window.DIGIBL_LANGUAGE_CONFIG=' . wp_json_encode($state + [ 'policy' => self::policy(), 'catalog' => self::catalog($state['language']), 'baseUrl' => DIGIBL_URL . 'languages/', 'version' => DIGIBL_VERSION, 'endpoint' => rest_url('digiter-builder/v1/interface-language'), 'nonce' => wp_create_nonce('wp_rest'), 'storageKey' => 'dbs-language-v2-' . get_current_blog_id() . '-' . get_current_user_id(), ], JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . ';', 'before'); } }