Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/Override/OverrideAwareTranslator.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace CylleneDigital\AiTranslationBundle\Override;

use CylleneDigital\AiTranslationBundle\Catalogue\TranslationCatalogue;
use CylleneDigital\AiTranslationBundle\Entity\TranslationOverride;
use CylleneDigital\AiTranslationBundle\Locale\LocaleFallback;
use CylleneDigital\AiTranslationBundle\Repository\TranslationOverrideRepository;
use CylleneDigital\AiTranslationBundle\Scope\ScopeProviderInterface;
Expand Down Expand Up @@ -307,7 +308,10 @@ private function fetchOverrides(string $locale, string $scope): array
// 3. within a rank, additional-root catalogues ("@label/…" — themes and
// friends, whose files also win at runtime) shadow the main root's;
// 4. catalogue/key order otherwise.
usort($overrides, static fn ($a, $b): int => [-LocaleFallback::rank($a->getLocale()), '' === $a->getScope(), !str_starts_with($a->getCatalogue(), '@'), $a->getCatalogue(), $a->getKey()] <=> [-LocaleFallback::rank($b->getLocale()), '' === $b->getScope(), !str_starts_with($b->getCatalogue(), '@'), $b->getCatalogue(), $b->getKey()]);
// The sort key of each row is built once: ranking both locales again in every
// comparison weighed more than the query on a large locale.
$sortKeys = array_map(static fn (TranslationOverride $override): array => [-LocaleFallback::rank($override->getLocale()), '' === $override->getScope(), !str_starts_with($override->getCatalogue(), '@'), $override->getCatalogue(), $override->getKey()], $overrides);
uksort($overrides, static fn (int $a, int $b): int => $sortKeys[$a] <=> $sortKeys[$b]);

$result = [];
/** @var array<string, array<string, string>> $winningLocale domain => key => the locale that answered */
Expand Down
40 changes: 29 additions & 11 deletions src/Override/OverrideReader.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,11 @@ public function getTranslationsForCatalogues(array $catalogues, string $locale,
}
}

