Skip to content

Commit 9af8a04

Browse files
Squashed 'packages/laravel-translations/' changes from 39dcba0..098fcdf
098fcdf Merge pull request #21 from backstagephp/feature/translation-rules c79b689 Merge branch 'main' into feature/translation-rules 12d989c Fix styling 71331d3 Update prism-php/prism requirement from ^0.97 to ^0.98 51b100b Fix styling b72f64d Use facade a1c7496 Use php 8.3, Laravel 12.x c2257f0 Fix styling f08fed0 Fix wrong conditional 2816f08 Update migration 2d7b4da Remove debugging b4bf7d3 Optimize getTextualRulesQuery 7cd672e Update language rule condition b00b9eb Remove language condition observer d01e401 Fix styling 3bd6cfc Remove enums f5853a2 wip b20dd8d wip 72ff6e6 Fix styling 2df09a5 Update translation rules bbbe1d4 Add language relation b964f49 Use facade 0828bdf wip b6cfcbe Update value structure b8fc8d6 Create observer for multiple values 82c20e4 Rename cases 893bddb Format 7ef0af6 Fix styling 84c1a6a Use facade b960060 Update prism vendor c3dd628 Fix styling 7932282 Init translation rules git-subtree-dir: packages/laravel-translations git-subtree-split: 098fcdfd3f4a09403bb67c1e56b294edfb7485d8
1 parent 8c20002 commit 9af8a04

File tree

6 files changed

+207
-2
lines changed

6 files changed

+207
-2
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
use Illuminate\Database\Migrations\Migration;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Support\Facades\Schema;
6+
7+
return new class extends Migration
8+
{
9+
public function up()
10+
{
11+
Schema::create('language_rules', function (Blueprint $table) {
12+
$table->id();
13+
14+
$table->string('code', 5);
15+
16+
$table->string('name');
17+
18+
$table->longText('global_instructions')->nullable();
19+
20+
$table->timestamps();
21+
$table->softDeletes();
22+
});
23+
24+
Schema::create('language_rules_conditions', function (Blueprint $table) {
25+
$table->id();
26+
27+
$table->foreignId('language_rule_id')->constrained('language_rules');
28+
29+
$table->string('key');
30+
31+
$table->string('type'); // must, must_not
32+
33+
$table->json('value');
34+
35+
$table->timestamps();
36+
});
37+
}
38+
39+
public function down()
40+
{
41+
Schema::dropIfExists('language_rules_conditions');
42+
43+
Schema::dropIfExists('language_rules');
44+
}
45+
};

packages/laravel-translations/src/Drivers/AITranslator.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@
33
namespace Backstage\Translations\Laravel\Drivers;
44

55
use Backstage\Translations\Laravel\Contracts\TranslatorContract;
6+
use Backstage\Translations\Laravel\Models\Language;
67
use Prism\Prism\Facades\Prism;
78

