str_starts_with((string)$key, 'copilot.'))); sort($expectedKeys); if (!$expectedKeys) { fail('No copilot.* keys found in en.json.'); } $mainLangFiles = glob($langDir . '/??.json') ?: []; sort($mainLangFiles); foreach ($mainLangFiles as $file) { $data = loadLangJson($file); $keys = array_values(array_filter(array_keys($data), fn($key) => str_starts_with((string)$key, 'copilot.'))); sort($keys); $missing = array_values(array_diff($expectedKeys, $keys)); $extra = array_values(array_diff($keys, $expectedKeys)); if ($missing || $extra) { fail(basename($file) . ' copilot key mismatch. Missing: ' . implode(', ', $missing) . ' Extra: ' . implode(', ', $extra)); } foreach ($expectedKeys as $key) { if (!is_string($data[$key] ?? null) || trim((string)$data[$key]) === '') { fail(basename($file) . ' has empty value for ' . $key); } } } $js = (string)file_get_contents($root . '/js/ai-copilot.js'); preg_match_all("/label\\(\\s*['\"](copilot\\.[a-z0-9_]+)['\"]/", $js, $matches); $jsKeys = array_values(array_unique($matches[1] ?? [])); sort($jsKeys); $missingInEnglish = array_values(array_diff($jsKeys, $expectedKeys)); if ($missingInEnglish) { fail('Copilot JS references missing en.json keys: ' . implode(', ', $missingInEnglish)); } // ---- Agent texts (js/ai-agent.js) --------------------------------------------------------------------- $expectedAgent = array_values(array_filter(array_keys($english), fn($key) => str_starts_with((string)$key, 'agent.'))); sort($expectedAgent); if (!$expectedAgent) { fail('No agent.* keys found in en.json.'); } $placeholders = static function (string $text): array { preg_match_all('/\{[a-z]+\}/', $text, $found); $list = array_values(array_unique($found[0])); sort($list); return $list; }; foreach ($mainLangFiles as $file) { $data = loadLangJson($file); $keys = array_values(array_filter(array_keys($data), fn($key) => str_starts_with((string)$key, 'agent.'))); sort($keys); $missing = array_values(array_diff($expectedAgent, $keys)); $extra = array_values(array_diff($keys, $expectedAgent)); if ($missing || $extra) { fail(basename($file) . ' agent key mismatch. Missing: ' . implode(', ', $missing) . ' Extra: ' . implode(', ', $extra)); } foreach ($expectedAgent as $key) { if (!is_string($data[$key] ?? null) || trim((string)$data[$key]) === '') { fail(basename($file) . ' has empty value for ' . $key); } if ($placeholders($data[$key]) !== $placeholders($english[$key])) { fail(basename($file) . ' changes the placeholders of ' . $key . ': ' . $data[$key]); } } } $german = loadLangJson($langDir . '/de.json'); foreach ($expectedAgent as $key) { $text = (string)$german[$key]; if (preg_match('/\b(Sie|Ihr|Ihre|Ihren|Ihrem|Ihrer|Ihnen)\b/u', $text)) fail('German agent text must use "du": ' . $key . ' = ' . $text); if (preg_match('/\p{L}[:*_]innen|\p{L}[:*_]in\b|\p{Ll}Innen\b/u', $text)) fail('German agent text must not use gender special characters: ' . $key . ' = ' . $text); } $agentJs = (string)file_get_contents($root . '/js/ai-agent.js') . (string)file_get_contents($root . '/includes/ai/agent/preview.php') . (string)file_get_contents($root . '/includes/ai/agent/changeset.php') . (string)file_get_contents($root . '/includes/ai/agent/changeset-apply.php'); // the preview bar and the review texts are built on the server preg_match_all("/['\"](agent\.[a-z0-9_.]+)['\"]/", $agentJs, $agentMatches); $agentJsKeys = array_values(array_unique(array_filter($agentMatches[1] ?? [], fn($key) => !in_array($key, ['agent.step.', 'agent.field.', 'agent.safety.', 'agent.safety_refuse.'], true)))); sort($agentJsKeys); $unknownAgentKeys = array_values(array_diff($agentJsKeys, $expectedAgent)); if ($unknownAgentKeys) { fail('Agent JS references missing en.json keys: ' . implode(', ', $unknownAgentKeys)); } $unusedAgentKeys = array_values(array_filter(array_diff($expectedAgent, $agentJsKeys), fn($key) => !str_starts_with($key, 'agent.step.') && !str_starts_with($key, 'agent.field.') && !str_starts_with($key, 'agent.safety.') && !str_starts_with($key, 'agent.safety_refuse.'))); if ($unusedAgentKeys) { fail('Agent texts that no code uses: ' . implode(', ', $unusedAgentKeys)); } // The safety check builds its texts from codes (agent.safety.{code}, agent.safety_refuse.{category}): every text belongs to a code in safety.php and every code has a text. $safetySource = (string)file_get_contents($root . '/includes/ai/agent/safety.php'); preg_match_all("/\\\$add\\('([a-z_]+)'/", $safetySource, $safetyCodes); $safetyCodes = array_values(array_unique($safetyCodes[1] ?? [])); if (count($safetyCodes) < 8) fail('Could not read the safety codes from safety.php (' . count($safetyCodes) . ').'); foreach ($safetyCodes as $code) { if (!isset($english['agent.safety.' . $code])) fail('No text for the safety code ' . $code); } foreach ($expectedAgent as $key) { if (str_starts_with($key, 'agent.safety.') && !in_array(substr($key, strlen('agent.safety.')), $safetyCodes, true)) fail('Safety text without a code in safety.php: ' . $key); } foreach (['mass_delete', 'hate', 'create_crime'] as $category) { if (!isset($english['agent.safety_refuse.' . $category]) || !str_contains($safetySource, "'" . $category . "'")) fail('Refusal text or category missing: ' . $category); } // Every tool has a timeline text; unknown tools fall back to agent.step.unknown. $toolSource = (string)file_get_contents($root . '/includes/ai/agent/tools.php'); preg_match_all("/^ '([a-z_]+)' => \['description'/m", $toolSource, $toolMatches); $toolNames = $toolMatches[1] ?? []; if (count($toolNames) < 25) { fail('Could not read the tool names from tools.php (' . count($toolNames) . ').'); } foreach ($toolNames as $tool) { if (!isset($english['agent.step.' . $tool])) fail('No timeline text for tool ' . $tool); if (!str_contains($agentJs, "'" . $tool . "'")) fail('js/ai-agent.js does not list tool ' . $tool . ' in TOOL_STEPS.'); } // ---- Dashboard actions: every text the server and the dashboard script use exists, in every language --------------------------- $dashboardActionSources = (string)file_get_contents($root . '/includes/ai/raiju-map.php') . (string)file_get_contents($root . '/includes/ai/dashboard-actions.php') . (string)file_get_contents($root . '/admin/dashboard/scripts/settings.php') . (string)file_get_contents($root . '/admin/api/ai.php'); preg_match_all("/['\"](copilot\.dash_[a-z_A-Z]+)['\"]/", $dashboardActionSources, $dashMatches); $dashKeys = array_values(array_filter(array_unique($dashMatches[1] ?? []), fn($key) => !in_array($key, ['copilot.dash_setting_', 'copilot.dash_value_'], true))); foreach (['adminTheme', 'primaryColor', 'accentColor', 'adminLanguage'] as $setting) $dashKeys[] = 'copilot.dash_setting_' . $setting; // built from the setting name foreach (['light', 'dark', 'system'] as $value) $dashKeys[] = 'copilot.dash_value_' . $value; if (count($dashKeys) < 20) fail('Could not read the dashboard action texts (' . count($dashKeys) . ').'); foreach ($mainLangFiles as $file) { $strings = json_decode((string)file_get_contents($file), true); foreach ($dashKeys as $key) { if (!isset($strings[$key]) || trim((string)$strings[$key]) === '') fail('Dashboard action text ' . $key . ' is missing in ' . basename($file)); } } // ---- Agent settings and system status in the dashboard ---------------------------------------------------------------- $dashboardKeys = array_values(array_filter(array_keys($english), fn($key) => str_starts_with((string)$key, 'ai.agent_') || str_starts_with((string)$key, 'ai.limit') || str_starts_with((string)$key, 'ai.models_') || str_starts_with((string)$key, 'system.agent') || in_array($key, ['system.configured', 'system.not_configured'], true))); sort($dashboardKeys); if (count($dashboardKeys) < 50) fail('The agent settings texts are missing from en.json.'); foreach ($mainLangFiles as $file) { $data = loadLangJson($file); foreach ($dashboardKeys as $key) { if (!is_string($data[$key] ?? null) || trim((string)$data[$key]) === '') fail(basename($file) . ' lacks ' . $key); if ($placeholders($data[$key]) !== $placeholders($english[$key])) fail(basename($file) . ' changes the placeholders of ' . $key); } } foreach ($dashboardKeys as $key) { $text = (string)$german[$key]; if (preg_match('/\b(Sie|Ihr|Ihre|Ihren|Ihrem|Ihrer|Ihnen)\b/u', $text)) fail('German settings text must use "du": ' . $key . ' = ' . $text); if (preg_match('/\p{L}[:*_]innen|\p{L}[:*_]in\b|\p{Ll}Innen\b/u', $text)) fail('German settings text must not use gender special characters: ' . $key . ' = ' . $text); } $dashboardSources = (string)file_get_contents($root . '/js/ai-copilot.js') . (string)file_get_contents($root . '/admin/dashboard.php') . (string)file_get_contents($root . '/admin/dashboard/scripts/overview-ai.php') . (string)file_get_contents($root . '/admin/dashboard/scripts/system.php'); preg_match_all("/['\"]((?:ai\.agent_|ai\.limit|ai\.models_|system\.agent)[a-zA-Z_]*|system\.configured|system\.not_configured)['\"]/", $dashboardSources, $usedMatches); $usedDashboardKeys = array_values(array_unique(array_filter($usedMatches[1] ?? [], fn($key) => !in_array($key, ['ai.agent_right_', 'ai.limit_kind_'], true)))); // the rights are looped over below $unknownDashboardKeys = array_values(array_diff($usedDashboardKeys, $dashboardKeys)); if ($unknownDashboardKeys) fail('The dashboard uses texts that are missing: ' . implode(', ', $unknownDashboardKeys)); foreach (['publish', 'stockImages', 'uploadImages', 'generateImages', 'research', 'structural', 'createPages', 'createNews', 'createEvents', 'editSharedContent'] as $right) { if (!isset($english['ai.agent_right_' . $right])) fail('No settings text for the right ' . $right); } foreach (['month', 'requests', 'text', 'images'] as $kind) { if (!isset($english['ai.limit_kind_' . $kind])) fail('No text for the limit kind ' . $kind); } $unusedDashboardKeys = array_values(array_filter(array_diff($dashboardKeys, $usedDashboardKeys), fn($key) => !str_starts_with($key, 'ai.agent_right_') && !str_starts_with($key, 'ai.limit_kind_'))); if ($unusedDashboardKeys) fail('Settings texts that nothing uses: ' . implode(', ', $unusedDashboardKeys)); echo json_encode([ 'ok' => true, 'languages' => array_map('basename', $mainLangFiles), 'copilotKeys' => count($expectedKeys), 'jsKeys' => count($jsKeys), 'agentKeys' => count($expectedAgent), 'agentJsKeys' => count($agentJsKeys), 'dashboardKeys' => count($dashboardKeys) ], JSON_UNESCAPED_SLASHES) . PHP_EOL;