rtrim((string)dbnToolsEnv('DBN_AZURE_OPENAI_ENDPOINT', ''), '/'), 'api_key' => (string)dbnToolsEnv('DBN_AZURE_OPENAI_API_KEY', ''), 'api_version' => (string)dbnToolsEnv('DBN_AZURE_OPENAI_API_VERSION', '2024-02-01'), 'chat_deployment' => (string)dbnToolsEnv('DBN_AZURE_OPENAI_CHAT_DEPLOYMENT', 'gpt-4o-mini'), ]; function azureChat(array $config, array $messages, array $options = []): array { $url = $config['endpoint'] . '/openai/deployments/' . rawurlencode($config['chat_deployment']) . '/chat/completions?api-version=' . rawurlencode($config['api_version']); $payload = json_encode(array_filter([ 'messages' => $messages, 'temperature' => $options['temperature'] ?? 0.1, 'max_tokens' => $options['max_tokens'] ?? 4096, 'response_format' => isset($options['json']) && $options['json'] ? ['type' => 'json_object'] : null, ], fn($v) => $v !== null), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'api-key: ' . $config['api_key'], ], CURLOPT_TIMEOUT => (int)($options['timeout'] ?? 120), CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false, ]); $body = curl_exec($ch); $code = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE); $err = curl_error($ch); curl_close($ch); if ($body === false) throw new RuntimeException("Azure curl failed: $err"); $decoded = json_decode($body, true); if (!is_array($decoded)) throw new RuntimeException("Azure returned non-JSON (HTTP $code)"); if ($code < 200 || $code >= 300) { throw new RuntimeException('Azure error: ' . ($decoded['error']['message'] ?? "HTTP $code")); } return $decoded; } $languages = [ 'no' => 'Norwegian (Norsk bokmål)', 'uk' => 'Ukrainian', 'pl' => 'Polish', ]; $systemPrompt = <<<'PROMPT' You are a professional legal translator specializing in Norwegian family law and child welfare. Translate all JSON string values to {LANG}. PRESERVE AS-IS: - Norwegian institution names: Barnevernet, Statsforvalteren, Bufdir, Fylkesnemnda, NAV, Tingretten - Norwegian legal act names: forvaltningsloven, barnevernsloven, opplæringslova, EMK, fvl - Norwegian legal terms: klage, vedtak, saksnummer, akutt, omsorgsovertakelse, tiltaksplan - Technical terms: MCP, DBN, dbn.tool_name, JSON, Bearer token, npx, stdio - Brand names: Do Better Norge, Claude, Cursor, VS Code, Windsurf, Copilot - Code snippets, URLs, env var names like DBN_MCP_TOKEN - HTML entities and tags exactly as they appear - The symbol ← and → Return a JSON object with EXACTLY the same keys as the input. Translate only the string values. Use formal/professional register appropriate for a legal tools platform. PROMPT; function translateBatch(array $azConfig, string $langName, string $systemPrompt, array $batch): array { $prompt = str_replace('{LANG}', $langName, $systemPrompt); $json = json_encode($batch, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $response = azureChat($azConfig, [ ['role' => 'system', 'content' => $prompt], ['role' => 'user', 'content' => $json], ], ['json' => true, 'max_tokens' => 4096, 'timeout' => 120]); $content = trim((string)($response['choices'][0]['message']['content'] ?? '')); $decoded = json_decode($content, true); if (!is_array($decoded)) { echo " [WARN] JSON parse failed, trying extraction...\n"; if (preg_match('/\{.*\}/s', $content, $m)) { $decoded = json_decode($m[0], true); } } if (!is_array($decoded)) { echo " [ERROR] Could not parse response, using English fallback\n"; return $batch; } foreach ($batch as $k => $v) { if (!isset($decoded[$k])) $decoded[$k] = $v; } return $decoded; } function writePhpFile(string $path, array $data, string $comment): void { $php = " 'MCP — Do Better Norge', 'mcp_meta_desc' => 'Connect Claude, Cursor, and other AI tools to all 19 DBN legal preparation tools via MCP.', 'mcp_hero_badge' => '✦ Plus & Pro', 'mcp_hero_h1' => 'Use DBN tools from Claude, Cursor & Copilot', 'mcp_hero_sub' => 'Connect any MCP client to all 19 Do Better Norge tools — transcription, legal analysis, timelines, redaction, and more.', 'mcp_token_section_title' => 'Your MCP token', 'mcp_gate_guest_p' => 'Sign in to create your personal MCP token. Available to Plus and Pro members.', 'mcp_gate_guest_btn' => 'Sign in', 'mcp_gate_free_p' => 'MCP access is available on Plus and Pro plans. Upgrade to connect your AI tools.', 'mcp_gate_free_btn' => 'Upgrade plan', 'mcp_token_hint' => 'Tokens are shown once at creation. Create one per client (Claude, Cursor, VS Code…).', 'mcp_token_create_btn' => 'Create token', 'mcp_token_reveal_label' => 'Copy this token now — it will not be shown again:', 'mcp_token_copy_btn' => 'Copy token', 'mcp_token_no_tokens' => 'No MCP tokens yet.', 'mcp_token_active' => 'Active', 'mcp_token_revoked' => 'Revoked', 'mcp_token_never_used' => 'Never used', 'mcp_token_last_used' => 'Last used', 'mcp_token_revoke_btn' => 'Revoke', 'mcp_config_title' => 'Client configuration', 'mcp_config_hint' => 'Paste your token into the config below after creating it above.', 'mcp_config_token_filled' => 'Token auto-filled.', 'mcp_config_run_terminal' => 'Run in your terminal:', 'mcp_test_btn' => 'Test connection', 'mcp_test_no_token' => 'Create a token first.', 'mcp_test_testing' => 'Testing…', 'mcp_tools_title' => 'Available tools', 'mcp_tools_sub' => 'All tools run on your Plus or Pro plan credits. Click a card for full technical details.', 'mcp_tools_param_req_hint' => 'Purple = required', 'mcp_tools_view_details' => 'View details →', // privacy section 'mcp_privacy_title' => 'Privacy', 'mcp_privacy_text' => 'Process-and-forget by default. All tool calls process your text in memory and return results to your AI client. Nothing is saved to My Case unless you explicitly call dbn.save_to_case.', 'mcp_privacy_legal' => 'Tools provide legal preparation support, not final legal advice. Results are for informational purposes and should be reviewed by a qualified legal professional.', // mcp-tool.php 'mcp_tool_back' => '← Back to MCP setup', 'mcp_tool_params_title' => 'Parameters', 'mcp_tool_no_params' => 'This tool takes no input parameters.', 'mcp_tool_col_param' => 'Parameter', 'mcp_tool_col_type' => 'Type', 'mcp_tool_col_required' => 'Required', 'mcp_tool_col_desc' => 'Description', 'mcp_tool_example_req' => 'Example request', 'mcp_tool_example_resp' => 'Example response', 'mcp_tool_connect_title' => 'Connect', 'mcp_tool_connect_text' => 'Create your MCP token on the setup page and use it with any supported client.', 'mcp_tool_setup_link' => 'Set up MCP →', 'mcp_tool_yes' => 'Yes', 'mcp_tool_no' => 'No', ]; echo "\n=== Part 1: Translating MCP UI chrome strings ===\n"; $chromeTranslations = ['en' => $chromeEn]; $batchSize = 20; $keys = array_keys($chromeEn); $batches = array_chunk($keys, $batchSize); foreach ($languages as $langCode => $langName) { echo " Language: $langName ($langCode)\n"; $langResult = []; foreach ($batches as $idx => $batchKeys) { $batchNum = $idx + 1; echo " Batch $batchNum/" . count($batches) . " (" . count($batchKeys) . " strings)...\n"; $batchInput = []; foreach ($batchKeys as $k) $batchInput[$k] = $chromeEn[$k]; $langResult = array_merge($langResult, translateBatch($azConfig, $langName, $systemPrompt, $batchInput)); } $chromeTranslations[$langCode] = $langResult; } $chromeOutPath = dirname(__DIR__) . '/translations/mcp-chrome.php'; writePhpFile($chromeOutPath, $chromeTranslations, 'MCP UI chrome translations — mcp.php + mcp-tool.php'); echo " Written: $chromeOutPath\n"; // ───────────────────────────────────────────────────────────────────────────── // Part 2 — MCP tool content (display_name, description, param descriptions) // ───────────────────────────────────────────────────────────────────────────── // English source for all tool content — param descriptions are authored here // since DbnMcpRuntime.php params mostly lack description fields. $toolsEn = [ 'dbn.search_legal' => [ 'display_name' => 'Search DBN legal corpus', 'description' => 'Search the DBN Norwegian family-law corpus.', 'params' => [ 'query' => 'The search query (minimum 3 characters). Enter a legal topic, keyword, or question.', 'language' => 'Response language: en, no, uk, pl, or auto (detect from input).', 'limit' => 'Maximum number of results to return (1–10).', 'corpus_scope' => 'Which corpus to search: shared (public legal corpus), private (your uploaded documents), or both.', ], ], 'dbn.ask' => [ 'display_name' => 'Ask a legal question', 'description' => 'Answer a legal preparation question with source-grounded DBN context.', 'params' => [ 'question' => 'The legal question to answer (minimum 5 characters).', 'language' => 'Response language: en, no, uk, pl, or auto.', 'use_case_context' => 'Include context from your Case Workbench session (true/false).', ], ], 'dbn.summarize' => [ 'display_name' => 'Summarize document', 'description' => 'Summarize pasted case text with optional legal-corpus enrichment.', 'params' => [ 'text' => 'The text to summarize.', 'language' => 'Response language: en, no, uk, pl, or auto.', 'use_legal_corpus' => 'Enrich the summary with relevant legal corpus passages (true/false).', 'use_case_context' => 'Include context from your Case Workbench session (true/false).', ], ], 'dbn.timeline' => [ 'display_name' => 'Extract timeline', 'description' => 'Extract dates, hearings, milestones, and deadlines from case text.', 'params' => [ 'text' => 'The case text to extract dates from.', 'language' => 'Response language: en, no, uk, pl, or auto.', 'focus' => 'What to extract: all (every date), deadlines (appeal windows and filing deadlines), hearings (tribunal and court dates), cps (Barnevernet milestones).', 'use_case_context' => 'Include context from your Case Workbench session (true/false).', ], ], 'dbn.redact' => [ 'display_name' => 'Redact private data', 'description' => 'Remove or pseudonymize names, IDs, phone numbers, addresses, and places.', 'params' => [ 'text' => 'The text to redact.', 'language' => 'Language of the input text: en, no, uk, pl, or auto.', 'mode' => 'Redaction scope: standard (names, IDs, phones) or strict (also addresses, locations, institutions).', 'output_format' => 'Replacement style: contextual ([PERSON A]), generic (█████), or pseudonym (consistent invented names).', ], ], 'dbn.translate' => [ 'display_name' => 'Translate legal document', 'description' => 'Translate Norwegian family-law text with legal terminology annotations.', 'params' => [ 'text' => 'The text to translate.', 'source_lang' => 'Language of the input text. Use auto to detect automatically.', 'target_lang' => 'Language to translate into (required): en, no, uk, or pl.', 'doc_type' => 'Document type hint for legal terminology: auto, barnevernet, adopsjon, emergency, samvaer, fylkesnemnd, or other.', ], ], 'dbn.legal_analysis' => [ 'display_name' => 'Legal analysis', 'description' => 'Extract legal issues from a document and answer each with DBN legal context.', 'params' => [ 'text' => 'The document text to analyze.', 'language' => 'Response language: en, no, uk, pl, or auto.', 'doc_type' => 'Document type hint: auto, barnevernet, adopsjon, emergency, samvaer, fylkesnemnd, or other.', ], ], 'dbn.korrespond' => [ 'display_name' => 'Draft authority correspondence', 'description' => 'Draft a reply or new letter to Norwegian authorities.', 'params' => [ 'narrative' => 'Description of your situation — what happened and what you need.', 'received_text' => 'Text of the letter or decision you received (for reply mode).', 'recipient_body' => 'The authority you are writing to (e.g. Barnevernet, NAV, Skole, Kommune).', 'goal' => 'Your legal goal — what you want the letter to achieve.', 'mode' => 'reply (responding to a received letter) or initiate (starting new correspondence).', 'language' => 'Response language: en, no, uk, pl, or auto.', 'use_case_context' => 'Include context from your Case Workbench session (true/false).', 'force_draft' => 'Skip the clarify gate and draft immediately (true/false).', ], ], 'dbn.barnevernet_analyze' => [ 'display_name' => 'Analyze Barnevernet document', 'description' => 'Analyze child-welfare documents for red flags and legal issues.', 'params' => [ 'document_text' => 'Full text of the child welfare document to analyze (required).', 'filename' => 'Original filename for context (helps identify document type).', 'advocate_role' => 'Your role in the case: parent, lawyer, guardian ad litem, etc.', 'language' => 'Response language: en, no, uk, pl, or auto.', 'use_case_context' => 'Include context from your Case Workbench session (true/false).', ], ], 'dbn.advocate_brief' => [ 'display_name' => 'Create advocate brief', 'description' => 'Generate a source-grounded brief for a chosen party or role.', 'params' => [ 'query' => 'Legal question or topic for the brief.', 'paste_text' => 'Document text to use as the basis for the brief.', 'advocate_role' => 'The party or role to advocate for (required). E.g. parent, child, Barnevernet.', 'language' => 'Response language: en, no, uk, pl, or auto.', 'use_case_context' => 'Include context from your Case Workbench session (true/false).', ], ], 'dbn.deep_research' => [ 'display_name' => 'Deep research', 'description' => 'Expand a legal question into research angles and synthesize a cited brief.', 'params' => [ 'query' => 'Legal question to research in depth.', 'paste_text' => 'Supporting document text to research from.', 'language' => 'Response language: en, no, uk, pl, or auto.', 'use_case_context' => 'Include context from your Case Workbench session (true/false).', ], ], 'dbn.discrepancy_find' => [ 'display_name' => 'Find document discrepancies', 'description' => 'Compare two document versions for contradictions, deletions, and added claims.', 'params' => [ 'document_a_text' => 'Text of the first document (required).', 'document_b_text' => 'Text of the second document to compare against the first (required).', 'filename_a' => 'Filename of the first document (for reference in the report).', 'filename_b' => 'Filename of the second document (for reference in the report).', 'language' => 'Response language: en, no, uk, pl, or auto.', ], ], 'dbn.transcribe_audio' => [ 'display_name' => 'Transcribe audio', 'description' => 'Transcribe an audio file from base64-encoded content or a URL.', 'params' => [ 'audio_base64' => 'Base64-encoded audio file content. Use either this or audio_url.', 'audio_url' => 'URL to a publicly accessible audio file. Use either this or audio_base64.', 'filename' => 'Original filename with extension (e.g. recording.mp3) — helps set the correct audio format.', 'language' => 'Language spoken in the audio (e.g. no, en, uk). Leave blank for auto-detection.', 'diarize' => 'Enable speaker diarization — label each segment with a speaker identifier (true/false).', ], ], 'dbn.corpus_stats' => [ 'display_name' => 'Corpus statistics', 'description' => 'Return document and chunk counts and active legal sources in the DBN corpus.', 'params' => [], ], 'dbn.list_documents' => [ 'display_name' => 'List corpus documents', 'description' => 'List DBN legal corpus documents with optional filters.', 'params' => [ 'category' => 'Filter by document category (e.g. barnevernet, statsforvalter, echr).', 'title' => 'Filter by title — partial match.', 'limit' => 'Maximum number of documents to return (1–50, default 20).', 'offset' => 'Number of documents to skip (for pagination).', ], ], 'dbn.get_document' => [ 'display_name' => 'Get document chunks', 'description' => 'Fetch a document and its text chunks by numeric document ID.', 'params' => [ 'document_id' => 'The numeric ID of the document to retrieve (required).', ], ], 'dbn.citation_graph' => [ 'display_name' => 'Explore citation graph', 'description' => 'Explore cites, cited-by, implementation, or chain relationships.', 'params' => [ 'doc_id' => 'The numeric ID of the document to explore (required).', 'action' => 'Relationship to traverse: cites, cited_by, implements, or chain (full citation path).', 'depth' => 'How many levels of relationships to explore (1–3, default 1).', ], ], 'dbn.case_workbench_plan' => [ 'display_name' => 'Plan next case step', 'description' => 'Create a stateless legal preparation plan based on your situation.', 'params' => [ 'situation' => 'Describe your current legal situation and what you need help with (required).', 'goal' => 'Your desired outcome or legal goal.', 'deadline' => 'Any relevant deadline (date or description, e.g. "3 weeks").', 'language' => 'Response language: en, no, uk, pl, or auto.', ], ], 'dbn.save_to_case' => [ 'display_name' => 'Save result to My Case', 'description' => 'Explicitly save a prior MCP tool result to the user case record.', 'params' => [ 'tool' => 'The MCP tool slug whose result you are saving (required).', 'title' => 'A descriptive title for this saved result.', 'input_payload' => 'The input parameters used to generate the result (required).', 'output_payload' => 'The tool result to save (required).', 'meta' => 'Optional metadata (e.g. notes, tags, source reference).', ], ], ]; echo "\n=== Part 2: Translating MCP tool content ===\n"; // Flatten all tool strings for translation (display_name, description, params) // Key format: {short_slug}__name, {short_slug}__desc, {short_slug}__p__{param} $flatSource = []; $slugMap = []; // short_slug => full_slug foreach ($toolsEn as $fullSlug => $toolData) { $shortSlug = str_replace('dbn.', '', $fullSlug); $slugMap[$shortSlug] = $fullSlug; $flatSource[$shortSlug . '__name'] = $toolData['display_name']; $flatSource[$shortSlug . '__desc'] = $toolData['description']; foreach ($toolData['params'] as $param => $desc) { $flatSource[$shortSlug . '__p__' . $param] = $desc; } } // Translate in batches of 20 $flatKeys = array_keys($flatSource); $batches = array_chunk($flatKeys, 20); $flatTranslations = ['en' => $flatSource]; foreach ($languages as $langCode => $langName) { echo " Language: $langName ($langCode)\n"; $langResult = []; foreach ($batches as $idx => $batchKeys) { $batchNum = $idx + 1; echo " Batch $batchNum/" . count($batches) . " (" . count($batchKeys) . " strings)...\n"; $batchInput = []; foreach ($batchKeys as $k) $batchInput[$k] = $flatSource[$k]; $langResult = array_merge($langResult, translateBatch($azConfig, $langName, $systemPrompt, $batchInput)); } $flatTranslations[$langCode] = $langResult; } // Reconstruct nested structure: [full_slug][lang] => ['display_name', 'description', 'params'] $toolTranslations = []; foreach ($toolsEn as $fullSlug => $toolData) { $shortSlug = str_replace('dbn.', '', $fullSlug); foreach (['en', 'no', 'uk', 'pl'] as $lc) { $toolTranslations[$fullSlug][$lc] = [ 'display_name' => $flatTranslations[$lc][$shortSlug . '__name'] ?? $toolData['display_name'], 'description' => $flatTranslations[$lc][$shortSlug . '__desc'] ?? $toolData['description'], 'params' => [], ]; foreach (array_keys($toolData['params']) as $param) { $toolTranslations[$fullSlug][$lc]['params'][$param] = $flatTranslations[$lc][$shortSlug . '__p__' . $param] ?? $toolData['params'][$param]; } } } $toolOutPath = dirname(__DIR__) . '/includes/mcp-tool-translations.php'; writePhpFile($toolOutPath, $toolTranslations, 'MCP tool translations — display_name, description, param descriptions'); echo " Written: $toolOutPath\n"; echo "\n✓ Done. Next steps:\n"; echo " 1. Copy translations from translations/mcp-chrome.php into includes/i18n.php\n"; echo " 2. The tool translations are ready in includes/mcp-tool-translations.php\n";