Files
dobetternorge-tools/scripts/generate-mcp-translations.php
T
daveadmin 1bfafa9908 Localize mcp.php + add mcp-tool.php detail pages for all 19 MCP tools
- Replace all hardcoded English strings in mcp.php with dbnToolsT() calls
- Add 44 MCP UI chrome translation keys to includes/i18n.php (en/no/uk/pl)
- Generate includes/mcp-tool-translations.php with tool names, descriptions,
  and parameter docs translated into Norwegian, Ukrainian, and Polish via Azure OpenAI
- Create mcp-tool.php: parameterized detail page (?tool=dbn.slug) with parameter
  table, example request/response JSON, and privacy section, all localized
- Add "View details →" links on tool cards in mcp.php (shown on expand)
- Add translations/mcp-chrome.php and scripts/generate-mcp-translations.php

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 12:05:07 +02:00

464 lines
25 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* generate-mcp-translations.php
* One-shot CLI script: php scripts/generate-mcp-translations.php
*
* Produces two files:
* 1. translations/mcp-chrome.php — UI chrome strings for mcp.php + mcp-tool.php
* 2. includes/mcp-tool-translations.php — tool names, descriptions, param docs in all 4 languages
*/
declare(strict_types=1);
define('SCRIPT_MODE', true);
require_once dirname(__DIR__) . '/includes/bootstrap.php';
// ─────────────────────────────────────────────────────────────────────────────
// Azure config (same pattern as generate-page-translations.php)
// ─────────────────────────────────────────────────────────────────────────────
$azConfig = [
'endpoint' => 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 = "<?php\n// $comment\n// DO NOT EDIT MANUALLY — regenerate with scripts/generate-mcp-translations.php\nreturn ";
$php .= var_export($data, true);
$php .= ";\n";
file_put_contents($path, $php);
}
// ─────────────────────────────────────────────────────────────────────────────
// Part 1 — MCP UI chrome strings
// ─────────────────────────────────────────────────────────────────────────────
$chromeEn = [
// mcp.php
'mcp_page_title' => '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 (110).',
'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 (150, 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 (13, 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";