Skip to main content

Translation (Đa ngôn ngữ)

Component Translation cung cấp hệ thống dịch đa ngôn ngữ với hỗ trợ namespace, số nhiều (pluralization), tham số động và nhiều driver lưu trữ. Locale được lưu qua Context nên an toàn trong môi trường coroutine (Swoole).

Cấu hình

File config/translation.php:

return [
// Driver: 'array', 'json', 'database'
'driver' => env('TRANSLATION_DRIVER', 'array'),

// Locale mặc định
'locale' => env('APP_LOCALE', 'vi'),

// Locale dự phòng (fallback)
'fallback' => env('APP_FALLBACK_LOCALE', 'en'),

// Danh sách locale hợp lệ
'available_locales' => ['vi', 'en'],

// Thư mục chứa file dịch
'paths' => [
base_path('lang'),
],

// Cấu hình driver database
'drivers' => [
'database' => [
'connection' => null, // null = dùng kết nối mặc định
'table' => 'translations',
],
],

// Cache translations
'cache' => [
'enabled' => env('TRANSLATION_CACHE', false),
'driver' => null, // null = dùng cache driver mặc định
'ttl' => 3600, // giây
'prefix' => 'trans:',
],
];

Cấu trúc File Dịch

Array Driver (PHP)

lang/
├── vi/
│ ├── messages.php
│ ├── validation.php
│ └── tour.php
└── en/
├── messages.php
├── validation.php
└── tour.php
// lang/vi/messages.php
return [
'welcome' => 'Chào mừng, :name!',
'greeting' => 'Xin chào :Name', // :Name → ucfirst
'errors' => [
'not_found' => 'Không tìm thấy :resource.',
],
];

JSON Driver

lang/
├── vi/
│ ├── messages.json
│ └── validation.json
└── en/
├── messages.json
└── validation.json
// lang/vi/messages.json
{
"welcome": "Chào mừng, :name!",
"errors": {
"not_found": "Không tìm thấy :resource."
}
}

Sử Dụng Cơ Bản

Helper function t()

// Bản dịch đơn giản
t('messages.welcome');

// Với tham số
t('messages.welcome', ['name' => 'Nguyễn Văn A']);
// → "Chào mừng, Nguyễn Văn A!"

// Key dạng namespace.key.subkey
t('messages.errors.not_found', ['resource' => 'Tour']);
// → "Không tìm thấy Tour."

// Override locale cho một lần dịch
t('messages.welcome', ['name' => 'John'], 'en');

// Lấy Translator instance (không truyền key)
$translator = t();

Qua Translator instance

use Vietiso\Core\Translation\Translator;

$translator = translator(); // helper function

$translator->get('messages.welcome', ['name' => 'An']);
$translator->has('messages.welcome');

Cú Pháp Key

Dạng keyNamespaceKey
messages.welcomemessageswelcome
validation.requiredvalidationrequired
tour.detail.titletourdetail.title
mymodule::errors.403mymoduleerrors.403
welcome (không có dấu chấm)messageswelcome

Key không tìm thấy sẽ trả về chính key đó thay vì lỗi.

Tham Số Động

Hai kiểu placeholder đều được hỗ trợ:

// File dịch
return [
'greeting' => 'Xin chào :name, bạn có :count thông báo.',
'info' => 'Xin chào {name}, bạn có {count} thông báo.',
];
t('messages.greeting', ['name' => 'An', 'count' => 5]);
// → "Xin chào An, bạn có 5 thông báo."

Biến thể hoa/thường

// File dịch
return [
'hello' => 'Xin chào :name, :NAME, :Name',
];

t('messages.hello', ['name' => 'an']);
// → "Xin chào an, AN, An"
PlaceholderKết quả
:namegiá trị gốc
:NAMEUPPERCASE
:NameUcfirst

Số Nhiều (Pluralization)

Dạng đơn giản (pipe |)

// File dịch
return [
'item' => 'Một sản phẩm|Nhiều sản phẩm',
];

