Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add OpenAI integration #9

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/.idea
/vendor
composer.lock
.DS_Store
67 changes: 56 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,67 @@ Did you buy a Laravel script and it doesn't have your language in the`lang`file?
Do you want to make your web application bilingual, but you don't know how to translate all those words?</br>
Do you not have the possibility to use the JSON format that many Laravel language translation packages give you?


# Installation
```
composer require alisalehi/laravel-lang-files-translator
```

# 💎Usage
After installation, publish the configuration file:
```
php artisan vendor:publish --tag=lang-files-translator-config
```

# Configuration

The package supports two translation providers:
- Google Translate (default)
- OpenAI (requires API key)

To use OpenAI for translations, set your API key in `.env`:
```
php artisan translate:lang {from} {to}
OPENAI_API_KEY=your-api-key
```
for example, your locale is English and you have en lang files and want to have these files to Persian(fa) lang too.
just enough to run:

# 💎Usage

The package provides a simple artisan command to translate your language files:

```bash
php artisan translate:lang {from} {to} [options]
```

## Available Options

### Using Google Translate (Default)
```bash
php artisan translate:lang en fa
```
and done!
Go to lang/fa and you will see all the translated files from the en folder.
No configuration needed, just run the command.

### Using OpenAI
First, add your OpenAI API key to `.env`:
```
OPENAI_API_KEY=your-api-key
```

Then run the command with OpenAI options:
```bash
php artisan translate:lang en fa --provider=openai --model=gpt-3.5-turbo
```

how to use video ⤵️
## Command Reference

### Arguments:
- `from`: Source language code (e.g., en)
- `to`: Target language code (e.g., fa)

### Options:
- `--provider`: Translation provider (google or openai)
- `--model`: OpenAI model name (required when using openai)

After running the command, translated files will be created in the `lang/{target-language}` folder.

Comment on lines +21 to +75
Copy link
Owner

@alisalehi1380 alisalehi1380 Feb 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

اینو بزار اینجا.
کلا زمانی که زیاد توضیح میدی طرف فکر میکنه خیلی سخته کارکردن با پکیج شما.
تمام توضیحاتی که دادی رو باید طرف با کامند php artisan translate:lang -help در بیاره.

نکته آخر: به طرف اجازه نده فکر کنه. اون دلش میخواد فقط کامند ها رو کپی پیست کنه :)

publish the configuration:

php artisan vendor:publish --tag=lang-files-translator-config

💎Usage

Google Translate (Default)

php artisan translate:lang {from} {to}

OpenAI

set your API key in .env:

OPENAI_API_KEY=your-api-key

Then run the command with OpenAI options:

php artisan translate:lang {from} {to} --provider=openai --model=gpt-3.5-turbo

## Demo Video ⤵️

https://github.com/alisalehi1380/laravel-lang-files-translator/assets/111766206/748eaba0-29a3-4782-8505-1d8368d44ed2

Expand All @@ -42,14 +84,17 @@ As Einstein said, **"There's a way to do it better!"** So I welcome any change t

Just open an issue or pull a request.