89
class AITranslator implements TranslatorContract
910
{
1011
public function translate(string | array $text, string $targetLanguage, ?string $extraPrompt = null): string | array
1112
{
13+
$translationRules = Language::query()->where('code', $targetLanguage)->first()->getTextualRulesQuery();
14+
1215
if (is_array($text)) {
13-
return $this->translateJson($text, $targetLanguage, $extraPrompt);
16+
return $this->translateJson($text, $targetLanguage, $extraPrompt."\n\n".$translationRules);
1417
}
1518

1619
$systemPromptLines = [
@@ -78,8 +81,12 @@ public function translate(string | array $text, string $targetLanguage, ?string
7881

7982
$systemPrompt = implode("\n", $systemPromptLines);
8083

84+
$systemPrompt = '<translation-system-prompt>'.$systemPrompt.'</translation-system-prompt>';
85+
8186
$instructionsString = implode("\n", $instructions);
8287

88+
$prompt = '<translation-instructions>'.$instructionsString.'</translation-instructions>'."\n\n".$translationRules;
89+
8390
$response = Prism::text()
8491
->withClientOptions([
8592
'timeout' => 600,
@@ -91,7 +98,7 @@ public function translate(string | array $text, string $targetLanguage, ?string
9198
->withClientRetry(4, 100)
9299
->using(config('translations.translators.drivers.ai.provider'), config('translations.translators.drivers.ai.model'))
93100
->withSystemPrompt($systemPrompt)
94-
->withPrompt($instructionsString)
101+
->withPrompt($prompt)
95102
->asText();
96103

97104
return trim($response->text);

packages/laravel-translations/src/Models/Language.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ public function translations(): HasMany
6262
return $this->hasMany(Translation::class, 'code', 'code');
6363
}
6464

65+
public function languageRules(): HasMany
66+
{
67+
return $this->hasMany(LanguageRule::class, 'code', 'code');
68+
}
69+
6570
public function getLanguageCodeAttribute()
6671
{
6772
return explode('-', $this->attributes['code'])[0];
@@ -72,6 +77,40 @@ public function getCountryCodeAttribute()
7277
return explode('-', $this->attributes['code'])[1];
7378
}
7479

80+
public function getTextualRulesQuery(): string
81+
{
82+
if (! $this->languageRules()->exists()) {
83+
return '';
84+
}
85+
86+
$baseRules = <<<'HTML'
87+
<translation-rules-query-base-rules>
88+
Important: Always treat singular, plural, diminutives, and common English equivalents as identical before translating.
89+
For example: 'Worst', 'worstjes', 'Sausage', 'Sausages' are all equivalent.
90+
Also: 'Huis', 'Huisje', 'House' are all equivalent.
91+
And: 'Kaas', 'Kaasje', 'Cheese' are all equivalent.
92+
93+
Apply all translation rules after this normalization.
94+
If a rule states a text *must not* match or translate to something, avoid that.
95+
If it *must* match or translate to something, enforce that.
96+
Multiple values follow the same logic.
97+
If a translation *must be* a certain text, it cannot equal the source.
98+
</translation-rules-query-base-rules>
99+
HTML;
100+
101+
$this->load('languageRules');
102+
103+
$rules = $this
104+
->languageRules
105+
->map(function (LanguageRule $languageRule) {
106+
return '<translation-rules-query>'.$languageRule->getTextualQuery().'</translation-rules-query>';
107+
})
108+
->filter()
109+
->implode("\n");
110+
111+
return $baseRules."\n\n".$rules;
112+
}
113+
75114
public function getLocalizedCountryNameAttribute($locale = null)
76115
{
77116
$code = strtolower(explode('-', $this->attributes['code'])[1] ?? $this->attributes['code']);
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
namespace Backstage\Translations\Laravel\Models;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
use Illuminate\Database\Eloquent\SoftDeletes;
7+
8+
class LanguageRule extends Model
9+
{
10+
use SoftDeletes;
11+
12+
protected $table = 'language_rules';
13+
14+
protected $fillable = [
15+
'code',
16+
'name',
17+
'global_instructions',
18+
];
19+
20+
public function conditions()
21+
{
22+
return $this->hasMany(LanguageRuleCondition::class);
23+
}
24+
25+
public function language()
26+
{
27+
return $this->belongsTo(Language::class, 'code', 'code');
28+
}
29+
30+
public function getTextualQuery(): string
31+
{
32+
$text = '';
33+
34+
if ($this->global_instructions) {
35+
$text .= "\n<global-instructions>".str($this->global_instructions)->stripTags()->toString().'</global-instructions>';
36+
}
37+
38+
$this->load('conditions')->conditions->each(function (LanguageRuleCondition $condition) use (&$text) {
39+
if ($query = $condition->getTextualQuery()) {
40+
$text .= "\n<translation-rules-query-condition-subquery>".$query.'</translation-rules-query-condition-subquery>';
41+
}
42+
});
43+
44+
return '<translation-rules-query-conditions>'.trim($text).'</translation-rules-query-conditions>';
45+
}
46+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
namespace Backstage\Translations\Laravel\Models;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
7+
class LanguageRuleCondition extends Model
8+
{
9+
protected $table = 'language_rules_conditions';
10+
11+
protected $fillable = [
12+
'language_rule_id',
13+
'key',
14+
'type',
15+
'value',
16+
];
17+
18+
protected $casts = [
19+
'type' => 'string',
20+
'value' => 'array',
21+
];
22+
23+
public function languageRule()
24+
{
25+
return $this->belongsTo(LanguageRule::class);
26+
}
27+
28+
public function getTextualQuery(): string
29+
{
30+
$key = $this->key;
31+
32+
if ($this->type !== 'must' && $this->type !== 'must_not') {
33+
return '';
34+
}
35+
36+
$values = $this->value ?? [];
37+
38+
if (empty($values) || ! is_array($values)) {
39+
report(new \Exception('Value is null or empty for key: '.$key.' and type: '.$this->type.' and id: '.$this->id));
40+
41+
return '';
42+
}
43+
44+
$resultingQuery = [];
45+
$resultingQuery[] = 'The following rules must be applied while translating the text:';
46+
47+
if (count($values) === 1) {
48+
if ($this->type === 'must') {
49+
$resultingQuery[] = "{$key} must translate to '{$values[0]}'";
50+
}
51+
52+
if ($this->type === 'must_not') {
53+
$resultingQuery[] = "{$key} must not translate to '{$values[0]}'";
54+
}
55+
} else {
56+
if ($this->type === 'must') {
57+
$resultingQuery[] = "{$key} must translate to one of these options: '".implode("', '", $values)."'";
58+
}
59+
60+
if ($this->type === 'must_not') {
61+
$resultingQuery[] = "{$key} must not translate to any of these options: '".implode("', '", $values)."'";
62+
}
63+
}
64+
65+
return implode("\n", $resultingQuery);
66+
}
67+
}

packages/laravel-translations/src/TranslationServiceProvider.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public function configurePackage(Package $package): void
2929
'create_languages_table',
3030
'create_translations_table',
3131
'create_translated_attributes_table',
32+
'create_language_rules_tables',
3233
)
3334
->hasConfigFile('translations')
3435
->hasCommands(

0 commit comments

Comments
 (0)