Hyperf 的 setLocale() 在协程中不可直接使用,因其修改单例 Translator 的全局 locale 属性,导致多协程间语言状态污染;正确做法是通过 CoroutineContext 隔离 locale,结合中间件动态注入、严格匹配目录命名(如 zh_CN)、缓存 key 包含 locale 维度,确保翻译隔离与准确。

Hyperf 的 hyperf/translation 组件本身不支持运行时动态切换语言(比如用户点一下按钮就切中文/英文),它默认依赖 PHP-FPM 或 Swoole 请求生命周期内的 app()->setLocale(),但在协程环境下直接调用会导致 locale 泄漏——即一个协程改了语言,其他并发协程也意外被影响。
为什么 setLocale() 在协程里不能直接用
Hyperf 基于 Swoole 协程,多个请求共享同一个进程和部分全局状态。app()->setLocale('zh_CN') 实际修改的是容器单例中的 locale 属性,而这个实例在协程间复用。一旦 A 请求设成 zh_CN,B 请求还没来得及设就被“继承”了,结果返回错语言的文案。
- 现象:多用户同时访问,A 切英文、B 切中文,但 B 的响应里混着英文翻译
- 根本原因:
TranslationServiceProvider注册的Translator是单例,且未做协程隔离 - 官方文档没强调这点,容易误以为和 Laravel 一样直接设就行
正确做法:用 CoroutineContext 隔离 locale
把语言标识存在当前协程上下文里,每次翻译前从上下文中取,避免污染全局状态。需要手动包装 Translator 实例或重写其 get() 行为。
- 在
config/autoload/translation.php中保留默认配置,但不要依赖app()->setLocale() - 写一个协程安全的
LocaleTranslator类,构造时从CoroutineContext::get('locale')读取,fallback 到配置默认值 - 在中间件中解析请求头(如
Accept-Language)或查询参数(如?lang=ja),并写入上下文:CoroutineContext::set('locale', $lang) - 翻译调用统一走这个封装类,例如
$this->translator->trans('auth.failed'),内部自动取上下文 locale
示例中间件片段:
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$lang = $request->getQueryParams()['lang'] ?? $request->getHeaderLine('Accept-Language');
$locale = $lang === 'ja' ? 'ja_JP' : 'zh_CN';
CoroutineContext::set('locale', $locale);
return $handler->handle($request);
}
资源文件路径与加载逻辑要匹配 locale 格式
hyperf/translation 默认按 resources/lang/{locale}/{group}.php 加载,但很多人忽略 locale 字符串大小写和分隔符必须严格匹配文件路径。
- 错误写法:
setLocale('zh-cn')→ 尝试加载resources/lang/zh-cn/auth.php,但实际目录是zh_CN - 正确命名:目录名用
zh_CN、en_US、ja_JP,代码里传的 locale 字符串必须完全一致 - 注意 Windows 系统对大小写不敏感,Linux 上会 404,本地测试没问题上线就报
InvalidArgumentException: Translation not found - 建议在
config/autoload/translation.php中显式配置'locale' => 'zh_CN'作为 fallback,避免未命中时静默失败
缓存翻译内容时别忘了带 locale 维度
如果启用了 cache_driver(如 redis),默认缓存 key 是 translation:{group}:{key},没有包含 locale。结果就是不同语言共用同一份缓存,切语言后文案不变。
- 解决方案:重写
Hyperf\Translation\Translator的parseKey()或自定义MessageSelector,确保 cache key 包含 locale,例如translation:{locale}:{group}:{key} - 更简单的方式:关掉翻译缓存(开发期够用),或用
ArrayTranslator替代,它不走缓存,纯内存加载 - 验证是否生效:改一个语言的文案,清 Redis,再请求两次不同语言,看缓存 key 是否区分 locale
真正麻烦的不是加多语言功能,而是协程下每个环节都要主动考虑“这个对象是不是跨协程共享的”,locale、translator、甚至你封装的 i18n 工具类,都得检查是否做了上下文绑定。漏掉任意一环,国际化就会变成随机语言。


















