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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog.

## [3.1.0] - TBD

### Added
- Added adapter-based Lang provider support with `DeepL` and `Google Translate` adapters plus shared remote request/caching infrastructure (#533)

### Changed
- Refactored the Lang package to resolve adapter instances through `LangFactory` configuration and load file translations lazily on first use instead of preloading them during web boot (#533)
- **BREAKING:** Reshaped Lang configuration so `lang.default` now selects the adapter, locale fallback moved to `lang.default_locale`, and the unused `lang.enabled` toggle was removed (#533)
- **BREAKING:** Removed `Lang::isEnabled()` from the public Lang API because it no longer affected runtime behavior (#533)

## [3.0.3] - 2026-07-10

### Changed
Expand Down
9 changes: 3 additions & 6 deletions src/App/Traits/WebAppTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,12 @@ private function resolveRoute(): ?MatchedRoute
}

/**
* @throws LangException|ConfigException|DiException|BaseException|ReflectionException
* Resolve lang config and current locale before controller hooks run.
* @throws LangException|ConfigException|LoaderException|DiException|ReflectionException
*/
private function loadLanguage(): void
{
$lang = LangFactory::get();

if ($lang->isEnabled()) {
$lang->load();
}
LangFactory::get();
}

/**
Expand Down
131 changes: 131 additions & 0 deletions src/Lang/Adapters/DeepLAdapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<?php

declare(strict_types=1);

/**
* Quantum PHP Framework
* An open-source software development framework for PHP
* @link https://quantumphp.io
*/

namespace Quantum\Lang\Adapters;

use Quantum\Lang\Contracts\LangAdapterInterface;
use Quantum\Lang\Traits\RemoteAdapterTrait;
use Quantum\Lang\Exceptions\LangException;
use Quantum\App\Exceptions\BaseException;
use Quantum\HttpClient\HttpClient;
use ReflectionException;
use ErrorException;

class DeepLAdapter implements LangAdapterInterface
{
use RemoteAdapterTrait;

public const API_URL = 'https://api.deepl.com/v2/translate';

public const TIMEOUT = 30;

protected string $lang;

/**
* @param array<string, mixed> $params
*/
public function __construct(string $lang, array $params, ?HttpClient $httpClient = null)
{
$this->lang = $lang;
$this->params = $params;
$this->httpClient = $httpClient ?? new HttpClient();
}

public function setLang(string $lang): LangAdapterInterface
{
$this->lang = $lang;
return $this;
}

/**
* @param array<int|string, mixed>|string|null $params
* @throws BaseException|ReflectionException|ErrorException
*/
public function get(string $key, $params = null): string
{
$text = $this->buildSourceText($key, $params);

if ($this->shouldBypassProvider($key, $text)) {
return $text;
}

$cached = $this->getCachedTranslation('deepl', $text);

if ($cached !== null) {
return $cached;
}

$authKey = (string) ($this->params['auth_key'] ?? '');

if ($authKey === '') {
throw LangException::missingConfig('lang.deepl.auth_key');
}

$response = $this->sendRequest(
(string) ($this->params['api_url'] ?? self::API_URL),
$this->buildPayload($text),
[
'Authorization' => 'DeepL-Auth-Key ' . $authKey,
'Content-Type' => 'application/json',
],
[
CURLOPT_TIMEOUT => self::TIMEOUT,
]
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

$translation = $this->extractTranslation($response);

$this->setCachedTranslation('deepl', $text, $translation);

return $translation;
}

/**
* @throws LangException
*/
private function buildPayload(string $text): string
{
$payload = [
'text' => [$text],
'target_lang' => strtoupper($this->lang),
];

if (!empty($this->params['source_locale'])) {
$payload['source_lang'] = strtoupper((string) $this->params['source_locale']);
}

$payloadJson = json_encode($payload);

if ($payloadJson === false) {
throw LangException::payloadEncodingFailed('DeepL');
}

return $payloadJson;
}

/**
* @param mixed $response
* @throws LangException
*/
private function extractTranslation($response): string
{
if (
!is_object($response)
|| !isset($response->translations)
|| !is_array($response->translations)
|| !isset($response->translations[0]->text)
|| !is_string($response->translations[0]->text)
) {
throw LangException::invalidProviderResponse('DeepL');
}

return $response->translations[0]->text;
}
}
76 changes: 45 additions & 31 deletions src/Lang/Translator.php → src/Lang/Adapters/FileAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@
* @link https://quantumphp.io
*/

namespace Quantum\Lang;
namespace Quantum\Lang\Adapters;

use Quantum\Lang\Contracts\LangAdapterInterface;
use Quantum\Config\Exceptions\ConfigException;
use Quantum\Loader\Exceptions\LoaderException;
use Quantum\Lang\Exceptions\LangException;
Expand All @@ -18,23 +19,37 @@
use Dflydev\DotAccessData\Data;
use ReflectionException;

/**
* Class Translator
* @package Quantum\Lang
*/
class Translator
class FileAdapter implements LangAdapterInterface
{
protected string $lang;

/**
* @var array<string, mixed>
*/
protected array $params = [];

private ?Data $translations = null;

public function __construct(string $lang)
/**
* @param array<string, mixed> $params
*/
public function __construct(string $lang, array $params = [])
{
$this->lang = $lang;
$this->params = $params;
}

public function setLang(string $lang): LangAdapterInterface
{
if ($this->lang !== $lang) {
$this->lang = $lang;
$this->flush();
}

return $this;
}

/**
* Load translation files
* @throws LangException|LoaderException|ConfigException|DiException|BaseException|ReflectionException
*/
public function loadTranslations(): void
Expand Down Expand Up @@ -68,31 +83,14 @@ public function loadTranslations(): void
}

/**
* Load translations
* @param array<string> $files
* @throws ConfigException|DiException|BaseException|ReflectionException
* @param array<int|string, mixed>|string|null $params
*/
private function loadFiles(array $files): void
public function get(string $key, array|string $params = null): string
{
if ($this->translations === null) {
return;
$this->loadTranslations();
}

foreach ($files as $file) {
$fileName = fs()->fileName($file);

$this->translations->import([
$fileName => fs()->require($file),
]);
}
}

/**
* Get translation by key
* @param array<int|string, mixed>|string|null $params
*/
public function get(string $key, $params = null): string
{
if ($this->translations && $this->translations->has($key)) {
$message = $this->translations->get($key);
return $params ? _message($message, $params) : $message;
Expand All @@ -101,11 +99,27 @@ public function get(string $key, $params = null): string
return $key;
}

/**
* Reset translations
*/
public function flush(): void
{
$this->translations = null;
}

/**
* @param array<string> $files
* @throws ConfigException|DiException|BaseException|ReflectionException
*/
private function loadFiles(array $files): void
{
if ($this->translations === null) {
return;
}

foreach ($files as $file) {
$fileName = fs()->fileName($file);

$this->translations->import([
$fileName => fs()->require($file),
]);
}
}
}
Loading
Loading