t('messages.item', ['count' => 1]); // "Một sản phẩm"
t('messages.item', ['count' => 5]); // "Nhiều sản phẩm"

Dạng phạm vi tường minh

return [
'item' => '{0} Không có sản phẩm|{1} Một sản phẩm|[2,*] :count sản phẩm',
'booking' => '{0} Chưa có đặt phòng|{1} Có :count đặt phòng|[2,10] Có :count đặt phòng|[11,*] Hơn :count đặt phòng',
];

t('messages.item', ['count' => 0]); // "Không có sản phẩm"
t('messages.item', ['count' => 1]); // "Một sản phẩm"
t('messages.item', ['count' => 5]); // "5 sản phẩm"
t('messages.item', ['count' => 100]); // "100 sản phẩm"

Cú pháp phạm vi:

  • {0} — khớp chính xác số 0
  • {1} — khớp chính xác số 1
  • [2,10] — khớp từ 2 đến 10
  • [11,*] — khớp từ 11 trở lên

Count là array hoặc Countable

$items = [1, 2, 3, 4, 5];

t('messages.item', ['count' => $items]);
// count($items) = 5 → "5 sản phẩm"

Quy tắc số nhiều theo ngôn ngữ

MessageSelector tự động áp dụng quy tắc CLDR phù hợp với từng locale:

LocaleQuy tắc
vi, zh, ja, ko, th, idKhông có số nhiều (luôn dùng form đầu)
en, de, es, fr, it, ...2 dạng: 1 / khác
ru, uk, hr, sr3 dạng: 1 / 2-4 / còn lại
ar6 dạng theo CLDR tiếng Ả-rập
pl3 dạng theo quy tắc Ba Lan

Quản Lý Locale

$translator = translator();

// Lấy locale hiện tại (từ Context của coroutine)
$locale = $translator->getLocale(); // 'vi'

// Đổi locale (chỉ ảnh hưởng coroutine hiện tại)
$translator->setLocale('en');

// Locale dự phòng
$translator->getFallback(); // 'en'
$translator->setFallback('en');

// Kiểm tra locale hợp lệ
$translator->isValidLocale('fr'); // false (nếu không có trong available_locales)
$translator->getAvailableLocales(); // ['vi', 'en']

Locale được lưu trong Context (per-coroutine), đảm bảo mỗi request trong Swoole không ảnh hưởng nhau.

Namespace Vendor (Module)

Dùng :: để tham chiếu namespace từ module bên ngoài:

// Đăng ký path cho namespace riêng
$translator->addPath('/path/to/module/lang', 'booking');

// Sử dụng
t('booking::messages.confirm');
t('booking::errors.not_found', ['id' => 42]);

Cấu trúc thư mục module:

/path/to/module/lang/
├── vi/
│ ├── messages.php
│ └── errors.php
└── en/
├── messages.php
└── errors.php

Thêm Path Tại Runtime

// Thêm path dịch mặc định (namespace 'messages')
$translator->addPath(base_path('lang'));

// Thêm với namespace cụ thể
$translator->addPath('/modules/tour/lang', 'tour');

Drivers

Array Driver (PHP files) — mặc định

'driver' => 'array',

Đọc file PHP trả về array, hỗ trợ nested keys:

// lang/vi/tour.php
return [
'title' => 'Tour du lịch',
'detail' => [
'price' => 'Giá: :price VND',
'duration' => 'Thời gian: :days ngày',
],
];
t('tour.detail.price', ['price' => '5,000,000']); // "Giá: 5,000,000 VND"

JSON Driver

'driver' => 'json',

Cấu trúc giống Array Driver nhưng dùng file .json:

// lang/vi/tour.json
{
"title": "Tour du lịch",
"detail": {
"price": "Giá: :price VND"
}
}

Database Driver

'driver' => 'database',
'drivers' => [
'database' => [
'connection' => null,
'table' => 'translations',
],
],

Bảng được tạo tự động nếu chưa tồn tại:

