Разработка системы управления переводами (i18n) сайта
Система управления переводами — необходима для мультиязычных сайтов, где нужно регулярно добавлять и обновлять тексты на нескольких языках без редактирования кода или JSON-файлов вручную. Переводчики работают в удобном интерфейсе, разработчики добавляют новые ключи через код.
Варианты хранения переводов
Файловый подход (Laravel lang, i18next) — переводы в PHP/JSON-файлах. Минусы: редактирование требует доступа к файловой системе, нет истории изменений, сложно для нетехнических переводчиков.
Database-подход — переводы в БД. Редакторы работают через UI, история изменений, возможность переводить без деплоя. Это рекомендуемый подход для команд с переводчиками.
Гибридный — перевод в файлах как fallback, переопределение через БД.
Модель данных
translation_keys (
id, key, -- 'checkout.button.pay', 'nav.home'
namespace, -- 'frontend', 'emails', 'admin'
description, -- подсказка для переводчика
created_at
)
translations (
id, key_id, locale,
value, -- переведённый текст
status: pending | approved | needs_review,
translated_by, approved_by,
created_at, updated_at
)
translation_change_log (
id, translation_id, old_value, new_value, changed_by, changed_at
)
Сбор непереведённых ключей
В коде используются ключи через __() или t(). Artisan-команда сканирует код и добавляет новые ключи в БД:
class ScanTranslationKeys extends Command
{
public function handle(): void
{
$files = File::allFiles(resource_path('views'))
->merge(File::allFiles(resource_path('js')));
$keys = [];
foreach ($files as $file) {
$content = File::get($file);
preg_match_all("/__\('([^']+)'\)/", $content, $matches);
preg_match_all("/t\('([^']+)'\)/", $content, $jsMatches);
$keys = array_merge($keys, $matches[1], $jsMatches[1]);
}
$unique = array_unique($keys);
$existing = TranslationKey::pluck('key')->toArray();
$new = array_diff($unique, $existing);
foreach ($new as $key) {
TranslationKey::create(['key' => $key, 'namespace' => $this->guessNamespace($key)]);
}
$this->info("Найдено новых ключей: " . count($new));
}
}
Интерфейс переводчика
Основной экран — таблица с фильтрами:
| Фильтры | Колонки |
|---|---|
| Namespace | Ключ |
| Язык | Оригинал (ru) |
| Статус | Текущий перевод |
| Поиск по ключу/тексту | Действие: редактировать |
Форма редактирования — инлайн в таблице или боковая панель. Рядом с полем перевода — оригинальный текст на базовом языке. Кнопка "Машинный перевод" делает запрос к DeepL или Google Translate API для черновика.
API машинного перевода
class DeepLTranslationService
{
public function translate(string $text, string $targetLang, string $sourceLang = 'RU'): string
{
$response = Http::withToken(env('DEEPL_API_KEY'))
->post('https://api-free.deepl.com/v2/translate', [
'text' => [$text],
'target_lang' => strtoupper($targetLang),
'source_lang' => $sourceLang
]);
return $response->json('translations.0.text');
}
}
Кеширование переводов
Переводы из БД кешируются в Redis. При изменении — инвалидация только затронутого namespace:
class Translation extends Model
{
protected static function booted(): void
{
static::saved(fn($t) => Cache::forget("translations:{$t->locale}:{$t->key->namespace}"));
}
}
Экспорт/импорт для переводческих агентств
Для работы с агентствами — экспорт в XLIFF (стандартный формат для CAT-инструментов) или Excel:
// Экспорт непереведённых ключей в Excel
$untranslated = TranslationKey::whereDoesntHave('translations', fn($q) =>
$q->where('locale', $targetLocale)
)->get();
$export = $untranslated->map(fn($key) => [
'key' => $key->key,
'source' => $key->translations->where('locale', 'ru')->first()?->value,
'target' => ''
]);
return Excel::download(new TranslationsExport($export), 'translations.xlsx');
Процесс добавления нового языка
- Добавить locale в
config/app.phpи запуститьphp artisan translations:scan - В интерфейсе переводчика выбрать новый язык — показаны все непереведённые ключи
- Перевести вручную или через машинный перевод → проверить → утвердить
- Включить язык в language switcher сайта
Срок разработки: 4–6 недель для полной системы с интерфейсом переводчиков, машинным переводом, историей изменений и экспортом/импортом.







