PHP: Yandex Translate API: API Key + Folder ID и чистый PHP-класс

Подключаем перевод текста с помощью 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 key
    • YANDEX_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
Просмотров: 1k GitHub
Материал помог? Оценка помогает находить устаревшие и спорные решения.

Комментарии

0

Нашли ошибку или хотите дополнить материал?

Комментариев пока нет.Можно первым дополнить материал или сообщить об ошибке.
Telegram