mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-17 15:32:01 +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? =
|
private fun Map<String, String>.getHeaderIgnoreCase(name: String): String? =
|
||||||
entries.firstOrNull { (key, _) -> key.equals(name, ignoreCase = true) }?.value
|
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(
|
private data class LimitedReadResult(
|
||||||
val bytes: ByteArray,
|
val bytes: ByteArray,
|
||||||
val truncated: Boolean,
|
val truncated: Boolean,
|
||||||
|
|
@ -134,7 +141,7 @@ private suspend fun executeTextRequest(
|
||||||
body: String = "",
|
body: String = "",
|
||||||
): String = withContext(Dispatchers.IO) {
|
): String = withContext(Dispatchers.IO) {
|
||||||
val normalizedMethod = method.uppercase()
|
val normalizedMethod = method.uppercase()
|
||||||
val sanitizedHeaders = headers.withoutAcceptEncoding()
|
val sanitizedHeaders = headers.withoutAcceptEncoding().withDefaultAcceptLanguage()
|
||||||
val builder = Request.Builder().url(url)
|
val builder = Request.Builder().url(url)
|
||||||
sanitizedHeaders.forEach { (key, value) ->
|
sanitizedHeaders.forEach { (key, value) ->
|
||||||
builder.header(key, value)
|
builder.header(key, value)
|
||||||
|
|
@ -214,7 +221,7 @@ actual suspend fun httpRequestRaw(
|
||||||
): RawHttpResponse =
|
): RawHttpResponse =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
val normalizedMethod = method.uppercase()
|
val normalizedMethod = method.uppercase()
|
||||||
val sanitizedHeaders = headers.withoutAcceptEncoding()
|
val sanitizedHeaders = headers.withoutAcceptEncoding().withDefaultAcceptLanguage()
|
||||||
val builder = Request.Builder().url(url)
|
val builder = Request.Builder().url(url)
|
||||||
sanitizedHeaders.forEach { (key, value) ->
|
sanitizedHeaders.forEach { (key, value) ->
|
||||||
builder.header(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
|
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 =
|
actual suspend fun httpGetText(url: String): String =
|
||||||
addonHttpClient
|
addonHttpClient
|
||||||
.get(url) {
|
.get(url) {
|
||||||
accept(ContentType.Application.Json)
|
accept(ContentType.Application.Json)
|
||||||
|
header(HttpHeaders.AcceptLanguage, buildAcceptLanguageHeader())
|
||||||
}
|
}
|
||||||
.let { response ->
|
.let { response ->
|
||||||
val payload = response.bodyAsText()
|
val payload = response.bodyAsText()
|
||||||
|
|
@ -67,6 +78,7 @@ actual suspend fun httpPostJson(url: String, body: String): String =
|
||||||
.post(url) {
|
.post(url) {
|
||||||
accept(ContentType.Application.Json)
|
accept(ContentType.Application.Json)
|
||||||
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||||
|
header(HttpHeaders.AcceptLanguage, buildAcceptLanguageHeader())
|
||||||
setBody(body)
|
setBody(body)
|
||||||
}
|
}
|
||||||
.let { response ->
|
.let { response ->
|
||||||
|
|
@ -87,7 +99,7 @@ actual suspend fun httpGetTextWithHeaders(
|
||||||
addonHttpClient
|
addonHttpClient
|
||||||
.get(url) {
|
.get(url) {
|
||||||
accept(ContentType.Application.Json)
|
accept(ContentType.Application.Json)
|
||||||
headers.forEach { (key, value) ->
|
headers.withDefaultAcceptLanguage().forEach { (key, value) ->
|
||||||
header(key, value)
|
header(key, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -111,7 +123,7 @@ actual suspend fun httpPostJsonWithHeaders(
|
||||||
.post(url) {
|
.post(url) {
|
||||||
accept(ContentType.Application.Json)
|
accept(ContentType.Application.Json)
|
||||||
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
header(HttpHeaders.ContentType, ContentType.Application.Json.toString())
|
||||||
headers.forEach { (key, value) ->
|
headers.withDefaultAcceptLanguage().forEach { (key, value) ->
|
||||||
header(key, value)
|
header(key, value)
|
||||||
}
|
}
|
||||||
setBody(body)
|
setBody(body)
|
||||||
|
|
@ -138,7 +150,7 @@ actual suspend fun httpRequestRaw(
|
||||||
.request {
|
.request {
|
||||||
url(url)
|
url(url)
|
||||||
this.method = HttpMethod.parse(method.uppercase())
|
this.method = HttpMethod.parse(method.uppercase())
|
||||||
headers.forEach { (key, value) ->
|
headers.withDefaultAcceptLanguage().forEach { (key, value) ->
|
||||||
header(key, value)
|
header(key, value)
|
||||||
}
|
}
|
||||||
if (this.method == HttpMethod.Post || this.method == HttpMethod.Put || this.method == HttpMethod.Patch) {
|
if (this.method == HttpMethod.Post || this.method == HttpMethod.Put || this.method == HttpMethod.Patch) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue