本文目录导读:

我来设计一个PHP多语言方案,涵盖从简单到复杂的各种场景。
基础架构设计
目录结构
project/
├── app/
│ ├── Languages/
│ │ ├── en/
│ │ │ ├── messages.php
│ │ │ ├── validation.php
│ │ │ └── errors.php
│ │ ├── zh-CN/
│ │ │ ├── messages.php
│ │ │ └── ...
│ │ └── ja/
│ │ └── ...
│ ├── Core/
│ │ └── Translator.php
│ └── Helpers/
│ └── translate_helper.php
└── config/
└── language.php
核心翻译类实现
<?php
// app/Core/Translator.php
namespace App\Core;
class Translator
{
private static $instance = null;
private $translations = [];
private $locale = 'en';
private $fallbackLocale = 'en';
private $loadedFiles = [];
// 语言包目录
private $langPath = null;
// 支持的语言列表
private $supportedLocales = ['en', 'zh-CN', 'ja', 'fr', 'de'];
// 缓存
private $cache = [];
private function __construct()
{
$this->langPath = dirname(__DIR__) . '/Languages/';
$this->setLocale($this->getBrowserLocale());
}
public static function getInstance()
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* 设置当前语言
*/
public function setLocale(string $locale): void
{
$locale = $this->normalizeLocale($locale);
if (in_array($locale, $this->supportedLocales)) {
$this->locale = $locale;
$this->loadLocale($locale);
}
// 设置PHP locale
setlocale(LC_ALL, $locale . '.UTF-8');
// 设置时区
date_default_timezone_set($this->getTimezoneForLocale($locale));
}
/**
* 获取当前语言
*/
public function getLocale(): string
{
return $this->locale;
}
/**
* 翻译方法
*/
public function trans(string $key, array $replacements = [], string $locale = null): string
{
$locale = $locale ?: $this->locale;
// 检查缓存
$cacheKey = md5($key . $locale);
if (isset($this->cache[$cacheKey])) {
return $this->applyReplacements($this->cache[$cacheKey], $replacements);
}
// 解析key:file.key.nested
$segments = explode('.', $key);
$file = array_shift($segments);
// 加载语言文件
if (!isset($this->translations[$locale][$file])) {
$this->loadFile($locale, $file);
}
// 查找翻译
$translation = $this->findTranslation($this->translations[$locale][$file] ?? [], $segments);
// 如果没找到,尝试fallback
if ($translation === null && $locale !== $this->fallbackLocale) {
$this->loadLocale($this->fallbackLocale);
$translation = $this->findTranslation($this->translations[$this->fallbackLocale][$file] ?? [], $segments);
}
// 最终fallback返回key
$translation = $translation ?? $key;
// 缓存结果
$this->cache[$cacheKey] = $translation;
return $this->applyReplacements($translation, $replacements);
}
/**
* 复数翻译
*/
public function transChoice(string $key, int $number, array $replacements = []): string
{
$locale = $this->locale;
$replacements['count'] = $number;
$segments = explode('.', $key);
$file = array_shift($segments);
$this->loadFile($locale, $file);
$translations = $this->findTranslation($this->translations[$locale][$file] ?? [], $segments);
if (is_array($translations)) {
$translation = $this->selectPluralForm($translations, $number, $locale);
} else {
$translation = $translations ?? $key;
}
return $this->applyReplacements($translation, $replacements);
}
/**
* 加载语言文件
*/
private function loadFiles(string $locale): void
{
$path = $this->langPath . $locale . '/';
if (!is_dir($path)) {
return;
}
$files = glob($path . '*.php');
foreach ($files as $file) {
$filename = basename($file, '.php');
$this->loadFile($locale, $filename);
}
}
/**
* 加载单个文件
*/
private function loadFile(string $locale, string $file): void
{
if (!isset($this->loadedFiles[$locale][$file])) {
$path = $this->langPath . $locale . '/' . $file . '.php';
if (file_exists($path)) {
$this->translations[$locale][$file] = require $path;
$this->loadedFiles[$locale][$file] = true;
}
}
}
/**
* 查找嵌套key
*/
private function findTranslation($translations, array $segments)
{
$result = $translations;
foreach ($segments as $segment) {
if (!isset($result[$segment])) {
return null;
}
$result = $result[$segment];
}
return $result;
}
/**
* 应用替换
*/
private function applyReplacements(string $translation, array $replacements): string
{
foreach ($replacements as $key => $value) {
$translation = str_replace(':' . $key, $value, $translation);
}
return $translation;
}
/**
* 选择复数形式
*/
private function selectPluralForm(array $forms, int $number, string $locale): string
{
// 中文没有复数形式
if (in_array($locale, ['zh-CN', 'ja', 'ko'])) {
return $forms['other'] ?? reset($forms);
}
// 英文复数规则
if (in_array($locale, ['en', 'de', 'fr'])) {
return $number == 1 ? $forms['one'] : $forms['other'];
}
// 俄语等复杂复数
if ($locale === 'ru') {
$mod10 = $number % 10;
$mod100 = $number % 100;
if ($mod10 == 1 && $mod100 != 11) {
return $forms['one'];
}
if ($mod10 >= 2 && $mod10 <= 4 && ($mod100 < 10 || $mod100 >= 20)) {
return $forms['few'];
}
return $forms['many'];
}
// 默认
return $forms['other'] ?? reset($forms);
}
/**
* 获取浏览器语言
*/
private function getBrowserLocale(): string
{
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
$browserLocales = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']);
$locale = substr($browserLocales[0], 0, 2);
// 映射常见浏览器语言
$mapping = [
'en' => 'en',
'zh' => 'zh-CN',
'ja' => 'ja',
'fr' => 'fr',
];
return $mapping[$locale] ?? 'en';
}
return 'en';
}
/**
* 规范化语言代码
*/
private function normalizeLocale(string $locale): string
{
// 统一格式:zh-CN, en-GB
$localeMap = [
'zh_cn' => 'zh-CN',
'zh-tw' => 'zh-TW',
'en_us' => 'en',
'en_gb' => 'en',
];
return $localeMap[strtolower($locale)] ?? $locale;
}
/**
* 获取时区
*/
private function getTimezoneForLocale(string $locale): string
{
$timezones = [
'en' => 'America/New_York',
'zh-CN' => 'Asia/Shanghai',
'ja' => 'Asia/Tokyo',
'fr' => 'Europe/Paris',
];
return $timezones[$locale] ?? 'UTC';
}
/**
* 创建翻译辅助函数
*/
public function t(string $key, array $replacements = []): string
{
return $this->trans($key, $replacements);
}
}
语言文件示例
<?php
// app/Languages/zh-CN/messages.php
return [
'welcome' => '欢迎来到 :app_name',
'login' => '登录',
'logout' => '退出登录',
'register' => '注册',
'navigation' => [
'home' => '首页',
'about' => '关于我们',
'contact' => '联系我们',
'products' => '产品',
],
'messages' => [
'success' => '操作成功',
'error' => '操作失败',
'warning' => '警告',
'info' => '提示信息',
],
'greetings' => [
'morning' => '早上好',
'afternoon' => '下午好',
'evening' => '晚上好',
],
];
// app/Languages/en/messages.php
return [
'welcome' => 'Welcome to :app_name',
'login' => 'Login',
'logout' => 'Logout',
'register' => 'Register',
// ... 其他英文翻译
];
复数翻译示例
<?php
// app/Languages/en/messages.php
return [
'items_count' => [
'one' => ':count item',
'other' => ':count items',
],
];
// app/Languages/zh-CN/messages.php
return [
'items_count' => [
'other' => '共 :count 个项目',
],
];
辅助函数和全局集成
<?php
// app/Helpers/translate_helper.php
if (!function_exists('t')) {
function t(string $key, array $replacements = []): string
{
return \App\Core\Translator::getInstance()->trans($key, $replacements);
}
}
if (!function_exists('trans')) {
function trans(string $key, array $replacements = []): string
{
return t($key, $replacements);
}
}
// 简单调用
echo t('welcome', ['app_name' => 'MyApp']);
echo t('navigation.home');
echo trans('messages.success');
// 复数
echo $translator->transChoice('items_count', 5); // "5 items"
echo $translator->transChoice('items_count', 1); // "1 item"
// 切换语言
$translator = \App\Core\Translator::getInstance();
$translator->setLocale('zh-CN');
echo $translator->trans('welcome', ['app_name' => '我的应用']);
高级功能
1 语言检测中间件
<?php
// app/Middleware/LocaleMiddleware.php
class LocaleMiddleware
{
public function handle($request, $next)
{
$translator = \App\Core\Translator::getInstance();
// 优先级:URL参数 > Session > Cookie > 浏览器设置
$locale = $request->get('lang')
?? $_SESSION['locale']
?? $_COOKIE['locale']
?? null;
if ($locale) {
$translator->setLocale($locale);
$_SESSION['locale'] = $translator->getLocale();
}
// 保存到Cookie
setcookie('locale', $translator->getLocale(), time() + 3600 * 24 * 30);
return $next($request);
}
}
2 SEO友好URL
<?php
// URL格式: /en/products, /zh-CN/products
class LocaleRouter
{
public function generateUrl(string $path, array $params = [], string $locale = null): string
{
$locale = $locale ?: \App\Core\Translator::getInstance()->getLocale();
$baseUrl = "/$locale/$path";
if (!empty($params)) {
$baseUrl .= '?' . http_build_query($params);
}
return $baseUrl;
}
}
3 数据库翻译
<?php
// app/Models/Product.php
class Product extends Model
{
// 可翻译字段
protected $translatable = ['name', 'description'];
public function getAttribute($key)
{
$locale = \App\Core\Translator::getInstance()->getLocale();
if (in_array($key, $this->translatable)) {
$key = $key . '_' . $locale;
}
return parent::getAttribute($key);
}
}
// 数据库表结构
// products table
// id, name_en, name_zh-CN, name_ja, description_en, ...
配置文件
<?php
// config/language.php
return [
// 默认语言
'default_locale' => 'en',
// 后备语言
'fallback_locale' => 'en',
// 支持的语言
'supported_locales' => [
'en' => 'English',
'zh-CN' => '简体中文',
'ja' => '日本語',
'fr' => 'Français',
],
// 语言切换时刷新页面
'redirect_on_change' => true,
// 缓存设置
'cache' => [
'enabled' => true,
'ttl' => 3600, // 1小时
],
// 复数规则
'plural_rules' => [
// 每种语言对应的复数规则
],
];
最佳实践建议
- 性能优化
- 使用APCu或Redis缓存翻译数组
- 按需只加载特定的语言文件
- 使用PHP opcache
// 缓存优化示例
public function loadFile($locale, $file)
{
$cacheKey = "lang.$locale.$file";
if ($this->cacheEnabled && apcu_exists($cacheKey)) {
return apcu_fetch($cacheKey);
}
// 加载文件...
if ($this->cacheEnabled) {
apcu_store($cacheKey, $translations, $this->cacheTtl);
}
}
-
开发工具
- 开发翻译代码生成器
- 自动化提取需要翻译的字符串
- 创建翻译检查器
-
测试
class TranslationTest extends PHPUnit\Framework\TestCase { public function testTranslationsExist() { $translator = Translator::getInstance(); foreach ($this->getSupportedLocales() as $locale) { $translator->setLocale($locale); // 检查所有key都存在 foreach ($this->getAllKeys() as $key) { $this->assertNotEquals( $key, $translator->trans($key), "Translation missing for key: $key in locale: $locale" ); } } } }
这个PHP多语言方案涵盖了从基本语言切换、复数翻译、SEO优化到缓存性能的完整实现,适用于各种规模的项目。