'af_ZA', 'ar' => 'ar_AR', 'az' => 'az_AZ', 'bg' => 'bg_BG', 'bn' => 'bn_BD', 'cs' => 'cs_CZ', 'da' => 'da_DK', 'de' => 'de_DE', 'el' => 'el_GR', 'en' => 'en_US', 'es' => 'es_ES', 'et' => 'am_ET', 'fa' => 'fa_IR', 'fi' => 'fi_FI', 'fr' => 'fr_FR', 'ga' => 'ga_IE', 'gu' => 'gu_IN', 'he' => 'he_IL', 'hi' => 'hi_IN', 'hr' => 'hr_HR', 'ht' => 'ht_HT', 'hu' => 'hu_HU', 'hy' => 'hy_AM', 'id' => 'id_ID', 'is' => 'is_IS', 'it' => 'it_IT', 'iu' => 'iu_CA', 'ja' => 'ja_JP', 'km' => 'km_KH', 'kn' => 'kn_IN', 'ko' => 'ko_KR', 'ku' => 'ku_TR', 'lo' => 'lo_LA', 'lt' => 'lt_LT', 'lv' => 'lv_LV', 'mg' => 'mg_MG', 'mi' => 'mi_NZ', 'ml' => 'ml_IN', 'mr' => 'mr_IN', 'ms' => 'ms_MY', 'mt' => 'mt_MT', 'my' => 'my_MM', 'ne' => 'ne_NP', 'nl' => 'nl_NL', 'no' => 'nb_NO', 'or' => 'or_IN', 'pa' => 'pa_IN', 'pl' => 'pl_PL', 'ps' => 'ps_AF', 'pt' => 'pt_PT', 'ro' => 'ro_RO', 'ru' => 'ru_RU', 'sk' => 'sk_SK', 'sl' => 'sl_SI', 'sm' => 'sm_WS', 'sq' => 'sq_AL', 'sv' => 'sv_SE', 'sw' => 'sw_KE', 'ta' => 'ta_IN', 'te' => 'te_IN', 'th' => 'th_TH', 'to' => 'to_TO', 'tr' => 'tr_TR', 'uk' => 'uk_UA', 'ur' => 'ur_PK', 'vi' => 'vi_VN', 'zh-Hans' => 'zh_CN', 'zh-Hant' => 'zh_TW', ); /** 自定义样式的长度上限(字节)。 */ const MAX_SWITCHER_CSS = 65536; /** * 语言元数据静态表缓存。 * * @var array|null */ private static $languages = null; /** * 旗帜图标表缓存。 * * @var array|null */ private static $flags = null; /** * 读取语言旗帜图标(iso => data URI)。 * * 单独一份文件、按需加载:几十 KB 的 base64 没必要跟着每个请求走, * 只有前台真要显示图标时才读。 * * @return array */ public static function flags() { if ( null === self::$flags ) { $file = APGT_PLUGIN_DIR . 'includes/flags.php'; self::$flags = is_readable( $file ) ? (array) require $file : array(); } return self::$flags; } /** * 取某个语言的旗帜图标。 * * @param string $iso 语言代码。 * @return string data URI;没有就返回空串。 */ public static function flag( $iso ) { $flags = self::flags(); return isset( $flags[ $iso ] ) ? (string) $flags[ $iso ] : ''; } /** * 读取语言元数据静态表(iso => 信息)。 * * @return array */ public static function language_meta() { if ( null === self::$languages ) { self::$languages = require APGT_PLUGIN_DIR . 'includes/languages.php'; } return self::$languages; } /** * 语言与基本配置默认值。 * * @return array */ public static function default_settings() { return array( 'default_language' => '', // 站点源语言 iso,如 zh-Hans 'is_trans_url' => 0, // 是否连站内链接的路径一起翻译 'languages' => array(), // iso => 目标语言配置,见 default_language_row() 'auto_worker' => 0, // 管理员登录后台时自动翻译待办页面 'worker_interval' => 60, // worker 心跳间隔(秒) 'batch_concurrency' => 2, // 「全部翻译」同时翻几个页面(1–5) 'realtime_trans' => 0, // 访客访问译文页面时实时补翻 'realtime_ratio' => 100, // 译文占比达到多少 % 才输出译文页面 'fetch_ip' => '', // 抓取原页面时把站点域名固定解析到的 IP,留空走系统 DNS 'menu_container' => '', // 语言菜单挂载到哪个元素 id,留空则浮动显示 'show_switcher' => 1, // 是否输出语言切换菜单 'show_hreflang' => 1, // 是否输出 hreflang 'show_flag' => 1, // 语言菜单里是否显示旗帜小图标 'switcher_style' => 'default', // 切换器样式:default 插件默认 / custom 自定义 CSS / none 不加载 ); } /** * 单个目标语言配置行的默认结构。 * * @param string $iso 语言代码。 * @return array */ public static function default_language_row( $iso = '' ) { $meta = self::language_meta(); return array( 'iso' => $iso, 'dir' => $iso, // URL 路径前缀 /en/ 'hreflang_name' => isset( $meta[ $iso ]['hreflang_name'] ) ? $meta[ $iso ]['hreflang_name'] : $iso, 'hreflang_area' => isset( $meta[ $iso ]['hreflang_area'] ) ? $meta[ $iso ]['hreflang_area'] : '', 'sort' => 0, 'mode' => '0', // 0所有用户 1仅搜索引擎 2仅普通用户 'text_replace' => array(), // [ {search, replace}, ... ] ); } /** * AI 服务配置默认值。 * * @return array */ public static function default_ai() { return array( // 由谁去请求 AI:'' 自动(站点级 AI 配置可用就用它,否则用下面这些字段)、 // 'core' 只用 WordPress 自带的 AI Client、'plugin' 只用下面这些字段、 // 'service' 用订阅服务(填一个访问密钥即可,不必自备 AI 平台账号)。 'provider' => '', // ── 订阅服务(Aipogeo)───────────────────────────────────── // 密钥在服务站点的会员中心「我的订阅」生成,只显示一次; // 插件只存这一个值,不保存任何 AI 平台的凭据。 'service_url' => 'https://translate.aipogeo.com', 'license_key' => '', 'license_site' => '', // 激活时登记的站点地址(服务端归一化后的值) 'license_checked' => 0, // 上次校验成功的时间戳 'license_snapshot' => array(), // 最近一次的订阅信息(额度/到期日),仅用于设置页展示 'platform' => '', // openai / claude / gemini / deepseek(由 model 自动解析,此处仅记录) 'model' => 'deepseek-chat', 'protocol' => '', // 协议格式,留空表示由模型名自动识别 'api_url' => '', // 接口地址,留空表示用 php-ai 库里该模型(或所选协议)的内置地址 'api_key' => '', 'proxy' => '', // socks5://host:port 或 http://host:port // 单次输出上限,不是上下文长度。各模型的上限差得很远(Claude 3 Opus 4K、 // GPT-4o 16K、DeepSeek 384K),填死一个值换模型就会被平台拒绝, // 所以默认 0=不传该参数,跟随所选模型在库里的内置值。 'max_tokens' => 0, 'timeout' => 300, ); } /** * 默认 AI 翻译指令。 * * 取自 website-dev「多语言翻译」默认指令(prompt_ask + prompt_output 合并), * 保留三个固定变量 {{language_from}} {{language_to}} {{translate_data}}。 * * @return string */ public static function default_prompt() { return <<<'PROMPT' You are a professional multilingual translator for SEO websites. Requirements: 1. The translation must read naturally, the way a native speaker of the target language would put it 2. Keep the original meaning; do not translate word for word 3. Polish the wording where that helps, so it works well for search engines (SEO friendly) 4. Keep the JSON structure unchanged. The JSON keys are the numbers I use to identify each source string — do not modify them 5. Return JSON only Notes: + A `%s` inside a string is a placeholder for the program's string formatting + Some source strings hold only part of a sentence, because the HTML text node they came from ends there. Do not expand them on your own, or the meaning will break once I put them back into the HTML + If a source string is a URL or a URI, keep its URI path information when translating Source language: {{language_from}} Target language: {{language_to}} Input: ```json {{translate_data}} ``` When returning the data, do not change the keys of the original data; output only the translated content for each original key. Output format: { "source number":"translated content" } PROMPT; } /** * 默认 URL 路径翻译指令。 * * 网址路径和正文的要求完全不同(要保持斜杠层级、不能带空格、扩展名与查询串 * 原样留着),混在正文指令里说不清楚,所以单独一条:开启「翻译 URL 路径」时, * 路径条目自成一批用这条指令送翻,正文指令不必写 URL 相关的要求。 * * 变量与正文指令一致:{{language_from}} {{language_to}} {{translate_data}}。 * * @return string */ public static function default_url_prompt() { return <<<'PROMPT' You are a professional multilingual translator for SEO websites. What you are translating here are the site's URL paths. Requirements: 1. Every word in the path that carries meaning must be translated into the target language. It makes no difference whether that segment was written in English, in pinyin or in some other language — translate it. Do not copy a segment back unchanged just because it looks like a common word (contact, about, news and the like) 2. Write it in the target language's own script; do not transliterate. If the target language is Japanese, write Japanese (kanji/kana); if it is Russian, write Cyrillic; if it is English, write English words — `/关于我们/` becomes `/about-us/` in English and `/私たちについて/` in Japanese, never a romanization such as `/watashitachi-nitsuite/` 3. Follow the writing conventions of the target language: languages written in the Latin alphabet (English, French, Vietnamese and so on) go all lowercase with words joined by a hyphen `-`; languages not written in the Latin alphabet (Japanese, Korean, Chinese, Russian and so on) are written the way that language normally writes, with no hyphens between words. No language may contain spaces 4. Keep the path structure unchanged: the leading `/` must stay, a trailing `/` must be present or absent exactly as in the original, and the number of segments must not change 5. Only these are kept exactly as they are: plain numbers, dates, version numbers, random strings and identifiers, all-caps abbreviations and product model numbers (WMS, SEO, API, X100 and so on), brand names, file extensions (.html/.php and so on), query strings (`?a=b`) and anchors (`#name`) 6. Check every entry and leave none untranslated: apply the rules above to each one before returning it, and do not copy an entry back unchanged just because it looks familiar 7. Keep the JSON structure unchanged. The JSON keys are the numbers I use to identify each source string — do not modify them 8. Return JSON only Notes: + What you return must be a path: no domain, no `http://` and no `https://` + The path is read by search engines, so use the wording that is most common in the target language rather than a word-for-word rendering + If no perfect wording comes to mind, still express the meaning in the target language; never fall back to copying the original as the translation Source language: {{language_from}} Target language: {{language_to}} Input: ```json {{translate_data}} ``` When returning the data, do not change the keys of the original data; output only the translated path for each original key. Output format: { "source number":"/translated/path/" } PROMPT; } /** * 读取语言与基本配置。 * * @return array */ public static function get_settings() { $saved = get_option( self::OPT_SETTINGS ); if ( ! is_array( $saved ) ) { $saved = array(); } return array_merge( self::default_settings(), $saved ); } /** * 保存语言与基本配置。 * * @param array $settings 配置数组。 * @return void */ public static function save_settings( array $settings ) { update_option( self::OPT_SETTINGS, $settings ); } /** * 读取 AI 服务配置。 * * @return array */ public static function get_ai() { $saved = get_option( self::OPT_AI ); if ( ! is_array( $saved ) ) { $saved = array(); } return array_merge( self::default_ai(), $saved ); } /** * 保存 AI 服务配置。 * * @param array $ai AI 配置数组。 * @return void */ public static function save_ai( array $ai ) { update_option( self::OPT_AI, $ai ); } /** * 读取 AI 翻译指令(未设置时返回默认)。 * * @return string */ public static function get_prompt() { $saved = get_option( self::OPT_PROMPT ); if ( ! is_string( $saved ) || '' === trim( $saved ) ) { return self::default_prompt(); } return $saved; } /** * 保存 AI 翻译指令。 * * @param string $prompt 指令文本。 * @return void */ public static function save_prompt( $prompt ) { update_option( self::OPT_PROMPT, (string) $prompt ); } /** * 读取 URL 路径翻译指令(未设置时返回默认)。 * * @return string */ public static function get_url_prompt() { $saved = get_option( self::OPT_URL_PROMPT ); if ( ! is_string( $saved ) || '' === trim( $saved ) ) { return self::default_url_prompt(); } return $saved; } /** * 保存 URL 路径翻译指令。 * * @param string $prompt 指令文本。 * @return void */ public static function save_url_prompt( $prompt ) { update_option( self::OPT_URL_PROMPT, (string) $prompt ); } /** * 取站点源语言 iso。 * * @return string */ public static function default_language() { $s = self::get_settings(); return (string) $s['default_language']; } /** * 取已配置的目标语言(不含源语言),按 sort 升序。 * * @return array iso => 语言配置行 */ public static function target_languages() { $s = self::get_settings(); $default = $s['default_language']; $langs = is_array( $s['languages'] ) ? $s['languages'] : array(); // 过滤掉与源语言相同的项。 $targets = array(); foreach ( $langs as $iso => $row ) { if ( '' === $iso || $iso === $default ) { continue; } $targets[ $iso ] = $row; } uasort( $targets, function ( $a, $b ) { return ( (int) $a['sort'] ) <=> ( (int) $b['sort'] ); } ); return $targets; } /** * 取语言目录(URL 前缀)到 iso 的映射。 * * @return array dir => iso */ public static function dir_to_iso_map() { $map = array(); foreach ( self::target_languages() as $iso => $row ) { $dir = isset( $row['dir'] ) && '' !== $row['dir'] ? $row['dir'] : $iso; $map[ $dir ] = $iso; } return $map; } /** * 切换器样式来源,非法值一律按默认处理。 * * @return string default|custom|none */ public static function switcher_style() { $s = self::get_settings(); $style = (string) $s['switcher_style']; return in_array( $style, self::SWITCHER_STYLES, true ) ? $style : 'default'; } /** * 默认样式表的路径。 * * 写死在代码里,不接受任何外部输入:读默认样式的地方只有这一个文件。 * * @return string */ private static function default_switcher_css_file() { return dirname( __DIR__ ) . '/assets/css/language-switch.css'; } /** * 读取插件自带的默认切换器样式,供编辑框预填与「恢复默认样式」使用。 * * @return string */ public static function default_switcher_css() { $file = self::default_switcher_css_file(); if ( ! is_readable( $file ) ) { return ''; } // 本地文件,路径见 default_switcher_css_file()。 $css = file_get_contents( $file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents return is_string( $css ) ? str_replace( "\r\n", "\n", $css ) : ''; } /** * 读取用户保存的自定义样式。 * * @return string */ public static function get_switcher_css() { $css = get_option( self::OPT_SWITCHER_CSS, '' ); return is_string( $css ) ? $css : ''; } /** * 保存自定义样式。调用方须先用 switcher_css_error() 校验过。 * * 内容有变化时顺带记下当时默认样式表的 md5;以后插件更新改了默认样式, * 设置页据此提示用户自己那份是在旧版基础上改的。 * * @param string $css CSS 文本。 * @return void */ public static function save_switcher_css( $css ) { $css = (string) $css; if ( $css === self::get_switcher_css() ) { return; } // 前台每个页面都要读,跟着 autoload 一起取出来,省一次查询。 update_option( self::OPT_SWITCHER_CSS, $css, true ); update_option( self::OPT_SWITCHER_CSS_BASE, '' === $css ? '' : md5( self::default_switcher_css() ), false ); } /** * 自定义样式是否基于旧版的默认样式改出来的。 * * @return bool */ public static function switcher_css_outdated() { if ( '' === trim( self::get_switcher_css() ) ) { return false; } $base = (string) get_option( self::OPT_SWITCHER_CSS_BASE, '' ); return '' !== $base && md5( self::default_switcher_css() ) !== $base; } /** * 校验一段自定义 CSS 能不能用。 * * 与核心「额外 CSS」同一条规则:合法的 CSS 用不着 <, * 没有 < 就写不出 从样式标签里逃出去。 * * @param string $css CSS 文本。 * @return string 空串表示可用;否则为错误代码 markup|too_long。 */ public static function switcher_css_error( $css ) { $css = (string) $css; if ( false !== strpos( $css, '<' ) ) { return 'markup'; } if ( strlen( $css ) > self::MAX_SWITCHER_CSS ) { return 'too_long'; } return ''; } /** * 某个语言是不是从右往左书写。 * * @param string $iso 语言代码。 * @return bool */ public static function is_rtl( $iso ) { $base = strtolower( strtok( (string) $iso, '-_' ) ); return in_array( $base, self::RTL_LANGUAGES, true ); } /** * 某个语言对外使用的语言标签(hreflang、 共用)。 * * 优先取语言管理里填的 hreflang 名称,其次取语言表,最后退回 iso。 * * @param string $iso 语言代码。 * @return string */ public static function hreflang_code( $iso ) { $iso = (string) $iso; $targets = self::target_languages(); if ( ! empty( $targets[ $iso ]['hreflang_name'] ) ) { return (string) $targets[ $iso ]['hreflang_name']; } $meta = self::language_meta(); if ( ! empty( $meta[ $iso ]['hreflang_name'] ) ) { return (string) $meta[ $iso ]['hreflang_name']; } return $iso; } /** * 某个语言的 Open Graph 区域设置,如 es_ES。 * * 语言管理里的 hreflang 名称带了地区(es-MX)就照它来,否则查表。 * * @param string $iso 语言代码。 * @return string 查不到时返回空串。 */ public static function og_locale( $iso ) { $code = self::hreflang_code( $iso ); if ( preg_match( '/^([a-z]{2,3})[-_]([A-Za-z]{2})$/', $code, $m ) ) { return strtolower( $m[1] ) . '_' . strtoupper( $m[2] ); } return isset( self::OG_LOCALES[ $iso ] ) ? self::OG_LOCALES[ $iso ] : ''; } }