Подключаем перевод текста с помощью Yandex Translate (Yandex Cloud) через Api-Key и folderId — без IAM-токенов.
Как подключить:
- В Yandex Cloud Console проверь, что биллинг активен (ACTIVE/TRIAL_ACTIVE).
- Создай/выбери Folder и скопируй Folder ID (виден в UI или в URL вида
/folders/<folder_ID>). - IAM → Service accounts: создай Service Account в этом Folder.
- Выдай сервис-аккаунту роль на папку для Translate:
ai.translate.user(или выше). - В сервис-аккаунте: Create new key → Create API key, scope:
yc.ai.translate.execute. - Сохрани ключ сразу (потом его не покажут).
- На сервере задай переменные окружения:
YANDEX_API_KEY= API keyYANDEX_FOLDER_ID= Folder ID
class translate
{
const TRANSLATE_URL = 'https://translate.api.cloud.yandex.net/translate/v2/translate';
private $YandexApiKey = '';
private $YandexFolderId = '';
private $Timeout = 15;
private $ConnectTimeout = 8;
private $MaxRetries = 2; // 0 = без повторов
private $RetryBaseDelayMs = 300; // базовая задержка перед повтором (экспоненциально)
private $UserAgent = 'php-translate/1.0';
private $CaBundlePath = ''; // опционально: путь к CA bundle (если нужно явно)
public function __construct(array $Options = array())
{
$this->LoadCredentials($Options);
if (isset($Options['Timeout'])) {
$this->Timeout = (int)$Options['Timeout'];
}
if (isset($Options['ConnectTimeout'])) {
$this->ConnectTimeout = (int)$Options['ConnectTimeout'];
}
if (isset($Options['MaxRetries'])) {
$this->MaxRetries = (int)$Options['MaxRetries'];
}
if (isset($Options['RetryBaseDelayMs'])) {
$this->RetryBaseDelayMs = (int)$Options['RetryBaseDelayMs'];
}
if (isset($Options['UserAgent'])) {
$this->UserAgent = trim((string)$Options['UserAgent']);
}
if (isset($Options['CaBundlePath'])) {
$this->CaBundlePath = trim((string)$Options['CaBundlePath']);
}
}
private function LoadCredentials(array $Options)
{
$ApiKey = isset($Options['ApiKey']) ? trim((string)$Options['ApiKey']) : '';
$FolderId = isset($Options['FolderId']) ? trim((string)$Options['FolderId']) : '';
if ($ApiKey === '') {
$EnvApiKey = getenv('YANDEX_API_KEY');
if ($EnvApiKey !== false) {
$ApiKey = trim((string)$EnvApiKey);
}
}
if ($FolderId === '') {
$EnvFolderId = getenv('YANDEX_FOLDER_ID');
if ($EnvFolderId !== false) {
$FolderId = trim((string)$EnvFolderId);
}
}
$this->YandexApiKey = $ApiKey;
$this->YandexFolderId = $FolderId;
}
public function setCredentials($ApiKey, $FolderId)
{
$this->YandexApiKey = trim((string)$ApiKey);
$this->YandexFolderId = trim((string)$FolderId);
}
public function get($Text, $LangOriginal = 'ru', $LangTarget = 'en', $Format = 'PLAIN_TEXT')
{
return $this->getBatch(array($Text), $LangOriginal, $LangTarget, $Format);
}
public function getBatch(array $Texts, $LangOriginal = 'ru', $LangTarget = 'en', $Format = 'PLAIN_TEXT')
{
$LangOriginal = (string)$LangOriginal;
$LangTarget = (string)$LangTarget;
$Format = (string)$Format;
if ($this->YandexApiKey === '' || $this->YandexFolderId === '') {
return $this->Err('NO_CREDENTIALS', 'Missing Yandex credentials (ApiKey/FolderId)', 0, null, null);
}
$Texts = $this->NormalizeTexts($Texts);
if (!is_array($Texts) || count($Texts) === 0) {
return $this->Err('EMPTY_TEXT', 'Texts are empty', 0, null, null);
}
$Payload = array(
'sourceLanguageCode' => $LangOriginal,
'targetLanguageCode' => $LangTarget,
'format' => $Format,
'texts' => $Texts,
'folderId' => $this->YandexFolderId,
);
$Headers = array(
'Content-Type: application/json',
'Authorization: Api-Key ' . $this->YandexApiKey,
'User-Agent: ' . ($this->UserAgent !== '' ? $this->UserAgent : 'php-translate/1.0'),
);
return $this->RequestWithRetry(self::TRANSLATE_URL, $Payload, $Headers);
}
private function NormalizeTexts(array $Texts)
{
$Out = array();
foreach ($Texts as $t) {
if (!is_scalar($t)) {
continue;
}
$s = trim((string)$t);
if ($s === '') {
continue;
}
$Out[] = $s;
}
return $Out;
}
private function RequestWithRetry($Url, array $Payload, array $Headers)
{
$Try = 0;
$LastError = null;
while (true) {
$Try++;
$Res = $this->DoRequest($Url, $Payload, $Headers);
if (!empty($Res->ok)) {
return $Res;
}
$LastError = $Res;
if ($Try > (1 + max(0, $this->MaxRetries))) {
return $LastError;
}
if (!$this->IsRetryable($Res)) {
return $LastError;
}
$DelayMs = $this->ComputeBackoffMs($Try);
$this->SleepMs($DelayMs);
}
}
private function DoRequest($Url, array $Payload, array $Headers)
{
$Json = json_encode($Payload, JSON_UNESCAPED_UNICODE);
if ($Json === false) {
return $this->Err('BAD_JSON', 'Failed to encode JSON payload', 0, null, null);
}
$ch = curl_init($Url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $Headers);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $Json);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, max(1, (int)$this->Timeout));
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, max(1, (int)$this->ConnectTimeout));
curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
// SSL: безопасно по умолчанию
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
// Опционально: явный CA bundle (если окружение кривое/минимальное)
if ($this->CaBundlePath !== '' && is_file($this->CaBundlePath)) {
curl_setopt($ch, CURLOPT_CAINFO, $this->CaBundlePath);
}
$Body = curl_exec($ch);
$CurlErrNo = curl_errno($ch);
$CurlErr = curl_error($ch);
$HttpCode = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($Body === false || $CurlErrNo) {
return $this->Err('CURL_ERROR', $CurlErr !== '' ? $CurlErr : 'Unknown cURL error', $HttpCode, null, array(
'curlErrNo' => $CurlErrNo,
));
}
$Decoded = json_decode($Body);
if (!is_object($Decoded)) {
return $this->Err('BAD_JSON', 'Invalid JSON response', $HttpCode, array('raw' => $Body), null);
}
// HTTP ошибки
if ($HttpCode >= 400) {
return $this->ParseApiError($Decoded, $HttpCode, $Body);
}
// Нормальный ответ
$Decoded->ok = true;
$Decoded->httpCode = $HttpCode;
return $Decoded;
}
private function ParseApiError($Decoded, $HttpCode, $RawBody)
{
$Msg = isset($Decoded->message) ? (string)$Decoded->message : 'API error';
$Code = isset($Decoded->code) ? (string)$Decoded->code : 'API_ERROR';
// Немного "очеловечим" типовые ситуации (без привязки к точным кодам провайдера)
$LocalCode = 'API_ERROR';
if ($HttpCode == 401 || $HttpCode == 403) {
$LocalCode = 'AUTH_OR_PERMISSIONS';
} elseif ($HttpCode == 429) {
$LocalCode = 'RATE_LIMIT';
} elseif ($HttpCode >= 500) {
$LocalCode = 'UPSTREAM_5XX';
} elseif ($HttpCode == 400) {
$LocalCode = 'BAD_REQUEST';
}
$Extra = array(
'apiCode' => $Code,
'response' => $Decoded,
);
// В отладке полезно иметь сырой ответ (но осторожно с логами)
$Extra['raw'] = $RawBody;
return $this->Err($LocalCode, $Msg, $HttpCode, $Extra, null);
}
private function IsRetryable($ErrObj)
{
$HttpCode = isset($ErrObj->httpCode) ? (int)$ErrObj->httpCode : 0;
$Code = isset($ErrObj->code) ? (string)$ErrObj->code : '';
// Retry по HTTP
if ($HttpCode == 429) {
return true;
}
if ($HttpCode >= 500 && $HttpCode <= 599) {
return true;
}
if ($HttpCode == 408) {
return true;
}
// Retry по cURL ошибкам (сетевые/таймауты)
if ($Code === 'CURL_ERROR' && isset($ErrObj->meta) && is_object($ErrObj->meta) && isset($ErrObj->meta->curlErrNo)) {
$No = (int)$ErrObj->meta->curlErrNo;
// Частые "временные" коды:
// 6: Couldn't resolve host, 7: Failed to connect, 28: timeout, 52: empty reply, 56: recv error
if (in_array($No, array(6, 7, 28, 52, 56), true)) {
return true;
}
}
return false;
}
private function ComputeBackoffMs($Try)
{
$Base = max(50, (int)$this->RetryBaseDelayMs);
$Pow = $Try - 1;
if ($Pow < 0) {
$Pow = 0;
}
if ($Pow > 6) {
$Pow = 6;
}
$Delay = $Base * (int)pow(2, $Pow);
// jitter 0..250ms
$Jitter = 0;
if (function_exists('random_int')) {
$Jitter = random_int(0, 250);
} else {
$Jitter = mt_rand(0, 250);
}
$Delay = $Delay + $Jitter;
// ограничим, чтобы не ждать вечность
if ($Delay > 5000) {
$Delay = 5000;
}
return (int)$Delay;
}
private function SleepMs($Ms)
{
$Ms = (int)$Ms;
if ($Ms <= 0) {
return;
}
usleep($Ms * 1000);
}
private function Err($Code, $Message, $HttpCode, $ExtraArrayOrNull, $MetaArrayOrNull)
{
$Obj = new stdClass();
$Obj->ok = false;
$Obj->code = (string)$Code;
$Obj->message = (string)$Message;
$Obj->httpCode = (int)$HttpCode;
if (is_array($ExtraArrayOrNull)) {
$Obj->extra = (object)$ExtraArrayOrNull;
} elseif (is_object($ExtraArrayOrNull)) {
$Obj->extra = $ExtraArrayOrNull;
}
if (is_array($MetaArrayOrNull)) {
$Obj->meta = (object)$MetaArrayOrNull;
} elseif (is_object($MetaArrayOrNull)) {
$Obj->meta = $MetaArrayOrNull;
}
return $Obj;
}
}
Пример использования
1. Перевод одной строки (get):
putenv('YANDEX_API_KEY=YOUR_API_KEY');
putenv('YANDEX_FOLDER_ID=YOUR_FOLDER_ID');
$Tr = new translate(array(
'Timeout' => 15,
'ConnectTimeout' => 8,
'MaxRetries' => 2,
'RetryBaseDelayMs' => 300,
'UserAgent' => 'my-app/1.0',
));
$Res = $Tr->get('Привет мир', 'ru', 'en');
if (!empty($Res->ok)) {
echo $Res->translations[0]->text . PHP_EOL;
} else {
echo 'Error: ' . $Res->code . ' :: ' . $Res->message . ' (HTTP ' . $Res->httpCode . ')' . PHP_EOL;
}
2. Пакетный перевод (batch):
putenv('YANDEX_API_KEY=YOUR_API_KEY');
putenv('YANDEX_FOLDER_ID=YOUR_FOLDER_ID');
$Tr = new translate();
$Res = $Tr->getBatch(array(
'Первый текст',
'Второй текст',
'Третий текст',
), 'ru', 'de');
if (!empty($Res->ok)) {
foreach ($Res->translations as $i => $t) {
echo ($i + 1) . ') ' . $t->text . PHP_EOL;
}
} else {
echo 'Error: ' . $Res->code . ' :: ' . $Res->message . ' (HTTP ' . $Res->httpCode . ')' . PHP_EOL;
}
Краевые случаи
- Пустой текст/все элементы пустые →
EMPTY_TEXT. - Не заданы креды →
NO_CREDENTIALS. - 429 (лимиты) →
RATE_LIMITи будут повторы (еслиMaxRetries> 0). - 5xx →
UPSTREAM_5XXи будут повторы. - 401/403 →
AUTH_OR_PERMISSIONS(и повторы не делаем — это не временная ошибка). - Некорректный JSON от API →
BAD_JSON+extra.raw.
Исходники и установка
- Репозиторий на GitHub: da41b94c/yandex-translate-php
- Установка через Composer:
composer require da41b94c/yandex-translate-php - Примеры использования в репозитории:
/examples
Комментарии
0Нашли ошибку или хотите дополнить материал?