foreach ($this->repository->findByLocaleAndCatalogues($locale, $catalogues, $scope) as $override) {
$catalogue = $override->getCatalogue();
$key = $override->getKey();
foreach ($this->repository->findValuesByLocaleAndCatalogues($locale, $catalogues, $scope) as $row) {
$catalogue = $row['catalogue'];
$key = $row['key'];
$translations[$catalogue][$key] ??= $empty;
$translations[$catalogue][$key]['override'] = $override->getValue();
$translations[$catalogue][$key]['override'] = $row['value'];
$translations[$catalogue][$key]['hasOverride'] = true;
}

Expand Down Expand Up @@ -106,13 +106,21 @@ public function getEffectiveOverridesByCatalogue(string $locale, string $scope =
$overrides = $this->repository->findForRuntime($locale, $scope);

// Least specific locale first, and within a locale the global rows before the
// scope's own ones: the later passes overwrite.
usort($overrides, static fn (TranslationOverride $a, TranslationOverride $b): int => [LocaleFallback::rank($a->getLocale()), '' === $a->getScope() ? 0 : 1] <=> [LocaleFallback::rank($b->getLocale()), '' === $b->getScope() ? 0 : 1]);
// scope's own ones: the later passes overwrite. Bucketed, not sorted: a comparison
// that ranks both locales again weighed more than the query on a large locale.
$passes = [];

foreach ($overrides as $override) {
$passes[LocaleFallback::rank($override->getLocale()) * 2 + ('' === $override->getScope() ? 0 : 1)][] = $override;
}

ksort($passes);
$byCatalogue = [];

foreach ($overrides as $override) {
$byCatalogue[$override->getCatalogue()][$override->getKey()] = $override->getValue();
foreach ($passes as $pass) {
foreach ($pass as $override) {
$byCatalogue[$override->getCatalogue()][$override->getKey()] = $override->getValue();
}
}

return $byCatalogue;
Expand Down Expand Up @@ -374,12 +382,22 @@ private function inherited(string $locale, string $scope, ?array $catalogues = n
static fn (array $row): bool => $row['locale'] !== $locale || $row['scope'] !== $scope,
));

usort($rows, static fn (array $a, array $b): int => [-LocaleFallback::rank($a['locale']), '' === $a['scope']] <=> [-LocaleFallback::rank($b['locale']), '' === $b['scope']]);
// The more specific locale first, and within a locale the scope's row before the
// global one. Bucketed, not sorted: a comparison that ranks both locales again
// weighed more than the query on a large locale.
$passes = [];

foreach ($rows as $row) {
$passes[-LocaleFallback::rank($row['locale']) * 2 + ('' === $row['scope'] ? 1 : 0)][] = $row;
}

ksort($passes);
$inherited = [];

foreach ($rows as $row) {
$inherited[$row['catalogue']][(string) $row['key']] ??= ['value' => $row['value'], 'locale' => $row['locale'], 'scope' => $row['scope']];
foreach ($passes as $pass) {
foreach ($pass as $row) {
$inherited[$row['catalogue']][(string) $row['key']] ??= ['value' => $row['value'], 'locale' => $row['locale'], 'scope' => $row['scope']];
}
}

return $inherited;
Expand Down
17 changes: 10 additions & 7 deletions src/Repository/TranslationOverrideRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,20 +88,23 @@ public function lockOneByKey(string $key, string $catalogue, string $locale, str

/**
* The overrides of these catalogues in one (locale, scope): one query for a whole view,
* filtered in SQL — a single catalogue costs what it always cost.
* filtered in SQL — a single catalogue costs what it always cost. Scalar rows, not
* entities: a view reads thousands of them, and building an entity per row (and
* leaving it in the host's unit of work) cost more than the query itself.
*
* @param list<string> $catalogues
*
* @return list<TranslationOverride>
* @return list<array{key: string, catalogue: string, value: string}>
*/
public function findByLocaleAndCatalogues(string $locale, array $catalogues, string $scope = ''): array
public function findValuesByLocaleAndCatalogues(string $locale, array $catalogues, string $scope = ''): array
{
if ([] === $catalogues) {
return [];
}

/** @var list<TranslationOverride> $result */
$result = $this->createQueryBuilder('t')
/** @var list<array{key: string, catalogue: string, value: string}> $rows */
$rows = $this->createQueryBuilder('t')
->select('t.key', 't.catalogue', 't.value')
->andWhere('t.locale = :locale')
->andWhere('t.catalogue IN (:catalogues)')
->andWhere('t.scope = :scope')
Expand All @@ -111,9 +114,9 @@ public function findByLocaleAndCatalogues(string $locale, array $catalogues, str
->orderBy('t.catalogue', 'ASC')
->addOrderBy('t.key', 'ASC')
->getQuery()
->getResult();
->getArrayResult();

return $result;
return $rows;
}

/**
Expand Down
7 changes: 5 additions & 2 deletions tests/Unit/Estimation/GenerationCostEstimatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,11 @@ private function createEstimator(array $frMessages, array $frShopMessages = [],
$author->method('getAuthorIdentifier')->willReturn(null);

$overrideRepository = $this->createStub(TranslationOverrideRepository::class);
$overrideRepository->method('findByLocaleAndCatalogues')->willReturnCallback(static fn (string $locale, array $catalogues, string $scope = ''): array => null !== $overrides
? array_merge(...array_map(static fn (string $catalogue): array => $overrides($locale, $catalogue, $scope), array_values(array_filter($catalogues, is_string(...)))))
$overrideRepository->method('findValuesByLocaleAndCatalogues')->willReturnCallback(static fn (string $locale, array $catalogues, string $scope = ''): array => null !== $overrides
? array_map(
static fn (TranslationOverride $override): array => ['key' => $override->getKey(), 'catalogue' => $override->getCatalogue(), 'value' => $override->getValue()],
array_merge(...array_map(static fn (string $catalogue): array => $overrides($locale, $catalogue, $scope), array_values(array_filter($catalogues, is_string(...))))),
)
: []);

$suggestionRepository = $this->createStub(TranslationSuggestionRepository::class);
Expand Down
22 changes: 16 additions & 6 deletions tests/Unit/Override/TranslationManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,9 @@ public function testTranslationsMergeFileEntriesWithOverrides(): void
$orphan->setValue('Orpheline');

$this->repository
->method('findByLocaleAndCatalogues')
->method('findValuesByLocaleAndCatalogues')
->with('fr_FR', ['shop/Product/messages'])
->willReturn([$override, $orphan]);
->willReturn(self::rows($override, $orphan));

$manager = $this->createManager();
$translations = $manager->getTranslationsForCatalogue('shop/Product/messages', 'fr_FR');
Expand All @@ -110,7 +110,7 @@ public function testTranslationsMergeFileEntriesWithOverrides(): void

public function testAKeyOnlyTranslatedInAnotherLanguageIsMissing(): void
{
$this->repository->method('findByLocaleAndCatalogues')->willReturn([]);
$this->repository->method('findValuesByLocaleAndCatalogues')->willReturn([]);

$manager = $this->createManager();

Expand Down Expand Up @@ -215,10 +215,10 @@ public function testAScopedBrowseShowsTheGlobalOverrideAsInherited(): void
$scoped->setValue('Ajouter au panier (B2B)');

$this->repository
->method('findByLocaleAndCatalogues')
->method('findValuesByLocaleAndCatalogues')
->willReturnCallback(static fn (string $locale, array $catalogues, string $scope = ''): array => match ($scope) {
'' => [$global],
'b2b' => [$scoped],
'' => self::rows($global),
'b2b' => self::rows($scoped),
default => [],
});
// What the entry can inherit from — its own b2b row included, set apart by the reader.
Expand Down Expand Up @@ -312,4 +312,14 @@ private function suggestionRepository(): TranslationSuggestionRepository

return $repository;
}

/**
* What the view reads of these overrides: scalar rows.
*
* @return list<array{key: string, catalogue: string, value: string}>
*/
private static function rows(TranslationOverride ...$overrides): array
{
return array_map(static fn (TranslationOverride $override): array => ['key' => $override->getKey(), 'catalogue' => $override->getCatalogue(), 'value' => $override->getValue()], array_values($overrides));
}
}
7 changes: 5 additions & 2 deletions tests/Unit/Suggestion/TranslationAiServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,11 @@ protected function setUp(): void
$this->staged[] = $suggestion;
});
$this->suggestionRepository->method('findPending')->willReturnCallback(fn (): array => $this->pending);
$this->overrideRepository->method('findByLocaleAndCatalogues')->willReturnCallback(fn (string $locale, array $catalogues, string $scope = ''): array => null !== $this->overrides
? array_merge(...array_map(fn (string $catalogue): array => ($this->overrides)($locale, $catalogue, $scope), array_values(array_filter($catalogues, is_string(...)))))
$this->overrideRepository->method('findValuesByLocaleAndCatalogues')->willReturnCallback(fn (string $locale, array $catalogues, string $scope = ''): array => null !== $this->overrides
? array_map(
static fn (TranslationOverride $override): array => ['key' => $override->getKey(), 'catalogue' => $override->getCatalogue(), 'value' => $override->getValue()],
array_merge(...array_map(fn (string $catalogue): array => ($this->overrides)($locale, $catalogue, $scope), array_values(array_filter($catalogues, is_string(...))))),
)
: []);
// The chain read behind the inherited values, served from the same per-test rows.
$this->overrideRepository->method('findInheritable')->willReturnCallback(function (array $locales, string $scope, ?array $catalogues = null): array {
Expand Down
Loading