mirror of
https://github.com/symfony/ai.git
synced 2026-03-23 23:42:18 +01:00
[Mate][Symfony] Add TranslationCollectorFormatter for Symfony profiler
This commit is contained in:
committed by
Christopher Hertel
parent
8b3c9f8221
commit
69fedcbd80
@@ -37,6 +37,7 @@
|
||||
"symfony/monolog-bundle": "^4.0",
|
||||
"symfony/runtime": "^8.0",
|
||||
"symfony/twig-bundle": "^8.0",
|
||||
"symfony/translation": "^8.0",
|
||||
"symfony/uid": "^8.0",
|
||||
"symfony/ux-dropzone": "^2.31",
|
||||
"symfony/ux-icons": "^2.31",
|
||||
|
||||
5
demo/config/packages/translation.yaml
Normal file
5
demo/config/packages/translation.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
framework:
|
||||
default_locale: en
|
||||
translator:
|
||||
default_path: '%kernel.project_dir%/translations'
|
||||
fallbacks: ['en']
|
||||
@@ -281,6 +281,19 @@
|
||||
"assets/controllers/blog_controller.js"
|
||||
]
|
||||
},
|
||||
"symfony/translation": {
|
||||
"version": "8.0",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "6.3",
|
||||
"ref": "620a1b84865ceb2ba304c8f8bf2a185fbf32a843"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/translation.yaml",
|
||||
"translations/.gitignore"
|
||||
]
|
||||
},
|
||||
"symfony/twig-bundle": {
|
||||
"version": "7.0",
|
||||
"recipe": {
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h1 class="text-dark-emphasis">Welcome to the <strong class="text-dark">Symfony AI</strong> Demo</h1>
|
||||
<h1 class="text-dark-emphasis">{{ 'home.title_prefix'|trans }} <strong class="text-dark">Symfony AI</strong> {{ 'home.title_suffix'|trans }}</h1>
|
||||
<p class="my-4 text-dark">
|
||||
This is a small demo app that can be used to explore the capabilities of Symfony AI Components together with
|
||||
Symfony UX and Twig Live Components.<br />
|
||||
{{ 'home.intro'|trans }}<br />
|
||||
{{ 'home.intro_suffix'|trans }}<br />
|
||||
Central to this demo are five chatbot examples that are implemented in <code>src/**/Chat.php</code> and AI
|
||||
configuration can be found in <code>config/packages/ai.yaml</code>.
|
||||
</p>
|
||||
<h3 class="text-dark">Examples</h3>
|
||||
<h3 class="text-dark">{{ 'home.examples'|trans }}</h3>
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<div class="card youtube bg-body shadow-sm">
|
||||
|
||||
6
demo/translations/messages.en.yaml
Normal file
6
demo/translations/messages.en.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
home:
|
||||
title_prefix: 'Welcome to the'
|
||||
title_suffix: 'Demo'
|
||||
intro: 'This is a small demo app that can be used to explore the capabilities of Symfony AI Components together with'
|
||||
intro_suffix: 'Symfony UX and Twig Live Components.'
|
||||
examples: 'Examples'
|
||||
@@ -5,6 +5,7 @@ CHANGELOG
|
||||
---
|
||||
|
||||
* Add `MailerCollectorFormatter` to expose Symfony Mailer data (recipients, body preview, links, attachments, transport) to AI via the profiler
|
||||
* Add `TranslationCollectorFormatter` to expose Symfony Translation data (locale, fallback locales, message states) to AI via the profiler
|
||||
|
||||
0.3
|
||||
---
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\AI\Mate\Bridge\Symfony\Profiler\Service\Formatter;
|
||||
|
||||
use Symfony\AI\Mate\Bridge\Symfony\Profiler\Service\CollectorFormatterInterface;
|
||||
use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface;
|
||||
use Symfony\Component\Translation\DataCollector\TranslationDataCollector;
|
||||
|
||||
/**
|
||||
* Formats translation collector data.
|
||||
*
|
||||
* @author Johannes Wachter <johannes@sulu.io>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @implements CollectorFormatterInterface<TranslationDataCollector>
|
||||
*/
|
||||
final class TranslationCollectorFormatter implements CollectorFormatterInterface
|
||||
{
|
||||
private const STATE_MAP = [0 => 'defined', 1 => 'missing', 2 => 'fallback'];
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'translation';
|
||||
}
|
||||
|
||||
public function format(DataCollectorInterface $collector): array
|
||||
{
|
||||
\assert($collector instanceof TranslationDataCollector);
|
||||
|
||||
return [
|
||||
'locale' => $collector->getLocale(),
|
||||
'fallback_locales' => $this->extractArray($collector->getFallbackLocales()),
|
||||
'count_defines' => $collector->getCountDefines(),
|
||||
'count_missings' => $collector->getCountMissings(),
|
||||
'count_fallbacks' => $collector->getCountFallbacks(),
|
||||
'messages' => $this->formatMessages($collector->getMessages()),
|
||||
];
|
||||
}
|
||||
|
||||
public function getSummary(DataCollectorInterface $collector): array
|
||||
{
|
||||
\assert($collector instanceof TranslationDataCollector);
|
||||
|
||||
return [
|
||||
'locale' => $collector->getLocale(),
|
||||
'count_defines' => $collector->getCountDefines(),
|
||||
'count_missings' => $collector->getCountMissings(),
|
||||
'count_fallbacks' => $collector->getCountFallbacks(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed>
|
||||
*/
|
||||
private function extractArray(mixed $data): array
|
||||
{
|
||||
if (\is_array($data)) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
if (\is_object($data) && method_exists($data, 'getValue')) {
|
||||
return (array) $data->getValue(true);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array<string, mixed>>
|
||||
*/
|
||||
private function formatMessages(mixed $messages): array
|
||||
{
|
||||
$messages = $this->extractArray($messages);
|
||||
|
||||
$formatted = [];
|
||||
foreach ($messages as $message) {
|
||||
if (!\is_array($message)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$formatted[] = [
|
||||
'locale' => $message['locale'] ?? null,
|
||||
'domain' => $message['domain'] ?? null,
|
||||
'id' => $message['id'] ?? null,
|
||||
'translation' => $message['translation'] ?? null,
|
||||
'state' => $this->mapState((int) ($message['state'] ?? 0)),
|
||||
'count' => $message['count'] ?? 1,
|
||||
];
|
||||
}
|
||||
|
||||
return $formatted;
|
||||
}
|
||||
|
||||
private function mapState(int $state): string
|
||||
{
|
||||
return self::STATE_MAP[$state] ?? 'defined';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Symfony package.
|
||||
*
|
||||
* (c) Fabien Potencier <fabien@symfony.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Symfony\AI\Mate\Bridge\Symfony\Tests\Profiler\Service\Formatter;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\AI\Mate\Bridge\Symfony\Profiler\Service\Formatter\TranslationCollectorFormatter;
|
||||
use Symfony\Component\Translation\DataCollector\TranslationDataCollector;
|
||||
use Symfony\Component\VarDumper\Cloner\Data;
|
||||
|
||||
/**
|
||||
* @author Johannes Wachter <johannes@sulu.io>
|
||||
*/
|
||||
final class TranslationCollectorFormatterTest extends TestCase
|
||||
{
|
||||
private TranslationCollectorFormatter $formatter;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->formatter = new TranslationCollectorFormatter();
|
||||
}
|
||||
|
||||
public function testGetName()
|
||||
{
|
||||
$this->assertSame('translation', $this->formatter->getName());
|
||||
}
|
||||
|
||||
public function testFormat()
|
||||
{
|
||||
$collector = $this->createMock(TranslationDataCollector::class);
|
||||
$collector->method('getLocale')->willReturn('en');
|
||||
$collector->method('getFallbackLocales')->willReturn(['fr', 'de']);
|
||||
$collector->method('getCountDefines')->willReturn(5);
|
||||
$collector->method('getCountMissings')->willReturn(2);
|
||||
$collector->method('getCountFallbacks')->willReturn(1);
|
||||
$collector->method('getMessages')->willReturn([
|
||||
[
|
||||
'locale' => 'en',
|
||||
'domain' => 'messages',
|
||||
'id' => 'hello',
|
||||
'translation' => 'Hello',
|
||||
'state' => 0,
|
||||
'count' => 3,
|
||||
],
|
||||
]);
|
||||
|
||||
$result = $this->formatter->format($collector);
|
||||
|
||||
$this->assertSame('en', $result['locale']);
|
||||
$this->assertSame(['fr', 'de'], $result['fallback_locales']);
|
||||
$this->assertSame(5, $result['count_defines']);
|
||||
$this->assertSame(2, $result['count_missings']);
|
||||
$this->assertSame(1, $result['count_fallbacks']);
|
||||
$this->assertCount(1, $result['messages']);
|
||||
$this->assertSame('en', $result['messages'][0]['locale']);
|
||||
$this->assertSame('messages', $result['messages'][0]['domain']);
|
||||
$this->assertSame('hello', $result['messages'][0]['id']);
|
||||
$this->assertSame('Hello', $result['messages'][0]['translation']);
|
||||
$this->assertSame('defined', $result['messages'][0]['state']);
|
||||
$this->assertSame(3, $result['messages'][0]['count']);
|
||||
}
|
||||
|
||||
public function testFormatWithMissingTranslation()
|
||||
{
|
||||
$collector = $this->createMock(TranslationDataCollector::class);
|
||||
$collector->method('getLocale')->willReturn('en');
|
||||
$collector->method('getFallbackLocales')->willReturn([]);
|
||||
$collector->method('getCountDefines')->willReturn(0);
|
||||
$collector->method('getCountMissings')->willReturn(1);
|
||||
$collector->method('getCountFallbacks')->willReturn(0);
|
||||
$collector->method('getMessages')->willReturn([
|
||||
[
|
||||
'locale' => 'en',
|
||||
'domain' => 'messages',
|
||||
'id' => 'missing.key',
|
||||
'translation' => 'missing.key',
|
||||
'state' => 1,
|
||||
'count' => 1,
|
||||
],
|
||||
]);
|
||||
|
||||
$result = $this->formatter->format($collector);
|
||||
|
||||
$this->assertSame('missing', $result['messages'][0]['state']);
|
||||
}
|
||||
|
||||
public function testFormatWithFallbackTranslation()
|
||||
{
|
||||
$collector = $this->createMock(TranslationDataCollector::class);
|
||||
$collector->method('getLocale')->willReturn('de');
|
||||
$collector->method('getFallbackLocales')->willReturn(['en']);
|
||||
$collector->method('getCountDefines')->willReturn(0);
|
||||
$collector->method('getCountMissings')->willReturn(0);
|
||||
$collector->method('getCountFallbacks')->willReturn(1);
|
||||
$collector->method('getMessages')->willReturn([
|
||||
[
|
||||
'locale' => 'en',
|
||||
'domain' => 'messages',
|
||||
'id' => 'fallback.key',
|
||||
'translation' => 'Fallback value',
|
||||
'state' => 2,
|
||||
'count' => 1,
|
||||
],
|
||||
]);
|
||||
|
||||
$result = $this->formatter->format($collector);
|
||||
|
||||
$this->assertSame('fallback', $result['messages'][0]['state']);
|
||||
}
|
||||
|
||||
public function testGetSummary()
|
||||
{
|
||||
$collector = $this->createMock(TranslationDataCollector::class);
|
||||
$collector->method('getLocale')->willReturn('en');
|
||||
$collector->method('getCountDefines')->willReturn(10);
|
||||
$collector->method('getCountMissings')->willReturn(3);
|
||||
$collector->method('getCountFallbacks')->willReturn(2);
|
||||
|
||||
$result = $this->formatter->getSummary($collector);
|
||||
|
||||
$this->assertSame('en', $result['locale']);
|
||||
$this->assertSame(10, $result['count_defines']);
|
||||
$this->assertSame(3, $result['count_missings']);
|
||||
$this->assertSame(2, $result['count_fallbacks']);
|
||||
$this->assertArrayNotHasKey('messages', $result);
|
||||
$this->assertArrayNotHasKey('fallback_locales', $result);
|
||||
}
|
||||
|
||||
public function testFormatHandlesDataObjectForFallbackLocales()
|
||||
{
|
||||
$data = $this->createMock(Data::class);
|
||||
$data->method('getValue')->with(true)->willReturn(['fr', 'de']);
|
||||
|
||||
$collector = $this->createMock(TranslationDataCollector::class);
|
||||
$collector->method('getLocale')->willReturn('en');
|
||||
$collector->method('getFallbackLocales')->willReturn($data);
|
||||
$collector->method('getCountDefines')->willReturn(0);
|
||||
$collector->method('getCountMissings')->willReturn(0);
|
||||
$collector->method('getCountFallbacks')->willReturn(0);
|
||||
$collector->method('getMessages')->willReturn([]);
|
||||
|
||||
$result = $this->formatter->format($collector);
|
||||
|
||||
$this->assertSame(['fr', 'de'], $result['fallback_locales']);
|
||||
}
|
||||
|
||||
public function testFormatHandlesDataObjectForMessages()
|
||||
{
|
||||
$messages = [
|
||||
[
|
||||
'locale' => 'en',
|
||||
'domain' => 'messages',
|
||||
'id' => 'hello',
|
||||
'translation' => 'Hello',
|
||||
'state' => 0,
|
||||
'count' => 1,
|
||||
],
|
||||
];
|
||||
|
||||
$data = $this->createMock(Data::class);
|
||||
$data->method('getValue')->with(true)->willReturn($messages);
|
||||
|
||||
$collector = $this->createMock(TranslationDataCollector::class);
|
||||
$collector->method('getLocale')->willReturn('en');
|
||||
$collector->method('getFallbackLocales')->willReturn([]);
|
||||
$collector->method('getCountDefines')->willReturn(1);
|
||||
$collector->method('getCountMissings')->willReturn(0);
|
||||
$collector->method('getCountFallbacks')->willReturn(0);
|
||||
$collector->method('getMessages')->willReturn($data);
|
||||
|
||||
$result = $this->formatter->format($collector);
|
||||
|
||||
$this->assertCount(1, $result['messages']);
|
||||
$this->assertSame('hello', $result['messages'][0]['id']);
|
||||
$this->assertSame('defined', $result['messages'][0]['state']);
|
||||
}
|
||||
}
|
||||
@@ -38,12 +38,14 @@
|
||||
"phpunit/phpunit": "^11.5.53",
|
||||
"symfony/http-kernel": "^7.3|^8.0",
|
||||
"symfony/mailer": "^7.3|^8.0",
|
||||
"symfony/translation": "^7.3|^8.0",
|
||||
"symfony/var-dumper": "^7.3|^8.0",
|
||||
"symfony/web-profiler-bundle": "^7.3|^8.0"
|
||||
},
|
||||
"suggest": {
|
||||
"symfony/http-kernel": "Required for profiler data access tools",
|
||||
"symfony/mailer": "Required for mailer profiler formatter",
|
||||
"symfony/translation": "Required for translation profiler collector formatter",
|
||||
"symfony/web-profiler-bundle": "Required for profiler data access tools"
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
|
||||
@@ -16,6 +16,7 @@ use Symfony\AI\Mate\Bridge\Symfony\Profiler\Service\CollectorRegistry;
|
||||
use Symfony\AI\Mate\Bridge\Symfony\Profiler\Service\Formatter\ExceptionCollectorFormatter;
|
||||
use Symfony\AI\Mate\Bridge\Symfony\Profiler\Service\Formatter\MailerCollectorFormatter;
|
||||
use Symfony\AI\Mate\Bridge\Symfony\Profiler\Service\Formatter\RequestCollectorFormatter;
|
||||
use Symfony\AI\Mate\Bridge\Symfony\Profiler\Service\Formatter\TranslationCollectorFormatter;
|
||||
use Symfony\AI\Mate\Bridge\Symfony\Profiler\Service\ProfilerDataProvider;
|
||||
use Symfony\AI\Mate\Bridge\Symfony\Service\ContainerProvider;
|
||||
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
||||
@@ -67,6 +68,10 @@ return static function (ContainerConfigurator $configurator) {
|
||||
->lazy()
|
||||
->tag('ai_mate.profiler_collector_formatter');
|
||||
|
||||
$services->set(TranslationCollectorFormatter::class)
|
||||
->lazy()
|
||||
->tag('ai_mate.profiler_collector_formatter');
|
||||
|
||||
// MCP Capabilities
|
||||
$services->set(ProfilerTool::class)
|
||||
->args([service(ProfilerDataProvider::class)]);
|
||||
|
||||
Reference in New Issue
Block a user