CREATE TABLE `translations` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
`locale` VARCHAR(10) NOT NULL,
`namespace` VARCHAR(100) NOT NULL,
`key` VARCHAR(255) NOT NULL,
`value` TEXT NOT NULL,
`created_at` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY `unique_translation` (`locale`, `namespace`, `key`),
INDEX `idx_locale_namespace` (`locale`, `namespace`)
);

Quản lý bản dịch trong database

use Vietiso\Core\Translation\Loader\DatabaseLoader;

/** @var DatabaseLoader $loader */
$loader = translator()->getLoader();

// Thêm hoặc cập nhật một bản dịch
$loader->set('vi', 'messages', 'welcome', 'Xin chào :name!');

// Thêm nhiều bản dịch cùng lúc
$loader->setMany('vi', 'tour', [
'title' => 'Danh sách tour',
'not_found' => 'Không tìm thấy tour.',
]);

// Import từ array (bỏ qua key đã tồn tại)
$loader->import('vi', 'messages', $translations);

// Import ghi đè key đã có
$loader->import('vi', 'messages', $translations, overwrite: true);

// Xóa một bản dịch
$loader->delete('vi', 'messages', 'welcome');

// Xóa toàn bộ namespace
$loader->deleteNamespace('vi', 'tour');

// Xóa cache trong memory
$loader->clearCache(); // toàn bộ
$loader->clearCache('vi'); // theo locale
$loader->clearCache('vi', 'tour'); // theo locale + namespace

Cache Bản Dịch

Bật cache để tránh đọc file/database nhiều lần:

'cache' => [
'enabled' => true,
'driver' => null, // null = dùng default cache driver
'ttl' => 3600,
'prefix' => 'trans:',
],

CachedLoader tự động bọc loader gốc, cache theo key trans:{locale}:{namespace}.

Xóa cache thủ công:

// Xóa toàn bộ translation cache
app()->get('translation')->flush();

Kiểm Tra Bản Dịch

$translator = translator();

// Kiểm tra key có tồn tại không
$translator->has('messages.welcome'); // true
$translator->has('messages.not_exist'); // false
$translator->has('messages.welcome', 'en'); // kiểm tra locale cụ thể

// Lấy array bản dịch (khi key trỏ vào nhóm)
$errors = $translator->get('validation');
// ['required' => '...', 'email' => '...']

Ví Dụ Thực Tế

Dịch thông báo lỗi validation

// lang/vi/validation.php
return [
'required' => 'Trường :field không được để trống.',
'email' => 'Trường :field phải là email hợp lệ.',
'min' => 'Trường :field phải có ít nhất :min ký tự.',
];

// Sử dụng
t('validation.required', ['field' => 'Họ tên']);
// → "Trường Họ tên không được để trống."

Dịch theo locale từ request

use Vietiso\Core\Http\Request;

public function setLocale(Request $request): Response
{
$locale = $request->input('locale', 'vi');
translator()->setLocale($locale);

return Response::json(['locale' => $locale]);
}

Middleware đổi locale tự động

use Vietiso\Core\Http\Request;
use Vietiso\Core\Http\Response;

class LocaleMiddleware
{
public function handle(Request $request, \Closure $next): Response
{
$locale = $request->header('Accept-Language')
?? $request->cookie->get('locale')
?? 'vi';

if (translator()->isValidLocale($locale)) {
translator()->setLocale($locale);
}

return $next($request);
}
}

Số nhiều trong ứng dụng tour

// lang/vi/tour.php
return [
'seats' => '{0} Hết chỗ|{1} Còn :count chỗ|[2,*] Còn :count chỗ trống',
'review' => '{1} :count đánh giá|[2,*] :count đánh giá',
];

t('tour.seats', ['count' => 0]); // "Hết chỗ"
t('tour.seats', ['count' => 1]); // "Còn 1 chỗ"
t('tour.seats', ['count' => 5]); // "Còn 5 chỗ trống"