## License
The MIT License (MIT). See **[License File](https://github.com/alisalehi1380/laravel-lang-files-translator/blob/master/LICENSE)** for more information.

## ❤️Contributing
This project exists thanks to all the people who
contribute. [CONTRIBUTING](https://github.com/alisalehi/laravel-lang-files-translator/graphs/contributors)
This project exists thanks to all the people who contribute:

- [Ali Salehi](https://github.com/alisalehi1380) - Original author and maintainer
- [Amirmohammad Mokhtari](https://github.com/am-mokhtari)
- [Navid Mirzaaghazadeh](https://github.com/mirzaaghazadeh) - Implemented OpenAI integration for natural language translation
Copy link
Owner

@alisalehi1380 alisalehi1380 Feb 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

همون طوری که همه مون میدونیم از اپن سورس پولی در نمیاد. فقط اعتبار هست که بهمون میده.
در همین PR ی که زدی توضیحات خیلی تکمیل تری وجود داره ولی اگر خیلی مهمه برات که در README باشه که دقیقا چه فیچری رو اضافه کردی، بزار باشه.

منم کلا بردار. ما کاری نکردیم که. هر چی بوده زحمات شما و دوستان بوده.


See all [contributors](https://github.com/alisalehi1380/laravel-lang-files-translator/graphs/contributors).

[img-package]: https://banners.beyondco.de/laravel-lang-files-translator%20.png?theme=dark&packageManager=composer+require&packageName=alisalehi%2Flaravel-lang-files-translator&pattern=fourPointStars&style=style_1&description=Easiest+way+to+translate+lang+files&md=1&showWatermark=0&fontSize=100px&images=translate
[ico-laravel]: https://img.shields.io/packagist/dependency-v/alisalehi/laravel-lang-files-translator/laravel/framework.svg?color=%23f13c2f
Expand Down
16 changes: 16 additions & 0 deletions config/lang-files-translator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

return [
/*
|--------------------------------------------------------------------------
| OpenAI Configuration
|--------------------------------------------------------------------------
|
| Configure OpenAI API settings for translation when using OpenAI provider
|
*/
'chatgpt' => [
'api_key' => env('OPENAI_API_KEY'),
'temperature' => 0.3, // Controls translation creativity (0.0 = precise, 1.0 = creative)
],
];
25 changes: 21 additions & 4 deletions src/Commands/Translate.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,36 @@

class Translate extends Command
{
protected $signature = 'translate:lang {from : translate from language} {to : translate to language}';
protected $signature = 'translate:lang {from : translate from language} {to : translate to language}
{--provider=google : translation provider (google or openai)}
{--model= : OpenAI model name (required when provider is openai)}';
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

برای راحتی استفاده، امکانش هست یه مدل پیشفرض داشته باشیم؟ اگر کاربر خواست تغییرش بده.

تو README دیدم نوشته بودی. اگه صلاح میدونی که خوبه، اینجا دیفالت همون رو بزار.


protected $description = 'translate lang files';

public function handle(TranslateService $translateService)
{
$this->info('start translation. please wait...');
$provider = $this->option('provider');
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

مدل رو هم بگیر

$model = $this->option('model');

if (!in_array($provider, ['google', 'openai'])) {
$this->error('Invalid provider. Use either "google" or "openai"');
return 1;
}

if ($provider === 'openai' && empty($this->option('model'))) {
$this->error('Model name is required when using OpenAI provider');
return 1;
}

$this->info('Start translation using ' . strtoupper($provider) . ' provider...');
$this->info(PHP_EOL . 'The speed of translation of files depends on the speed of the Internet,');
$this->info('the number of file lines and the indentation of each key.');
$this->info('So please be patient until the translation of the files is finished.');
$this->info('Thankful');

$translateService->to($this->argument('to'))->from($this->argument('from'))->translate();
$translateService
->to($this->argument('to'))
->from($this->argument('from'))
->withProvider($provider, $this->option('model'))
->translate();

$this->getOutput()->writeln(PHP_EOL . ' - Finished translation! (go to lang/' . $this->argument('to') . ' folder) ');

Expand All @@ -37,4 +54,4 @@ public function thanks(): void
$this->line('<fg=blue>|-------------------------------------------------|</>');
$this->line('https://github.com/alisalehi1380/laravel-lang-files-translator');
}
}
}
10 changes: 10 additions & 0 deletions src/Contracts/TranslatorInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Alisalehi\LaravelLangFilesTranslator\Contracts;

interface TranslatorInterface
{
public function setSource(string $source): self;
public function setTarget(string $target): self;
public function translate(string $text): string;
}
11 changes: 9 additions & 2 deletions src/LangFilesTranslatorServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,20 @@ class LangFilesTranslatorServiceProvider extends ServiceProvider

public function register(): void
{

$this->mergeConfigFrom(
__DIR__ . '/../config/lang-files-translator.php',
'lang-files-translator'
);
}

public function boot(): void
{
if ($this->app->runningInConsole()) {
$this->commands(self::$commandNames);

$this->publishes([
__DIR__ . '/../config/lang-files-translator.php' => config_path('lang-files-translator.php'),
], 'lang-files-translator-config');
}
}
}
}
32 changes: 21 additions & 11 deletions src/Services/TranslateService.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
namespace Alisalehi\LaravelLangFilesTranslator\Services;

use Illuminate\Support\Facades\File;
use Stichoza\GoogleTranslate\GoogleTranslate;
use Alisalehi\LaravelLangFilesTranslator\Contracts\TranslatorInterface;
use Alisalehi\LaravelLangFilesTranslator\Services\TranslatorFactory;
use Symfony\Component\Finder\SplFileInfo;

class TranslateService
{
private string $translate_from;
private string $translate_to;
private string $provider = 'google';
private ?string $model = null;

//setters
public function from(string $from): TranslateService
Expand All @@ -23,6 +26,13 @@ public function to(string $to): TranslateService
$this->translate_to = $to;
return $this;
}

public function withProvider(string $provider, ?string $model = null): TranslateService
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

بشه setProvider

{
$this->provider = $provider;
$this->model = $model;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

به نظرم دیفالت گذاشتن اینها خیلی منطقی نیست. خطوط 14 و 15

در اینجا

$translateService
            ->to($this->argument('to'))
            ->from($this->argument('from'))
            ->withProvider($provider, $this->option('model'))
            ->translate();

مقادیر provider و model رو داریم پاس میدیم. در خطوط بالایی هم تمام ولیدیشن هایی که برای این دو فیلد نیاز بوده، چک شده. پس اینجا قطعا این مقادیر وجود دارند.

دیفالت null رو هم دیگه نیازی بهش نداریم.

public function setProvider(string $provider, ?string $model): TranslateService

return $this;
}

public function translate(): void
{
Expand Down Expand Up @@ -60,30 +70,30 @@ private function getTranslatedData(SplFileInfo $file): string
return $this->addPhpSyntax($translatedData);
}

private function setUpGoogleTranslate(): GoogleTranslate
private function getTranslator(): TranslatorInterface
{
$google = new GoogleTranslate();
return $google->setSource($this->translate_from)
return TranslatorFactory::create($this->provider, $this->model)
->setSource($this->translate_from)
->setTarget($this->translate_to);
}

private function translateLangFiles(array $content): array
{
$google = $this->setUpGoogleTranslate();

if (empty($content))
if (empty($content)) {
return [];
}

return $this->translateRecursive($content, $google);
$translator = $this->getTranslator();
return $this->translateRecursive($content, $translator);
}

private function translateRecursive($content, $google) : array
private function translateRecursive($content, TranslatorInterface $translator): array
{
$trans_data = [];

foreach ($content as $key => $value) {
if (is_array($value)) {
$trans_data[$key] = $this->translateRecursive($value, $google);
$trans_data[$key] = $this->translateRecursive($value, $translator);
continue;
}

Expand All @@ -96,7 +106,7 @@ private function translateRecursive($content, $google) : array
)
: $value;

$translatedValue = $google->translate($modifiedValue);
$translatedValue = $translator->translate($modifiedValue);

$trans_data[$key] = $hasProps
? str_replace(['{', '}'], '', $translatedValue)
Expand Down
28 changes: 28 additions & 0 deletions src/Services/TranslatorFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

namespace Alisalehi\LaravelLangFilesTranslator\Services;

use Alisalehi\LaravelLangFilesTranslator\Contracts\TranslatorInterface;
use Alisalehi\LaravelLangFilesTranslator\Services\Translators\ChatGPTTranslator;
use Alisalehi\LaravelLangFilesTranslator\Services\Translators\GoogleTranslator;

class TranslatorFactory
{
public static function create(string $provider = 'google', ?string $model = null): TranslatorInterface
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

{
if ($provider === 'openai') {
if (empty($model)) {
throw new \InvalidArgumentException('Model parameter is required for OpenAI provider');
}
Comment on lines +14 to +16
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

نیازی به چک کردن نیست. قبلا چک شده و اینجا حتما هستش

$translator = new ChatGPTTranslator();
$translator->setModel($model);
return $translator;
}

if ($provider === 'google') {
return new GoogleTranslator();
}

throw new \InvalidArgumentException("Unsupported translator provider: {$provider}");
}
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

به نظرم داریم لقمه رو می‌پیچونیم :)

نظرت چیه تو همون getTranslator() هندل کنیم؟
https://github.com/alisalehi1380/laravel-lang-files-translator/pull/9/files#diff-6af0fbb9775aba1ee4a457d2ad8e49e9984b900ddc793ffc61af873ff7473ccdR73

اگه بخواییم خیلی وسواسی بریم جلو اینم باید با singleton پیاده سازیش کنیم که مطمئن باشیم در طول اجرا فقط و فقط یک instance وجود داره. ولی همین اوکیه. نیازی نیست تغییرش بدی. 🙏

70 changes: 70 additions & 0 deletions src/Services/Translators/ChatGPTTranslator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

namespace Alisalehi\LaravelLangFilesTranslator\Services\Translators;

use Alisalehi\LaravelLangFilesTranslator\Contracts\TranslatorInterface;
use Illuminate\Support\Facades\Http;

class ChatGPTTranslator implements TranslatorInterface
{
private string $source;
private string $target;
private string $apiKey;
private string $model;
private float $temperature;

public function __construct()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

میتونیم از Constructor property promotion ورژن 8 استفاده کنیم و این طوری بنویسیم.

public function __construct(
        private string $source,
        private string $target,
        private string $model,
        private string $apiKey,
        private float $temperature
    ) {

{
$this->apiKey = config('lang-files-translator.chatgpt.api_key');
$this->temperature = config('lang-files-translator.chatgpt.temperature', 0.3);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

به نظرم چون توی کانفیگ نوشتی، از اینجا برش دار.

config('lang-files-translator.chatgpt.temperature')


if (empty($this->apiKey)) {
throw new \RuntimeException('OpenAI API key is not configured. Set OPENAI_API_KEY in your .env file.');
}
}

public function setModel(string $model): self
{
$this->model = $model;
return $this;
}

public function setSource(string $source): TranslatorInterface
{
$this->source = $source;
return $this;
}

public function setTarget(string $target): TranslatorInterface
{
$this->target = $target;
return $this;
}

public function translate(string $text): string
{
$response = Http::withHeaders([
'Authorization' => 'Bearer ' . $this->apiKey,
'Content-Type' => 'application/json',
])->post('https://api.openai.com/v1/chat/completions', [
'model' => $this->model,
'messages' => [
[
'role' => 'system',
'content' => "You are a professional translator. Translate from {$this->source} to {$this->target}. Maintain any variables or placeholders in the text. Only return the translated text without explanations or additional context.",
],
[
'role' => 'user',
'content' => $text,
],
],
'temperature' => $this->temperature,
]);

if (!$response->successful()) {
throw new \RuntimeException('ChatGPT API request failed: ' . $response->body());
}

return trim($response->json('choices.0.message.content'));
}
}
Loading