Context
Language::setLang() currently always writes the chosen language to $_SESSION['language'] AND sends a Set-Cookie: language=…; Max-Age=2592000; Path=/ response header (vendor/neverwoods/bili/classes/Bili/Language.php:179-194). That behavior makes sense for stateful web apps, but it's a problem in stateless contexts.
We just hit this in the Celery REST API. A consumer calls a single endpoint that exposes ?language=es for an inline preview (e.g. POST /v3/time-off-request-description). Bili's setLang('espanol-utf-8') runs, sets a 30-day cookie, and from that point on every subsequent call from the same client (Postman, browser, curl with a jar) ships Cookie: language=espanol-utf-8 back to the API. LanguageMiddleware re-instantiates the singleton on every request, the constructor's getLang() picks up $_COOKIE['language'], and the whole API responds in Spanish — including endpoints that have nothing to do with that one preview parameter. From the consumer's point of view the language is "leaking" across endpoints.
REST APIs aren't supposed to carry language preference in cookies or sessions; the standard mechanism is the Accept-Language request header (RFC 9110 §12.5.4). So the API needs to ignore Bili's session/cookie path entirely while still being able to call setLang() for the in-process effect of loading the language file.
Proposal
Add an optional second argument to Language::setLang() controlling persistence:
public function setLang(string $strLang, bool $blnPersist = true)
$blnPersist = true (default): unchanged behavior. Writes $_SESSION['language'] and the 30-day cookie. Existing callers behave exactly as today.
$blnPersist = false: load the language file in-process only. Skip the setcookie() call. Skip the $_SESSION['language'] write.
Same idea could optionally be applied to the constructor / getLang() paths so the singleton can be initialized in a "stateless" mode that also ignores incoming $_SESSION['language'] / $_COOKIE['language'] when picking the active language. That's a nice-to-have; the setLang() flag is the immediate need.
Backward compatibility
Fully backward compatible: the default true preserves today's behavior for every existing caller. Only callers that pass false opt into the new path.
Use case
In the Celery web API, LanguageMiddleware (and a small app-level ApiLanguage helper we're adding) will call setLang($strLang, false) after resolving the language from Accept-Language (and from supported ?language= query params on a handful of endpoints). No cookies, no session writes, no cross-endpoint leakage.
Acceptance criteria
setLang($lang, false) does not call setcookie('language', …).
setLang($lang, false) does not write to $_SESSION['language'].
setLang($lang, false) still loads the language file so subsequent Language::get(…) calls return strings in $lang.
setLang($lang, true) and setLang($lang) behave exactly as today.
- A unit test covers each of the three cases.
Related: the Celery web API tracking issue references this from its own implementation plan.
Context
Language::setLang()currently always writes the chosen language to$_SESSION['language']AND sends aSet-Cookie: language=…; Max-Age=2592000; Path=/response header (vendor/neverwoods/bili/classes/Bili/Language.php:179-194). That behavior makes sense for stateful web apps, but it's a problem in stateless contexts.We just hit this in the Celery REST API. A consumer calls a single endpoint that exposes
?language=esfor an inline preview (e.g.POST /v3/time-off-request-description). Bili'ssetLang('espanol-utf-8')runs, sets a 30-day cookie, and from that point on every subsequent call from the same client (Postman, browser, curl with a jar) shipsCookie: language=espanol-utf-8back to the API.LanguageMiddlewarere-instantiates the singleton on every request, the constructor'sgetLang()picks up$_COOKIE['language'], and the whole API responds in Spanish — including endpoints that have nothing to do with that one preview parameter. From the consumer's point of view the language is "leaking" across endpoints.REST APIs aren't supposed to carry language preference in cookies or sessions; the standard mechanism is the
Accept-Languagerequest header (RFC 9110 §12.5.4). So the API needs to ignore Bili's session/cookie path entirely while still being able to callsetLang()for the in-process effect of loading the language file.Proposal
Add an optional second argument to
Language::setLang()controlling persistence:$blnPersist = true(default): unchanged behavior. Writes$_SESSION['language']and the 30-day cookie. Existing callers behave exactly as today.$blnPersist = false: load the language file in-process only. Skip thesetcookie()call. Skip the$_SESSION['language']write.Same idea could optionally be applied to the constructor /
getLang()paths so the singleton can be initialized in a "stateless" mode that also ignores incoming$_SESSION['language']/$_COOKIE['language']when picking the active language. That's a nice-to-have; thesetLang()flag is the immediate need.Backward compatibility
Fully backward compatible: the default
truepreserves today's behavior for every existing caller. Only callers that passfalseopt into the new path.Use case
In the Celery web API,
LanguageMiddleware(and a small app-levelApiLanguagehelper we're adding) will callsetLang($strLang, false)after resolving the language fromAccept-Language(and from supported?language=query params on a handful of endpoints). No cookies, no session writes, no cross-endpoint leakage.Acceptance criteria
setLang($lang, false)does not callsetcookie('language', …).setLang($lang, false)does not write to$_SESSION['language'].setLang($lang, false)still loads the language file so subsequentLanguage::get(…)calls return strings in$lang.setLang($lang, true)andsetLang($lang)behave exactly as today.Related: the Celery web API tracking issue references this from its own implementation plan.