mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-17 07:21:58 +00:00
feat(network): send Accept-Language header to addons
Adds an `Accept-Language` header on every addon HTTP call so Stremio- compatible addons that maintain localized payloads (catalog names, descriptions, …) can serve them in the user's language. Resolution rules (mirrors NuvioTV PR #1766 with stricter region handling): - User-picked in-app language (always region-less, e.g. `fr`): legacy behaviour `fr, en;q=0.7` — accept any French variant the addon serves. - Otherwise device locale tag from `Locale.getDefault().toLanguageTag()` / `NSLocale.preferredLanguages.first`. - Regional locale (`fr-FR`, `pt-BR`, `es-MX`): strict chain `fr-FR, fr;q=0.9, en;q=0.5` — exact region first, then any variant of the same language, then English. Cross-region variants (e.g. `fr-CA`) are not preferred. - English (with or without region): tag itself, no q-suffix. - Final fallback: `en`. Header is added as a default — if the caller already specified `Accept-Language` (case-insensitive), it is preserved untouched. Affects every place in the app that displays a catalog name pulled from an addon manifest: home rows, "see all" header, search → discover catalog selector, addon reorder home catalogs screen, collection editor catalog picker. Adding a request header is fully backward-compatible — addons that ignore it return identical payloads.
This commit is contained in:
parent
37203d1fc1
commit
8e97bac880
5 changed files with 89 additions and 5 deletions
|
|
@ -0,0 +1,6 @@
|
|||
package com.nuvio.app.features.addons
|
||||
|
||||
import java.util.Locale
|
||||
|
||||
internal actual fun deviceLanguageTag(): String? =
|
||||
Locale.getDefault().toLanguageTag().takeIf { it.isNotBlank() }
|
||||
|
|
@ -71,6 +71,13 @@ private fun Map<String, String>.withoutAcceptEncoding(): Map<String, String> =
|
|||
private fun Map<String, String>.getHeaderIgnoreCase(name: String): String? =
|
||||
entries.firstOrNull { (key, _) -> key.equals(name, ignoreCase = true) }?.value
|
||||
|
||||
private fun Map<String, String>.withDefaultAcceptLanguage(): Map<String, String> =
|
||||
if (getHeaderIgnoreCase("Accept-Language") != null) {
|
||||
this
|
||||
} else {
|
||||
this + ("Accept-Language" to buildAcceptLanguageHeader())
|
||||
}
|
||||
|
||||
private data class LimitedReadResult(
|
||||
val bytes: ByteArray,
|
||||
val truncated: Boolean,
|
||||
|
|
@ -134,7 +141,7 @@ private suspend fun executeTextRequest(
|
|||
body: String = "",
|
||||
): String = withContext(Dispatchers.IO) {
|
||||
val normalizedMethod = method.uppercase()
|
||||
val sanitizedHeaders = headers.withoutAcceptEncoding()
|
||||
val sanitizedHeaders = headers.withoutAcceptEncoding().withDefaultAcceptLanguage()
|
||||
val builder = Request.Builder().url(url)
|
||||
sanitizedHeaders.forEach { (key, value) ->
|
||||
builder.header(key, value)
|
||||
|
|
@ -214,7 +221,7 @@ actual suspend fun httpRequestRaw(
|
|||
): RawHttpResponse =
|
||||
withContext(Dispatchers.IO) {
|
||||
val normalizedMethod = method.uppercase()
|
||||
val sanitizedHeaders = headers.withoutAcceptEncoding()
|
||||
val sanitizedHeaders = headers.withoutAcceptEncoding().withDefaultAcceptLanguage()
|
||||
val builder = Request.Builder().url(url)
|
||||
sanitizedHeaders.forEach { (key, value) ->
|
||||
builder.header(key, value)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
package com.nuvio.app.features.addons
|
||||
|
||||
import com.nuvio.app.features.settings.ThemeSettingsStorage
|
||||
|
||||
/**
|
||||
* Returns the device's primary BCP-47 language tag (e.g. "fr-FR", "en", "es-MX"),
|
||||
* or `null` if the platform can't determine one.
|
||||
*/
|
||||
internal expect fun deviceLanguageTag(): String?
|
||||
|
||||
/**
|
||||
* Builds the value of the HTTP `Accept-Language` header that the addon HTTP
|
||||
* client should send.
|
||||
*
|
||||
* Resolution rules:
|
||||
* - If the user has explicitly picked an in-app language, that code wins
|
||||
* (the in-app language is always region-less, e.g. `fr`).
|
||||
* - Otherwise the device locale tag is used.
|
||||
*
|
||||
* Header format:
|
||||
* - Regional locale (`fr-FR`, `pt-BR`, `es-MX`): strict chain
|
||||
* `fr-FR, fr;q=0.9, en;q=0.5` — accept the exact region first, then any
|
||||
* variant of the same language, then English. Cross-region variants
|
||||
* (e.g. `fr-CA`) are not preferred.
|
||||
* - Language only (`fr`): legacy behaviour `fr, en;q=0.7` — any French variant
|
||||
* the addon serves is acceptable.
|
||||
* - English (with or without region): just the tag itself, no q-suffix.
|
||||
*/
|
||||
internal fun buildAcceptLanguageHeader(): String {
|
||||
val rawTag = ThemeSettingsStorage.loadSelectedAppLanguage()?.takeIf { it.isNotBlank() }
|
||||
?: deviceLanguageTag()?.takeIf {
|
||||
it.isNotBlank() && !it.equals("und", ignoreCase = true)
|
||||
}
|
||||
?: return "en"
|
||||
|
||||
val normalized = rawTag.replace('_', '-').trim()
|
||||
val parts = normalized.split('-', limit = 2)
|
||||
val language = parts[0].lowercase().takeIf { it.isNotBlank() } ?: return "en"
|
||||
val region = parts.getOrNull(1)?.takeIf { it.isNotBlank() }?.uppercase()
|
||||
|
||||
if (language == "en") {
|
||||
return if (region != null) "en-$region, en" else "en"
|
||||
}
|
||||
|
||||
return if (region != null) {
|
||||
"$language-$region, $language;q=0.9, en;q=0.5"
|
||||
} else {
|
||||
"$language, en;q=0.7"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.nuvio.app.features.addons
|
||||
|
||||
import platform.Foundation.NSLocale
|
||||
import platform.Foundation.preferredLanguages
|
||||
|
||||
internal actual fun deviceLanguageTag(): String? {
|
||||
val preferred = NSLocale.preferredLanguages.firstOrNull() as? String
|
||||
return preferred?.takeIf { it.isNotBlank() }
|
||||
}
|
||||
|
|
@ -46,10 +46,21 @@ private val addonHttpClient = HttpClient(Darwin) {
|
|||
expectSuccess = false
|
||||
}
|
||||
|
||||
private fun Map<String, String>.hasHeaderIgnoreCase(name: String): Boolean =
|
||||
keys.any { it.equals(name, ignoreCase = true) }
|
||||
|
||||
private fun Map<String, String>.withDefaultAcceptLanguage(): Map<String, String> =
|
||||
if (hasHeaderIgnoreCase("Accept-Language")) {
|
||||
this
|
||||
} else {
|
||||
this + ("Accept-Language" to buildAcceptLanguageHeader())
|
||||
}
|
||||
|
||||
actual suspend fun httpGetText(url: String): String =
|
||||
addonHttpClient
|
||||
.get(url) {
|
||||
accept(ContentType.Application.Json)
|
||||
header(HttpHeaders.AcceptLanguage, buildAcceptLanguageHeader())
|
||||
}
|
||||
.let { response ->
|
||||
val payload = response.bodyAsText()
|
||||
|
|
@ -67,6 +78,7 @@ actual suspend fun httpPostJson(url: String, body: String): String =
|
|||
.post(url) {
|
||||
accept(ContentType.Application.Json)
|
||||
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||
header(HttpHeaders.AcceptLanguage, buildAcceptLanguageHeader())
|
||||
setBody(body)
|
||||
}
|
||||
.let { response ->
|
||||
|
|
@ -87,7 +99,7 @@ actual suspend fun httpGetTextWithHeaders(
|
|||
addonHttpClient
|
||||
.get(url) {
|
||||
accept(ContentType.Application.Json)
|
||||
headers.forEach { (key, value) ->
|
||||
headers.withDefaultAcceptLanguage().forEach { (key, value) ->
|
||||
header(key, value)
|
||||
}
|
||||
}
|
||||
|
|
@ -111,7 +123,7 @@ actual suspend fun httpPostJsonWithHeaders(
|
|||
.post(url) {
|
||||
accept(ContentType.Application.Json)
|
||||
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||
headers.forEach { (key, value) ->
|
||||
headers.withDefaultAcceptLanguage().forEach { (key, value) ->
|
||||
header(key, value)
|
||||
}
|
||||
setBody(body)
|
||||
|
|
@ -138,7 +150,7 @@ actual suspend fun httpRequestRaw(
|
|||
.request {
|
||||
url(url)
|
||||
this.method = HttpMethod.parse(method.uppercase())
|
||||
headers.forEach { (key, value) ->
|
||||
headers.withDefaultAcceptLanguage().forEach { (key, value) ->
|
||||
header(key, value)
|
||||
}
|
||||
if (this.method == HttpMethod.Post || this.method == HttpMethod.Put || this.method == HttpMethod.Patch) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue