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 key | Namespace | Key |
|---|---|---|
messages.welcome | messages | welcome |
validation.required | validation | required |
tour.detail.title | tour | detail.title |
mymodule::errors.403 | mymodule | errors.403 |
welcome (không có dấu chấm) | messages | welcome |
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"
| Placeholder | Kết quả |
|---|---|
:name | giá trị gốc |
:NAME | UPPERCASE |
:Name | Ucfirst |
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:
| Locale | Quy tắc |
|---|---|
vi, zh, ja, ko, th, id | Khô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, sr | 3 dạng: 1 / 2-4 / còn lại |
ar | 6 dạng theo CLDR tiếng Ả-rập |
pl | 3 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"