diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000..2ccda069
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,38 @@
+# The TV app had no CI at all — only a release workflow — so a broken string
+# resource reached a device before anyone saw it. This is the cheap half of a
+# build: it runs in seconds and catches the failure that is invisible in review.
+name: CI
+
+on:
+ push:
+ branches: [master]
+ pull_request:
+
+jobs:
+ strings:
+ name: Translated strings
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: '3.12'
+ # A mismatched format specifier throws at getString time, on that locale
+ # only, on the screen that uses it. Nothing else in the build sees it.
+ - name: Check the string resources
+ run: python3 tools/check_strings.py
+
+ build:
+ name: Assemble debug
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: '17'
+ - uses: gradle/actions/setup-gradle@v4
+ # `mergeDebugResources` is where a duplicate or badly named string
+ # resource fails, and it fails for every locale at once.
+ - name: Build
+ run: ./gradlew :app:assembleDebug --no-daemon
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 8cdd2804..2155da22 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -54,6 +54,7 @@
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
+ android:localeConfig="@xml/locales_config"
android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true"
android:requestRawExternalStorageAccess="true"
diff --git a/app/src/main/java/com/saikou/sozo_tv/adapters/SeriesPageAdapter.kt b/app/src/main/java/com/saikou/sozo_tv/adapters/SeriesPageAdapter.kt
index 5266dba4..b692925f 100644
--- a/app/src/main/java/com/saikou/sozo_tv/adapters/SeriesPageAdapter.kt
+++ b/app/src/main/java/com/saikou/sozo_tv/adapters/SeriesPageAdapter.kt
@@ -90,7 +90,8 @@ class SeriesPageAdapter(
binding.country.text = data.episode.toString()
root.setOnClickListener { onItemClicked?.invoke(data, absoluteAdapterPosition) }
if (LocalData.isAnimeEnabled) {
- topContainer.text = "Episode ${data.episode ?: 0}"
+ topContainer.text = topContainer.context
+ .getString(R.string.episode_number, data.episode ?: 0)
} else {
topContainer.text = data.title
diff --git a/app/src/main/java/com/saikou/sozo_tv/adapters/SourceAdapter.kt b/app/src/main/java/com/saikou/sozo_tv/adapters/SourceAdapter.kt
index 0d7730b7..0f91e999 100644
--- a/app/src/main/java/com/saikou/sozo_tv/adapters/SourceAdapter.kt
+++ b/app/src/main/java/com/saikou/sozo_tv/adapters/SourceAdapter.kt
@@ -47,7 +47,8 @@ class SourceAdapter(
fun bind(item: ListItem.Header) {
binding.tvGroupCategory.text = item.category
binding.tvGroupTitle.text = item.title
- binding.tvSourceCount.text = "${item.count} sources"
+ binding.tvSourceCount.text = binding.root.resources
+ .getQuantityString(R.plurals.source_count, item.count, item.count)
}
}
diff --git a/app/src/main/java/com/saikou/sozo_tv/components/TvToggleRowView.kt b/app/src/main/java/com/saikou/sozo_tv/components/TvToggleRowView.kt
index d2fd21ff..ec9699f7 100644
--- a/app/src/main/java/com/saikou/sozo_tv/components/TvToggleRowView.kt
+++ b/app/src/main/java/com/saikou/sozo_tv/components/TvToggleRowView.kt
@@ -100,12 +100,12 @@ class TvToggleRowView @JvmOverloads constructor(
if (enabled) {
statusDot.background =
ContextCompat.getDrawable(context, R.drawable.netflix_status_dot_enabled)
- statusText.text = "Enabled"
+ statusText.text = context.getString(R.string.status_enabled)
statusText.setTextColor(ContextCompat.getColor(context, R.color.netflix_green))
} else {
statusDot.background =
ContextCompat.getDrawable(context, R.drawable.netflix_status_dot_disabled)
- statusText.text = "Disabled"
+ statusText.text = context.getString(R.string.status_disabled)
statusText.setTextColor(ContextCompat.getColor(context, R.color.netflix_gray))
}
}
diff --git a/app/src/main/java/com/saikou/sozo_tv/presentation/activities/UpdateActivity.kt b/app/src/main/java/com/saikou/sozo_tv/presentation/activities/UpdateActivity.kt
index 8977b0b3..ff07a89a 100644
--- a/app/src/main/java/com/saikou/sozo_tv/presentation/activities/UpdateActivity.kt
+++ b/app/src/main/java/com/saikou/sozo_tv/presentation/activities/UpdateActivity.kt
@@ -17,6 +17,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat
import com.bumptech.glide.Glide
+import com.saikou.sozo_tv.R
import com.saikou.sozo_tv.components.spoiler.SpoilerPlugin
import com.saikou.sozo_tv.databinding.ActivityUpdateBinding
import com.saikou.sozo_tv.domain.model.AppUpdate
@@ -97,8 +98,8 @@ class UpdateActivity : AppCompatActivity() {
binding.progressView1.gone()
setProgressUi(0)
- binding.bottomSheerCustomTitle.text = "Update Available"
- binding.updateTxt.text = "Update Now"
+ binding.bottomSheerCustomTitle.text = getString(R.string.update_available)
+ binding.updateTxt.text = getString(R.string.update_now)
binding.updateBtn.isEnabled = true
binding.updateBtn.visible()
@@ -166,7 +167,7 @@ class UpdateActivity : AppCompatActivity() {
val link = appLink ?: return
binding.progressView1.visible()
binding.updateBtn.isEnabled = false
- binding.updateTxt.text = "Downloading…"
+ binding.updateTxt.text = getString(R.string.update_downloading)
vm.startDownload(this, link)
}
@@ -255,9 +256,9 @@ class UpdateActivity : AppCompatActivity() {
when (st) {
is UpdateViewModel.UiState.Idle -> {
binding.progressView1.gone()
- binding.bottomSheerCustomTitle.text = "Update Available"
+ binding.bottomSheerCustomTitle.text = getString(R.string.update_available)
binding.updateBtn.isEnabled = true
- binding.updateTxt.text = "Update Now"
+ binding.updateTxt.text = getString(R.string.update_now)
binding.updateBtn.requestInitialFocus()
binding.updateBtn.visible()
}
@@ -265,16 +266,16 @@ class UpdateActivity : AppCompatActivity() {
is UpdateViewModel.UiState.Downloading -> {
binding.progressView1.visible()
binding.updateBtn.isEnabled = false
- binding.updateTxt.text = "Downloading…"
+ binding.updateTxt.text = getString(R.string.update_downloading)
setProgressUi(st.progress1000)
}
is UpdateViewModel.UiState.DownloadComplete -> {
binding.progressView1.gone()
- binding.bottomSheerCustomTitle.text = "Download Complete!"
+ binding.bottomSheerCustomTitle.text = getString(R.string.update_download_complete)
binding.updateBtn.visible()
binding.updateBtn.isEnabled = true
- binding.updateTxt.text = "Install Now"
+ binding.updateTxt.text = getString(R.string.update_install_now)
binding.updateBtn.requestInitialFocus()
if (!canInstallUnknownApps()) {
@@ -287,10 +288,10 @@ class UpdateActivity : AppCompatActivity() {
is UpdateViewModel.UiState.DownloadFailed -> {
binding.progressView1.gone()
- binding.bottomSheerCustomTitle.text = "Download Failed"
+ binding.bottomSheerCustomTitle.text = getString(R.string.update_download_failed)
binding.updateBtn.visible()
binding.updateBtn.isEnabled = true
- binding.updateTxt.text = "Try Again"
+ binding.updateTxt.text = getString(R.string.update_try_again)
binding.updateBtn.requestInitialFocus()
snackString("Download failed: ${st.error}")
}
diff --git a/app/src/main/java/com/saikou/sozo_tv/presentation/screens/category/CategoriesScreen.kt b/app/src/main/java/com/saikou/sozo_tv/presentation/screens/category/CategoriesScreen.kt
index 6af5d422..e9735f90 100644
--- a/app/src/main/java/com/saikou/sozo_tv/presentation/screens/category/CategoriesScreen.kt
+++ b/app/src/main/java/com/saikou/sozo_tv/presentation/screens/category/CategoriesScreen.kt
@@ -58,9 +58,9 @@ class CategoriesScreen : Fragment() {
super.onViewCreated(view, savedInstanceState)
val preference = PreferenceManager()
if (preference.isModeAnimeEnabled()) {
- binding.textView6.text = "Filter Anime"
+ binding.textView6.text = getString(R.string.filter_anime)
} else {
- binding.textView6.text = "Filter Movie"
+ binding.textView6.text = getString(R.string.filter_movie)
}
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
diff --git a/app/src/main/java/com/saikou/sozo_tv/presentation/screens/detail/CastDetailAdapter.kt b/app/src/main/java/com/saikou/sozo_tv/presentation/screens/detail/CastDetailAdapter.kt
index ff5030ba..6a9344b7 100644
--- a/app/src/main/java/com/saikou/sozo_tv/presentation/screens/detail/CastDetailAdapter.kt
+++ b/app/src/main/java/com/saikou/sozo_tv/presentation/screens/detail/CastDetailAdapter.kt
@@ -218,7 +218,8 @@ class CastDetailAdapter(
if (item.age.isNotEmpty()) {
binding.ageBadge.visible()
- binding.characterAge.text = "Age: ${item.age}"
+ binding.characterAge.text = binding.root.context
+ .getString(R.string.character_age, item.age)
binding.ageBadge.animate()
.scaleX(1.05f)
.scaleY(1.05f)
diff --git a/app/src/main/java/com/saikou/sozo_tv/presentation/screens/detail/MovieDetailsAdapter.kt b/app/src/main/java/com/saikou/sozo_tv/presentation/screens/detail/MovieDetailsAdapter.kt
index af4a2e9c..9bcfae00 100644
--- a/app/src/main/java/com/saikou/sozo_tv/presentation/screens/detail/MovieDetailsAdapter.kt
+++ b/app/src/main/java/com/saikou/sozo_tv/presentation/screens/detail/MovieDetailsAdapter.kt
@@ -225,7 +225,7 @@ class MovieDetailsAdapter(
}
override fun onFinish() {
- countDownText.text = "Aired!"
+ countDownText.text = countDownText.context.getString(R.string.countdown_aired)
}
}.also { it.start() }
} else {
diff --git a/app/src/main/java/com/saikou/sozo_tv/presentation/screens/episodes/EpisodeScreen.kt b/app/src/main/java/com/saikou/sozo_tv/presentation/screens/episodes/EpisodeScreen.kt
index af414383..2921fa6c 100644
--- a/app/src/main/java/com/saikou/sozo_tv/presentation/screens/episodes/EpisodeScreen.kt
+++ b/app/src/main/java/com/saikou/sozo_tv/presentation/screens/episodes/EpisodeScreen.kt
@@ -115,7 +115,7 @@ class EpisodeScreen : Fragment() {
binding.topContainer.gone()
binding.tabRv.gone()
binding.loadingLayout.visible()
- binding.loadingText.text = "Media is loading.."
+ binding.loadingText.text = getString(R.string.media_is_loading)
}
is Resource.Success -> {
@@ -200,7 +200,7 @@ class EpisodeScreen : Fragment() {
Resource.Loading -> {
binding.placeHolder.root.gone()
- binding.loadingText.text = "Episodes are loading.."
+ binding.loadingText.text = getString(R.string.episodes_are_loading)
// Only take the screen over while there is nothing to show. Hiding a populated
// grid on every page switch threw D-pad focus back out of the list.
if (adapter.itemCount == 0) {
diff --git a/app/src/main/java/com/saikou/sozo_tv/presentation/screens/profile/MyAccountPage.kt b/app/src/main/java/com/saikou/sozo_tv/presentation/screens/profile/MyAccountPage.kt
index 9d31ceda..83b11cae 100644
--- a/app/src/main/java/com/saikou/sozo_tv/presentation/screens/profile/MyAccountPage.kt
+++ b/app/src/main/java/com/saikou/sozo_tv/presentation/screens/profile/MyAccountPage.kt
@@ -32,6 +32,10 @@ import com.saikou.sozo_tv.utils.loadImage
import com.saikou.sozo_tv.utils.visible
import kotlinx.coroutines.launch
import org.koin.androidx.viewmodel.ext.android.activityViewModel
+import com.saikou.sozo_tv.components.TvOptionChipView
+import androidx.core.os.LocaleListCompat
+import androidx.appcompat.app.AppCompatDelegate
+import android.widget.LinearLayout
class MyAccountPage : Fragment() {
@@ -121,6 +125,7 @@ class MyAccountPage : Fragment() {
settingsViewModel.loadStats()
setupAppearanceSection()
+ setupLanguageSection()
setupContentControlsSection()
}
@@ -201,7 +206,7 @@ class MyAccountPage : Fragment() {
if (host is AuthNavigator) {
host.openLogin()
} else {
- Toast.makeText(requireContext(), "Login page not connected yet", Toast.LENGTH_SHORT)
+ Toast.makeText(requireContext(), getString(R.string.login_not_connected), Toast.LENGTH_SHORT)
.show()
}
}
@@ -212,6 +217,45 @@ class MyAccountPage : Fragment() {
setupThemeDemo(binding)
}
+ /**
+ * The in-app language picker.
+ *
+ * Android 13+ exposes a per-app language in the system settings, and
+ * `locales_config.xml` is what fills it — but the device this matters most
+ * on is a cheap TV box that shipped in a language its owner did not choose
+ * and would not know to change system-wide. So the same list is offered
+ * here, where they are already looking.
+ *
+ * `setApplicationLocales` recreates the activity itself; the AppCompat
+ * backport carries that down to API 21, so nothing here is gated on 13.
+ */
+ private fun setupLanguageSection() {
+ val container = binding.languageChips
+ container.removeAllViews()
+
+ val current = AppCompatDelegate.getApplicationLocales()
+ .takeIf { !it.isEmpty }?.get(0)?.language
+ ?: resources.configuration.locales[0].language
+
+ SUPPORTED_LOCALES.forEach { (tag, nativeName) ->
+ val chip = TvOptionChipView(requireContext()).apply {
+ text = nativeName
+ isSelected = tag == current
+ layoutParams = LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.WRAP_CONTENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT,
+ ).apply { marginEnd = (12 * resources.displayMetrics.density).toInt() }
+ setOnClickListener {
+ if (tag == current) return@setOnClickListener
+ AppCompatDelegate.setApplicationLocales(
+ LocaleListCompat.forLanguageTags(tag)
+ )
+ }
+ }
+ container.addView(chip)
+ }
+ }
+
private fun setupContentControlsSection() {
binding.contentControlsDropdown.setExpanded(
preferenceManager.isContentControlsExpanded(), animate = false
@@ -252,11 +296,22 @@ class MyAccountPage : Fragment() {
}
private fun updateContentControlsHeader() {
- val ch = if (preferenceManager.isChannelEnabled()) "Enabled" else "Disabled"
- val ns = if (preferenceManager.isNsfwEnabled()) "Enabled" else "Disabled"
+ val ch = getString(
+ if (preferenceManager.isChannelEnabled()) R.string.status_enabled
+ else R.string.status_disabled
+ )
+ val ns = getString(
+ if (preferenceManager.isNsfwEnabled()) R.string.status_enabled
+ else R.string.status_disabled
+ )
- binding.contentControlsDropdown.setSummary("Home Channels: $ch • Adult: $ns")
- binding.contentControlsDropdown.setBadge(if (preferenceManager.isNsfwEnabled()) "18+" else null)
+ binding.contentControlsDropdown.setSummary(
+ getString(R.string.content_controls_summary, ch, ns)
+ )
+ // "18+" is an age mark, not a word — the same in every language here.
+ binding.contentControlsDropdown.setBadge(
+ if (preferenceManager.isNsfwEnabled()) "18+" else null
+ )
}
private fun showNsfwWarningDialog() {
@@ -491,4 +546,27 @@ class MyAccountPage : Fragment() {
commit()
}
}
+
+ private companion object {
+ /**
+ * Kept in step with `res/xml/locales_config.xml`.
+ *
+ * Native names on purpose: someone looking for their own language finds
+ * it fastest written the way they write it, which is the whole point on
+ * a television that is currently speaking the wrong one.
+ */
+ val SUPPORTED_LOCALES = listOf(
+ "en" to "English",
+ "uz" to "O‘zbekcha",
+ "ru" to "Русский",
+ "de" to "Deutsch",
+ "nl" to "Nederlands",
+ "es" to "Español",
+ "pt" to "Português",
+ "fr" to "Français",
+ "tr" to "Türkçe",
+ "id" to "Bahasa Indonesia",
+ )
+ }
+
}
diff --git a/app/src/main/java/com/saikou/sozo_tv/presentation/screens/search/SearchScreen.kt b/app/src/main/java/com/saikou/sozo_tv/presentation/screens/search/SearchScreen.kt
index 14a6f252..679bd352 100644
--- a/app/src/main/java/com/saikou/sozo_tv/presentation/screens/search/SearchScreen.kt
+++ b/app/src/main/java/com/saikou/sozo_tv/presentation/screens/search/SearchScreen.kt
@@ -111,7 +111,7 @@ class SearchScreen : Fragment() {
if (searchEverywhere && !searchAllSources) {
searchAllSources = true
- binding.searchScopeToggle.text = "All sources"
+ binding.searchScopeToggle.text = getString(R.string.search_scope_all_sources)
}
binding.searchEdt.setText(query)
@@ -196,7 +196,7 @@ class SearchScreen : Fragment() {
isListening = true
onUi {
showVoiceOverlay(true)
- binding.voiceListeningOverlay.listeningTxt.text = "Listening..."
+ binding.voiceListeningOverlay.listeningTxt.text = getString(R.string.voice_listening)
binding.micBtn.setImageResource(R.drawable.ic_mic)
}
}
@@ -204,7 +204,7 @@ class SearchScreen : Fragment() {
override fun onBeginningOfSpeech() {
Log.d("SearchScreen", "Beginning of speech")
onUi {
- binding.voiceListeningOverlay.listeningTxt.text = "Listening... Speak now"
+ binding.voiceListeningOverlay.listeningTxt.text = getString(R.string.voice_listening_speak)
}
}
@@ -290,7 +290,7 @@ class SearchScreen : Fragment() {
showVoiceOverlay(false)
}, 1500)
} else {
- binding.voiceListeningOverlay.listeningTxt.text = "No speech detected"
+ binding.voiceListeningOverlay.listeningTxt.text = getString(R.string.voice_no_speech)
showVoiceOverlay(true)
binding.voiceListeningOverlay.root.postDelayed({
showVoiceOverlay(false)
@@ -366,7 +366,7 @@ class SearchScreen : Fragment() {
}
showVoiceOverlay(true)
- binding.voiceListeningOverlay.listeningTxt.text = "Starting…"
+ binding.voiceListeningOverlay.listeningTxt.text = getString(R.string.voice_starting)
speechRecognizer?.startListening(intent)
} catch (e: Exception) {
@@ -392,11 +392,11 @@ class SearchScreen : Fragment() {
if (intent.resolveActivity(requireContext().packageManager) != null) {
showVoiceOverlay(true)
- binding.voiceListeningOverlay.listeningTxt.text = "Starting voice search..."
+ binding.voiceListeningOverlay.listeningTxt.text = getString(R.string.voice_starting_search)
startActivityForResult(intent, VOICE_REQUEST_CODE)
} else {
Log.e("SearchScreen", "No speech recognition activity found")
- binding.voiceListeningOverlay.listeningTxt.text = "Voice search not available"
+ binding.voiceListeningOverlay.listeningTxt.text = getString(R.string.voice_unavailable)
showVoiceOverlay(true)
binding.voiceListeningOverlay.root.postDelayed({
showVoiceOverlay(false)
@@ -404,7 +404,7 @@ class SearchScreen : Fragment() {
}
} catch (e: Exception) {
Log.e("SearchScreen", "Alternative voice search error: ${e.message}")
- binding.voiceListeningOverlay.listeningTxt.text = "Voice search error"
+ binding.voiceListeningOverlay.listeningTxt.text = getString(R.string.voice_error)
showVoiceOverlay(true)
binding.voiceListeningOverlay.root.postDelayed({
showVoiceOverlay(false)
@@ -435,7 +435,7 @@ class SearchScreen : Fragment() {
binding.searchEdt.setText(spokenText)
binding.searchEdt.setSelection(spokenText.length)
performSearchImmediate(spokenText)
- binding.voiceListeningOverlay.listeningTxt.text = "Searching for: $spokenText"
+ binding.voiceListeningOverlay.listeningTxt.text = getString(R.string.voice_searching_for, spokenText)
showVoiceOverlay(true)
binding.voiceListeningOverlay.root.postDelayed({
showVoiceOverlay(false)
@@ -444,7 +444,7 @@ class SearchScreen : Fragment() {
} else {
binding.micBtn.setImageResource(R.drawable.ic_mic)
if (resultCode == Activity.RESULT_CANCELED) {
- binding.voiceListeningOverlay.listeningTxt.text = "Voice search cancelled"
+ binding.voiceListeningOverlay.listeningTxt.text = getString(R.string.voice_cancelled)
showVoiceOverlay(true)
binding.voiceListeningOverlay.root.postDelayed({
showVoiceOverlay(false)
@@ -565,7 +565,7 @@ class SearchScreen : Fragment() {
val title = searchModel.title?.trim().orEmpty()
if (title.isEmpty()) return
- Toast.makeText(requireContext(), "Opening “$title”…", Toast.LENGTH_SHORT).show()
+ Toast.makeText(requireContext(), getString(R.string.opening_title, title), Toast.LENGTH_SHORT).show()
viewLifecycleOwner.lifecycleScope.launch {
val regId = withContext(Dispatchers.IO) {
try {
diff --git a/app/src/main/java/com/saikou/sozo_tv/presentation/screens/splash/SplashScreen.kt b/app/src/main/java/com/saikou/sozo_tv/presentation/screens/splash/SplashScreen.kt
index 2037d512..d1e5af1f 100644
--- a/app/src/main/java/com/saikou/sozo_tv/presentation/screens/splash/SplashScreen.kt
+++ b/app/src/main/java/com/saikou/sozo_tv/presentation/screens/splash/SplashScreen.kt
@@ -216,7 +216,7 @@ class SplashScreen : Fragment() {
}
binding.loadingIndicator.visible()
binding.setupStatus.visible()
- binding.setupStatus.text = "Setting up sources…"
+ binding.setupStatus.text = getString(R.string.splash_setting_up_sources)
// The install outlives this fragment, so its progress callback must not pin the view
// hierarchy: hold the screen weakly and drop the update once the view is gone.
val handler = Handler(Looper.getMainLooper())
diff --git a/app/src/main/java/com/saikou/sozo_tv/presentation/screens/tv_garden/FilterDialogGarden.kt b/app/src/main/java/com/saikou/sozo_tv/presentation/screens/tv_garden/FilterDialogGarden.kt
index 6082cc77..3279eda4 100644
--- a/app/src/main/java/com/saikou/sozo_tv/presentation/screens/tv_garden/FilterDialogGarden.kt
+++ b/app/src/main/java/com/saikou/sozo_tv/presentation/screens/tv_garden/FilterDialogGarden.kt
@@ -30,9 +30,9 @@ class FilterDialogGarden : DialogFragment() {
super.onViewCreated(view, savedInstanceState)
setupDialogWindow()
- binding.categoryContainer.title.text = "By Category"
- binding.countryContainer.title.text = "By Country"
- binding.customContainer.title.text = "Custom List"
+ binding.categoryContainer.title.text = getString(R.string.garden_by_category)
+ binding.countryContainer.title.text = getString(R.string.garden_by_country)
+ binding.customContainer.title.text = getString(R.string.garden_custom_list)
binding.close.setOnClickListener { dismiss() }
diff --git a/app/src/main/java/com/saikou/sozo_tv/presentation/screens/tv_garden/TvGardenScreen.kt b/app/src/main/java/com/saikou/sozo_tv/presentation/screens/tv_garden/TvGardenScreen.kt
index 0691e9e7..c81f8541 100644
--- a/app/src/main/java/com/saikou/sozo_tv/presentation/screens/tv_garden/TvGardenScreen.kt
+++ b/app/src/main/java/com/saikou/sozo_tv/presentation/screens/tv_garden/TvGardenScreen.kt
@@ -71,7 +71,7 @@ class TvGardenScreen : Fragment() {
intent.putExtra("data", it)
requireActivity().startActivity(intent)
} else {
- Toast.makeText(requireContext(), "No stream available", Toast.LENGTH_SHORT)
+ Toast.makeText(requireContext(), getString(R.string.no_stream_available), Toast.LENGTH_SHORT)
.show()
}
}
diff --git a/app/src/main/res/layout/account_item.xml b/app/src/main/res/layout/account_item.xml
index 5c125025..5c3130f5 100644
--- a/app/src/main/res/layout/account_item.xml
+++ b/app/src/main/res/layout/account_item.xml
@@ -53,7 +53,7 @@
android:maxLines="1"
android:scrollHorizontally="true"
android:singleLine="true"
- android:text="Account"
+ android:text="@string/account"
android:textColor="@color/color_black_white_text"
android:textSize="@dimen/tv_text_label"
tools:text="@string/eshonov_fakhriyor" />
diff --git a/app/src/main/res/layout/activity_live_tv.xml b/app/src/main/res/layout/activity_live_tv.xml
index 1e113d04..b3db4b2a 100644
--- a/app/src/main/res/layout/activity_live_tv.xml
+++ b/app/src/main/res/layout/activity_live_tv.xml
@@ -42,7 +42,7 @@
android:layout_height="wrap_content"
android:fontFamily="@font/days"
android:padding="8dp"
- android:text="Channel is loading..."
+ android:text="@string/channel_is_loading"
android:textAlignment="center"
android:textColor="@color/lb_tv_white" />
@@ -73,7 +73,7 @@
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:gravity="center"
- android:text="Connection Error"
+ android:text="@string/connection_error"
android:textColor="@android:color/white"
android:textSize="@dimen/tv_text_subtitle" />
diff --git a/app/src/main/res/layout/banner_item.xml b/app/src/main/res/layout/banner_item.xml
index 9cf0d729..a013714d 100644
--- a/app/src/main/res/layout/banner_item.xml
+++ b/app/src/main/res/layout/banner_item.xml
@@ -90,7 +90,7 @@
android:lines="1"
android:maxEms="8"
android:maxLines="1"
- android:text="Play Now"
+ android:text="@string/play_now"
android:textAlignment="center"
android:textColor="@color/login_button_text_color_selected_state"
android:textSize="@dimen/tv_text_label" />
diff --git a/app/src/main/res/layout/contact_screen.xml b/app/src/main/res/layout/contact_screen.xml
index 77524bef..4d81fc28 100644
--- a/app/src/main/res/layout/contact_screen.xml
+++ b/app/src/main/res/layout/contact_screen.xml
@@ -54,7 +54,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/poppins_bold"
- android:text="Follow us on social"
+ android:text="@string/follow_us_on_social"
android:textSize="@dimen/tv_text_body" />
diff --git a/app/src/main/res/layout/content_header_menu_main_tv.xml b/app/src/main/res/layout/content_header_menu_main_tv.xml
index 1a8a351c..37d86020 100644
--- a/app/src/main/res/layout/content_header_menu_main_tv.xml
+++ b/app/src/main/res/layout/content_header_menu_main_tv.xml
@@ -51,7 +51,7 @@
android:maxEms="9"
android:maxLines="1"
android:paddingHorizontal="3dp"
- android:text="Profile"
+ android:text="@string/profile"
android:textColor="@color/color_black_white_text"
android:textSize="@dimen/tv_text_body"
app:layout_constraintEnd_toEndOf="parent"
diff --git a/app/src/main/res/layout/custom_tv_keyboard.xml b/app/src/main/res/layout/custom_tv_keyboard.xml
index 98f72ec2..77452139 100644
--- a/app/src/main/res/layout/custom_tv_keyboard.xml
+++ b/app/src/main/res/layout/custom_tv_keyboard.xml
@@ -384,7 +384,7 @@
android:layout_height="@dimen/tv_keyboard_key_h"
android:layout_margin="@dimen/tv_keyboard_key_gap"
android:textSize="@dimen/tv_keyboard_key_text"
- android:text=""" />
+ android:text="@string/quot" />
+ android:text="@string/amp" />
+ android:text="@string/space" />
diff --git a/app/src/main/res/layout/episode_screen.xml b/app/src/main/res/layout/episode_screen.xml
index f045918f..0e15582d 100644
--- a/app/src/main/res/layout/episode_screen.xml
+++ b/app/src/main/res/layout/episode_screen.xml
@@ -92,7 +92,7 @@
android:duplicateParentState="true"
android:fontFamily="@font/days"
android:gravity="center"
- android:text="Wrong Title?"
+ android:text="@string/find_correct_anime_title"
android:textColor="@color/login_button_text_color"
android:textSize="@dimen/tv_text_body" />
diff --git a/app/src/main/res/layout/exit_dialog.xml b/app/src/main/res/layout/exit_dialog.xml
index efa4e801..128fc3a7 100644
--- a/app/src/main/res/layout/exit_dialog.xml
+++ b/app/src/main/res/layout/exit_dialog.xml
@@ -21,7 +21,7 @@
android:layout_marginStart="14dp"
android:layout_marginTop="8dp"
android:fontFamily="@font/days"
- android:text="Exit Account"
+ android:text="@string/exit"
android:textColor="@color/white40"
android:textSize="@dimen/tv_text_subtitle"
app:layout_constraintStart_toStartOf="parent"
@@ -65,7 +65,7 @@
android:layout_gravity="center"
android:layout_marginTop="12dp"
android:fontFamily="@font/days"
- android:text="Account "
+ android:text="@string/account_2"
android:textColor="@color/lb_tv_white"
android:textSize="@dimen/tv_text_subtitle" />
@@ -94,7 +94,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:fontFamily="@font/days"
- android:text="Are you sure you want to exit ?"
+ android:text="@string/are_you_sure_you_want_to"
android:textColor="@color/lb_tv_white"
android:textSize="@dimen/tv_text_subtitle" />
@@ -127,7 +127,7 @@
android:fontFamily="@font/days"
android:paddingVertical="13dp"
- android:text="Not Now"
+ android:text="@string/not_now"
android:textAlignment="center"
android:textColor="@color/color_item_tv_category_tv"
android:textSize="@dimen/tv_text_label" />
@@ -155,7 +155,7 @@
android:fontFamily="@font/days"
android:paddingVertical="13dp"
- android:text="Yes Continue"
+ android:text="@string/yes_continue"
android:textAlignment="center"
android:textColor="@color/color_item_tv_category_tv"
android:textSize="@dimen/tv_text_label" />
diff --git a/app/src/main/res/layout/filter_dialog.xml b/app/src/main/res/layout/filter_dialog.xml
index 71f4a2f0..7963586d 100644
--- a/app/src/main/res/layout/filter_dialog.xml
+++ b/app/src/main/res/layout/filter_dialog.xml
@@ -20,7 +20,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
- android:text="Filter "
+ android:text="@string/filter"
android:textColor="@android:color/white"
android:textSize="18sp"
android:textStyle="bold" />
@@ -51,7 +51,7 @@
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"
- android:hint="Choose Sort"
+ android:hint="@string/choose_sort"
android:padding="10dp"
android:textColor="@color/lb_tv_white"
android:textColorHint="@color/lb_tv_white"
@@ -80,7 +80,7 @@
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center"
- android:hint="Choose Year"
+ android:hint="@string/choose_year"
android:padding="10dp"
android:textColor="@color/lb_tv_white"
android:textColorHint="@color/lb_tv_white"
@@ -115,7 +115,7 @@
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:fontFamily="@font/days"
- android:text="Choose Rating:"
+ android:text="@string/choose_rating"
android:textColor="@color/white40"
android:textSize="@dimen/tv_text_body" />
@@ -166,7 +166,7 @@
android:fontFamily="@font/days"
android:paddingVertical="13dp"
- android:text="Apply"
+ android:text="@string/apply"
android:textAlignment="center"
android:textColor="@color/color_item_tv_category_tv"
android:textSize="@dimen/tv_text_label" />
diff --git a/app/src/main/res/layout/filter_dialog_garden.xml b/app/src/main/res/layout/filter_dialog_garden.xml
index ec6ee3ef..e04a1d9a 100644
--- a/app/src/main/res/layout/filter_dialog_garden.xml
+++ b/app/src/main/res/layout/filter_dialog_garden.xml
@@ -19,7 +19,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
- android:text="Filter Tv Garden"
+ android:text="@string/filter_tv_garden"
android:textColor="@android:color/white"
android:textSize="18sp"
android:textStyle="bold" />
@@ -56,7 +56,7 @@
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:fontFamily="@font/days"
- android:text="Choose Type:"
+ android:text="@string/choose_type"
android:textColor="@color/white40"
android:textSize="@dimen/tv_text_body" />
@@ -113,7 +113,7 @@
android:fontFamily="@font/days"
android:paddingVertical="13dp"
- android:text="Apply"
+ android:text="@string/apply"
android:textAlignment="center"
android:textColor="@color/color_item_tv_category_tv"
android:textSize="@dimen/tv_text_label" />
diff --git a/app/src/main/res/layout/header_source.xml b/app/src/main/res/layout/header_source.xml
index 272516c0..b7b7db85 100644
--- a/app/src/main/res/layout/header_source.xml
+++ b/app/src/main/res/layout/header_source.xml
@@ -22,7 +22,7 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:fontFamily="@font/days"
- android:text="Providers"
+ android:text="@string/providers"
android:textColor="#FFFFFF"
android:textSize="22sp"
tools:ignore="HardcodedText" />
@@ -66,7 +66,7 @@
android:nextFocusDown="@id/btnTabServer"
android:paddingHorizontal="20dp"
android:paddingVertical="9dp"
- android:text="Update sources"
+ android:text="@string/sources_update"
android:textColor="@color/tv_field_text"
android:textSize="@dimen/tv_text_body"
tools:ignore="HardcodedText" />
@@ -91,7 +91,7 @@
android:nextFocusUp="@id/btnUpdateSources"
android:paddingHorizontal="26dp"
android:paddingVertical="10dp"
- android:text="Sozo"
+ android:text="@string/sozo"
android:textColor="#111417"
android:textSize="@dimen/tv_text_subtitle"
tools:ignore="HardcodedText" />
@@ -108,7 +108,7 @@
android:nextFocusUp="@id/btnUpdateSources"
android:paddingHorizontal="26dp"
android:paddingVertical="10dp"
- android:text="Aniyomi"
+ android:text="@string/aniyomi"
android:textColor="#CCCCCC"
android:textSize="@dimen/tv_text_subtitle" />
@@ -124,7 +124,7 @@
android:nextFocusUp="@id/btnUpdateSources"
android:paddingHorizontal="26dp"
android:paddingVertical="10dp"
- android:text="CloudStream"
+ android:text="@string/cloudstream"
android:textColor="#CCCCCC"
android:textSize="@dimen/tv_text_subtitle" />
@@ -143,7 +143,7 @@
android:layout_marginTop="16dp"
android:background="@drawable/bg_button_tv"
android:drawablePadding="8dp"
- android:hint="Search providers…"
+ android:hint="@string/search_providers"
android:imeOptions="actionSearch"
android:inputType="text"
android:maxLines="1"
@@ -196,7 +196,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="center"
- android:text="No providers available."
+ android:text="@string/sources_empty"
android:textColor="#888888"
android:textSize="@dimen/tv_text_body"
android:visibility="gone" />
diff --git a/app/src/main/res/layout/history_page.xml b/app/src/main/res/layout/history_page.xml
index 8f814f6d..8f467704 100644
--- a/app/src/main/res/layout/history_page.xml
+++ b/app/src/main/res/layout/history_page.xml
@@ -57,7 +57,7 @@
android:gravity="center"
android:paddingHorizontal="8dp"
android:paddingVertical="8dp"
- android:text="Clear"
+ android:text="@string/clear"
android:textColor="@color/color_item_tv_category_tv"
android:textSize="@dimen/tv_text_body"
tools:text="Clear" />
diff --git a/app/src/main/res/layout/item_bottombar.xml b/app/src/main/res/layout/item_bottombar.xml
index 1f02d78e..537f97c5 100644
--- a/app/src/main/res/layout/item_bottombar.xml
+++ b/app/src/main/res/layout/item_bottombar.xml
@@ -27,7 +27,7 @@
@@ -59,7 +59,7 @@
@@ -70,7 +70,7 @@
android:layout_marginStart="4dp"
android:fontFamily="@font/inter_bold"
android:paddingTop="4dp"
- android:text="Characters"
+ android:text="@string/characters"
android:textSize="@dimen/tv_text_label" />
@@ -91,7 +91,7 @@
@@ -102,7 +102,7 @@
android:layout_marginStart="4dp"
android:fontFamily="@font/inter_bold"
android:paddingTop="4dp"
- android:text="Tv Channels"
+ android:text="@string/tv_channels"
android:textSize="@dimen/tv_text_label" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_bottombar_garden.xml b/app/src/main/res/layout/item_bottombar_garden.xml
index 12bdec14..7a9ae202 100644
--- a/app/src/main/res/layout/item_bottombar_garden.xml
+++ b/app/src/main/res/layout/item_bottombar_garden.xml
@@ -27,7 +27,7 @@
@@ -58,7 +58,7 @@
@@ -68,7 +68,7 @@
android:layout_height="wrap_content"
android:paddingTop="4dp"
android:layout_marginStart="4dp"
- android:text="by Category"
+ android:text="@string/by_category"
android:fontFamily="@font/inter_bold"
android:textSize="@dimen/tv_text_label" />
diff --git a/app/src/main/res/layout/item_channel.xml b/app/src/main/res/layout/item_channel.xml
index b007570d..f038b91b 100644
--- a/app/src/main/res/layout/item_channel.xml
+++ b/app/src/main/res/layout/item_channel.xml
@@ -23,7 +23,7 @@
android:ellipsize="end"
android:gravity="center"
android:maxLines="2"
- android:text="Channel Name"
+ android:text="@string/channel_name"
android:textColor="@color/netflix_text_primary"
android:textSize="@dimen/tv_text_body"
android:textStyle="bold" />
@@ -45,7 +45,7 @@
@@ -55,7 +55,7 @@
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="8dp"
- android:contentDescription="Geo-blocked"
+ android:contentDescription="@string/geo_blocked"
android:src="@drawable/ic_geo_blocked"
android:visibility="gone"
app:tint="@color/netflix_gray" />
@@ -70,7 +70,7 @@
android:ellipsize="end"
android:gravity="center"
android:maxLines="1"
- android:text="Country • Language"
+ android:text="@string/country_language"
android:textColor="@color/netflix_text_secondary"
android:textSize="@dimen/tv_text_size_small" />
diff --git a/app/src/main/res/layout/item_channel_section.xml b/app/src/main/res/layout/item_channel_section.xml
index 49608dbd..b46f27c0 100644
--- a/app/src/main/res/layout/item_channel_section.xml
+++ b/app/src/main/res/layout/item_channel_section.xml
@@ -73,7 +73,7 @@
android:id="@+id/channel_status_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="Disabled"
+ android:text="@string/disabled"
android:textColor="@color/netflix_gray"
android:textSize="@dimen/tv_text_label"
android:fontFamily="sans-serif"/>
diff --git a/app/src/main/res/layout/item_content_mode.xml b/app/src/main/res/layout/item_content_mode.xml
index b30d3656..bd8f5204 100644
--- a/app/src/main/res/layout/item_content_mode.xml
+++ b/app/src/main/res/layout/item_content_mode.xml
@@ -10,7 +10,7 @@
android:id="@+id/contentModeTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="Content Mode"
+ android:text="@string/content_mode"
android:textSize="18sp"
android:textColor="@color/netflix_white"
android:fontFamily="sans-serif-medium"
@@ -20,7 +20,7 @@
android:id="@+id/contentModeSubtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:text="Choose your preferred content type"
+ android:text="@string/choose_your_preferred_content_type"
android:textSize="@dimen/tv_text_body"
android:textColor="@color/netflix_gray"
android:layout_marginBottom="16dp" />
@@ -37,7 +37,7 @@
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
- android:text="Anime Mode"
+ android:text="@string/anime_mode"
android:textColor="@color/netflix_text_primary"
android:gravity="center"
android:background="@drawable/switch_background" />
@@ -47,7 +47,7 @@
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
- android:text="Movie Mode"
+ android:text="@string/movie_mode"
android:textColor="@color/netflix_text_primary"
android:gravity="center"
android:background="@drawable/switch_selected_background" />
diff --git a/app/src/main/res/layout/item_content_mode_section.xml b/app/src/main/res/layout/item_content_mode_section.xml
index 2f8c0eb4..0b7acebc 100644
--- a/app/src/main/res/layout/item_content_mode_section.xml
+++ b/app/src/main/res/layout/item_content_mode_section.xml
@@ -9,7 +9,7 @@
+ android:text="@string/episode_2_will_be_released_in" />
@@ -52,7 +52,7 @@
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:lineSpacingExtra="2dp"
- android:text="Your watchlist has been updated with 5 new releases. Don't miss out on the latest content."
+ android:text="@string/your_watchlist_has_been_updated_with"
android:textColor="@color/notification_description_color"
android:textSize="@dimen/tv_text_body" />
@@ -70,7 +70,7 @@
android:drawableStart="@drawable/ic_time_small"
android:drawablePadding="6dp"
android:gravity="center_vertical"
- android:text="5 hours ago"
+ android:text="@string/news_time_sample"
android:textColor="@color/notification_time_color"
android:textSize="@dimen/tv_text_label" />
@@ -81,7 +81,7 @@
android:background="@drawable/notification_action_bg"
android:paddingHorizontal="12dp"
android:paddingVertical="6dp"
- android:text="update"
+ android:text="@string/update"
android:textColor="@color/notification_action_color"
android:textSize="@dimen/tv_text_label" />
diff --git a/app/src/main/res/layout/item_play_recommended.xml b/app/src/main/res/layout/item_play_recommended.xml
index 6da7864d..9cf1387e 100644
--- a/app/src/main/res/layout/item_play_recommended.xml
+++ b/app/src/main/res/layout/item_play_recommended.xml
@@ -13,7 +13,7 @@
android:layout_height="wrap_content"
android:layout_marginStart="14dp"
android:fontFamily="@font/days"
- android:text="Maybe You Like"
+ android:text="@string/maybe_you_like"
android:textColor="@color/lb_tv_white"
android:textSize="@dimen/tv_text_subtitle"
app:layout_constraintStart_toStartOf="parent"
diff --git a/app/src/main/res/layout/item_tab_chip.xml b/app/src/main/res/layout/item_tab_chip.xml
index df4a5bd2..5b7b2957 100644
--- a/app/src/main/res/layout/item_tab_chip.xml
+++ b/app/src/main/res/layout/item_tab_chip.xml
@@ -12,7 +12,7 @@
android:paddingStart="18dp"
android:paddingEnd="18dp"
- android:text="Watching"
+ android:text="@string/watching"
android:textAllCaps="false"
android:textColor="@color/color_item_tv_category_tv"
android:textSize="@dimen/tv_text_body" />
diff --git a/app/src/main/res/layout/my_account_page.xml b/app/src/main/res/layout/my_account_page.xml
index a4e48d8c..4bd009e3 100644
--- a/app/src/main/res/layout/my_account_page.xml
+++ b/app/src/main/res/layout/my_account_page.xml
@@ -446,7 +446,7 @@
android:layout_marginBottom="14dp"
app:expanded="false"
app:sectionSubtitle="@string/customize_subtitle_font_color_and_size_tv_friendly"
- app:sectionTitle="Subtitle Style">
+ app:sectionTitle="@string/section_subtitle_style">
@@ -546,7 +546,7 @@
android:layout_weight="1"
android:fontFamily="monospace"
android:gravity="center"
- android:text="Mono"
+ android:text="@string/subtitle_font_mono"
android:textColor="@color/netflix_text_primary"
android:textSize="@dimen/tv_text_body" />
@@ -556,7 +556,7 @@
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:fontFamily="sans-serif-medium"
- android:text="Text Size"
+ android:text="@string/text_size"
android:textColor="@color/netflix_white"
android:textSize="@dimen/tv_text_body" />
@@ -576,7 +576,7 @@
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:fontFamily="sans-serif-medium"
- android:text="Text Color"
+ android:text="@string/text_color"
android:textColor="@color/netflix_white"
android:textSize="@dimen/tv_text_body" />
@@ -700,7 +700,7 @@
android:layout_height="wrap_content"
android:layout_marginBottom="10dp"
android:fontFamily="sans-serif-medium"
- android:text="Subtitle Background"
+ android:text="@string/subtitle_background_2"
android:textColor="@color/netflix_white"
android:textSize="@dimen/tv_text_body" />
@@ -718,7 +718,7 @@
android:layout_marginEnd="6dp"
android:layout_weight="1"
android:fontFamily="sans-serif-medium"
- android:text="Off"
+ android:text="@string/off"
android:textColor="@color/netflix_text_primary" />
@@ -753,7 +753,7 @@
android:layout_marginEnd="6dp"
android:layout_weight="1"
android:fontFamily="sans-serif-medium"
- android:text="Off"
+ android:text="@string/off"
android:textColor="@color/netflix_text_primary" />
+ app:sectionTitle="@string/section_theme">
+ app:subtitleText="@string/theme_winter_mode_hint"
+ app:titleText="@string/theme_winter_mode" />
+
+
+
+
+
+
+
+
+
+
+ app:sectionSubtitle="@string/section_content_privacy_hint"
+ app:sectionTitle="@string/section_content_privacy">
@@ -47,7 +47,7 @@
android:gravity="center"
android:maxWidth="360dp"
android:lineSpacingExtra="1dp"
- android:text="You are about to enable adult content (NSFW). This will show mature content that may not be suitable for all audiences."
+ android:text="@string/you_are_about_to_enable_adult"
android:textColor="#9ca3af"
android:textSize="@dimen/tv_text_body" />
@@ -85,7 +85,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-medium"
- android:text="Age Verification"
+ android:text="@string/age_verification"
android:textColor="#ffffff"
android:textSize="@dimen/tv_text_subtitle" />
@@ -94,7 +94,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:lineSpacingExtra="1dp"
- android:text="You must be 18+ years old to view adult content"
+ android:text="@string/you_must_be_18_years_old"
android:textColor="#9ca3af"
android:textSize="@dimen/tv_text_body" />
@@ -128,7 +128,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-medium"
- android:text="Content Visibility"
+ android:text="@string/content_visibility"
android:textColor="#ffffff"
android:textSize="@dimen/tv_text_subtitle" />
@@ -137,7 +137,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:lineSpacingExtra="1dp"
- android:text="Adult content will be visible across the platform"
+ android:text="@string/adult_content_will_be_visible_across"
android:textColor="#9ca3af"
android:textSize="@dimen/tv_text_body" />
@@ -170,7 +170,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-medium"
- android:text="Easy to Disable"
+ android:text="@string/easy_to_disable"
android:textColor="#ffffff"
android:textSize="@dimen/tv_text_subtitle" />
@@ -179,7 +179,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:lineSpacingExtra="1dp"
- android:text="You can turn this off anytime in your account settings"
+ android:text="@string/you_can_turn_this_off_anytime"
android:textColor="#9ca3af"
android:textSize="@dimen/tv_text_body" />
@@ -207,7 +207,7 @@
android:focusableInTouchMode="true"
android:paddingHorizontal="32dp"
android:paddingVertical="12dp"
- android:text="I Understand, Enable"
+ android:text="@string/i_understand_enable"
android:textColor="@color/color_item_tv_category_tv"
android:textSize="@dimen/tv_text_subtitle"
android:fontFamily="sans-serif-medium" />
diff --git a/app/src/main/res/layout/nsfw_disable_dialog.xml b/app/src/main/res/layout/nsfw_disable_dialog.xml
index 03577010..8626534c 100644
--- a/app/src/main/res/layout/nsfw_disable_dialog.xml
+++ b/app/src/main/res/layout/nsfw_disable_dialog.xml
@@ -38,7 +38,7 @@
android:layout_marginBottom="16dp"
android:fontFamily="sans-serif-medium"
android:gravity="center"
- android:text="NSFW Content Disabled"
+ android:text="@string/nsfw_content_disabled"
android:textColor="#FFFFFF"
android:textSize="24sp"
android:textStyle="bold"
@@ -55,7 +55,7 @@
android:fontFamily="sans-serif"
android:gravity="center"
android:lineSpacingExtra="4dp"
- android:text="Adult content is currently disabled. To access this content, you need to enable NSFW settings in your preferences."
+ android:text="@string/adult_content_is_currently_disabled_to"
android:textColor="#B3B3B3"
android:textSize="@dimen/tv_text_subtitle"
app:layout_constraintBottom_toTopOf="@+id/buttonsContainer"
@@ -85,7 +85,7 @@
android:focusable="true"
android:fontFamily="@font/days"
android:gravity="center"
- android:text="Go to Settings"
+ android:text="@string/action_go_to_settings"
android:textAllCaps="false"
android:textColor="@color/color_black_white_text"
android:textSize="@dimen/tv_text_body" />
@@ -106,7 +106,7 @@
android:focusable="true"
android:fontFamily="sans-serif-medium"
android:gravity="center"
- android:text="Hide"
+ android:text="@string/hide"
android:textAllCaps="false"
android:textColor="@color/color_black_white_text"
android:textSize="@dimen/tv_text_body" />
diff --git a/app/src/main/res/layout/recent_search_item.xml b/app/src/main/res/layout/recent_search_item.xml
index b9855988..12088a8c 100644
--- a/app/src/main/res/layout/recent_search_item.xml
+++ b/app/src/main/res/layout/recent_search_item.xml
@@ -24,7 +24,7 @@
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"
- android:text="Search Query"
+ android:text="@string/search_query"
android:textColor="@color/netflix_white"
android:textSize="@dimen/tv_text_subtitle"
android:maxLines="1"
@@ -39,7 +39,7 @@
android:background="?android:attr/selectableItemBackgroundBorderless"
android:padding="4dp"
android:focusable="true"
- android:contentDescription="Remove search"
+ android:contentDescription="@string/remove_search"
app:tint="@color/netflix_gray" />
diff --git a/app/src/main/res/layout/search_screen.xml b/app/src/main/res/layout/search_screen.xml
index 4643d94f..d0d42263 100644
--- a/app/src/main/res/layout/search_screen.xml
+++ b/app/src/main/res/layout/search_screen.xml
@@ -42,7 +42,7 @@
android:focusable="true"
android:focusableInTouchMode="true"
android:gravity="center_vertical"
- android:hint="Search Anime..."
+ android:hint="@string/search_anime"
android:imeOptions="actionSearch"
android:inputType="text"
android:maxLines="1"
@@ -95,7 +95,7 @@
android:layout_weight="1"
android:ellipsize="end"
android:maxLines="1"
- android:text="Your Search Recommendations"
+ android:text="@string/your_search_recommendations"
android:textSize="17sp" />
- An error occurred
- Dismiss
- Change provider
Search
Main
- Movies
- TV
- Settings
- "Unlock unlimited entertainment with our SozoTv Subscription! Enjoy access to a vast library of movies across various genres, from timeless classics to the latest blockbusters. With new releases every week, you’ll never run out of exciting options to watch. Whether you're into animated films, gripping thrillers, heartwarming dramas, or action-packed adventures, our app has something for everyone. Subscribe now for a seamless movie-watching experience, ad-free, with the ability to watch anytime, anywhere!
- Tv Shows
- Hello blank fragment
- Siz hali tizimga kirmagansiz
- Login Page
- +998
- Subscription Code
- You can use the following methods to log in and register.
- Login
- QR Code orqali kirish
- Boshqa usullar orqali kirish
- Telefon raqamingizni\ntasdiqlang!
- Tasdiqlash
- 00000
- +998 90 364 08 04 telefon raqamiga 6 xonali kod yuborildi, ushbu kodni kiriting!
- Kodni kiriting
- 00:00
Back
Back to Main
- Qayta yuborish
- Tasdiqlash kodini kiriting!
- Shorts
Filter Anime
Tv Garden
- Sources
- telefon raqamiga 6 xonali kod yuborildi, ushbu kodni kiriting!
- Onlayn Tv
Something went wrong. Please try again.
Retry
- Obuna
- Tv Kanallar
- Profile
+ TV Channels
Eshonov Fakhriyor
- Akkount qo`shish
Subscription Status
Active
- Mening filmlarim
- My Subscription
My Info
Bookmarks Page
Sources
Watch History
- Obunalar
Get Messages
Exit Account
There is no movie or serial here right now
Cast details aren\'t available for this source
- Amallar
- Ha, o\'chirish
- Yo\'q, yopish
- Don`t ask me again
- Sizni 20.11.2024gacha faolligingiz bor. akkauntni o\'chirsangiz obuna bekor bo\'ladi.
- Siz rostan ham ushbu akkauntni o\'chirmoqchimisiz?
- One Piece, Naruto, Etc..
+ Actions
Watch Now
Update Now
- Watch Trailer
About Movie
- More Info
Release Year
Studio
- User Ratings
Category
- Rating
Language
- Yangi
- soat
- daqiqa
- Obunalar
- To’lov summasi:
- Ushbu to’lov tizimlari orqali to’lov qiling
- Categories
+ h
+ min
Please do`nt close
Contact
& @sozoApp
@@ -104,11 +41,7 @@
Enable or disable adult content visibility
Enable or disable channel content visibility
Adult Content (NSFW)
- CONTENT SETTINGS
- 0 unread
G
- Recent Searches
- Clear All
Home Page Channels
LIVE TV
Retry
@@ -119,7 +52,6 @@
Episode %1$s is loading…
Switching quality…
Could not switch quality — staying on the current one.
- Part %1$d • Episode %2$d
Loading episodes for part %1$s…
This is the last episode
This is the first episode
@@ -139,17 +71,11 @@
Back
No internet connection
Too many attempts. Try again in a moment.
- Notifications
My Account
Manage your profile and preferences
Guest Account
Login
- STATUS
Visitor
- Content Mode
- Choose your preferred content type
- Anime Mode
- Movie Mode
Customize subtitle font, color and size (TV friendly)
Preview
This is how subtitles will look on TV
@@ -158,18 +84,9 @@
Days
Enable or disable viewing history on home screen
History manager
- Current Selected Source: %1$s
Source
- No speech detected
- Starting voice search…
- Voice search not available
Your Search Recommendations
Results for \"%1$s\"
-
- Choose your preferred banner appearance
- Anime: %s
- Movie: %s
- Anime: %1$s, Movie: %2$s
Style
Off
On
@@ -193,14 +110,11 @@
Not connected — connect it in the Sozo phone app under Profile → Connections
Your AniList list is empty.
Nothing in “%1$s”.
- AniList ID
This device cannot play that format — trying another source…
This TV can\'t play this video format. Pick another quality or server.
Connection lost. Check your network and try again.
That link has expired. Try again, or pick a different server.
Playback failed. Try another server or quality.
- WATCHED
- SAVED
watched
saved
unread
@@ -226,7 +140,6 @@
Nothing matched that title
Go to Settings
Settings
- Current values shown beside each item.
Screen size
Fit
Whole frame visible, bars at the sides
@@ -360,4 +273,149 @@
Added %1$d source(s). They are in the list behind this screen.
Installed, but it added no sources. The link may point at the wrong file.
Could not install it: %1$s
+
+
+ Account
+ Channel is loading...
+ Connection Error
+ Play Now
+ Follow us on social
+ Telegram
+ Profile
+ "
+ &
+ SPACE
+ Qualit Dialog
+ "Account "
+ Are you sure you want to exit ?
+ Not Now
+ Yes Continue
+ "Filter "
+ Choose Rating:
+ Apply
+ Choose Sort
+ Choose Year
+ Filter Tv Garden
+ Choose Type:
+ Providers
+ Sozo
+ Aniyomi
+ CloudStream
+ Search providers…
+ Clear
+ Anime
+ Characters
+ Tv Channels
+ by Country
+ by Category
+ Channel Name
+ LIVE
+ Country • Language
+ Geo-blocked
+ Disabled
+ Content Mode
+ Choose your preferred content type
+ Anime Mode
+ Movie Mode
+ Episode 2 will be released in
+ ACTIVE
+ Watchlist Update
+ Your watchlist has been updated with 5 new releases. Don\'t miss out on the latest content.
+ 5 hours ago
+ update
+ Maybe You Like
+ Watching
+ Text Size
+ Text Color
+ Subtitle Background
+ Adult Content Warning
+ You are about to enable adult content (NSFW). This will show mature content that may not be suitable for all audiences.
+ Age Verification
+ You must be 18+ years old to view adult content
+ Content Visibility
+ Adult content will be visible across the platform
+ Easy to Disable
+ You can turn this off anytime in your account settings
+ I Understand, Enable
+ NSFW Content Disabled
+ Adult content is currently disabled. To access this content, you need to enable NSFW settings in your preferences.
+ Hide
+ Search Query
+ Remove search
+ This source
+ Search Anime...
+ Title
+ By Country
+ By Category
+ 16sp
+ Listening…
+ e.g. 夏目友人帳, Natsume Yuujinchou
+ HomeScreen
+ ContactScreen
+ search_screen
+ categories_screen
+ LiveTvPlayerScreen
+ tv_garden_screen
+ view_all_screen
+ SourceScreen
+ Aniyomi Settings
+ splash_screen
+ DetailPage
+ CastDetailScreen
+ TrailerPlayerScreen
+ EpisodeScreen
+ SeriesPlayerScreen
+ my_account_page
+ bookmark_screen
+ HistoryPage
+ AnilistScreen
+ news_page
+
+
+ Enabled
+ Disabled
+ Episode %1$d
+ By Category
+ By Country
+ Custom List
+ Setting up sources…
+ No stream available
+ Filter Anime
+ Filter Movie
+ Media is loading..
+ Episodes are loading..
+ Age: %1$s
+ Aired!
+ All sources
+ Listening...
+ Listening... Speak now
+ No speech detected
+ Starting…
+ Starting voice search...
+ Voice search not available
+ Voice search error
+ Searching for: %1$s
+ Voice search cancelled
+ Opening \u201c%1$s\u201d…
+ Login page not connected yet
+ Update Available
+ Downloading…
+ Download Complete!
+ Install Now
+ Download Failed
+ Try Again
+
+
+ Winter mode (Surprise Me)
+ Enable / disable Winter theme preview
+ Theme
+ Subtitle Style
+ Content & Privacy
+ Manage what shows on Home
+ Home Channels: %1$s \u2022 Adult: %2$s
+ Language
+ The language Sozo speaks on this television
diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml
new file mode 100644
index 00000000..1445bdae
--- /dev/null
+++ b/app/src/main/res/xml/locales_config.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tools/check_strings.py b/tools/check_strings.py
new file mode 100644
index 00000000..0c187d36
--- /dev/null
+++ b/tools/check_strings.py
@@ -0,0 +1,128 @@
+#!/usr/bin/env python3
+"""Checks the translated string resources against the English originals.
+
+ python3 tools/check_strings.py
+
+## Why this exists
+
+Android resolves a missing string by falling back to `values/`, so a gap is a
+word in the wrong language rather than a crash — invisible to the build, and to
+anyone not reading that language.
+
+A mismatched format specifier is not invisible. `getString(R.string.x, name)`
+against a translation that dropped `%1$s`, or turned it into `%1$d`, throws at
+the moment that screen renders, and only on a device set to that language.
+That is what this fails the build on.
+
+## The two tiers
+
+`en`, `uz` and `ru` are written by the team, so a key missing from them is a
+build failure — someone here can fix it before merging. The other seven are
+machine-translated and topped up in batches; blocking a PR on them would not
+produce seven translations, it would produce seven copies of the English string
+pasted in to get the build green. Those are reported instead.
+"""
+
+import re
+import sys
+import xml.etree.ElementTree as ET
+from pathlib import Path
+
+RES = Path(__file__).resolve().parent.parent / 'app/src/main/res'
+BLOCKING = {'uz', 'ru'}
+
+# Deliberately the same in every language, so a locale file that omits them is
+# correct rather than incomplete. Reporting these would train people to ignore
+# the output, which is the one thing a check like this cannot survive.
+NEVER_TRANSLATED = {
+ # nav-graph destination labels — never drawn on a leanback screen
+ 'homescreen', 'contactscreen', 'search_screen', 'categories_screen',
+ 'livetvplayerscreen', 'tv_garden_screen', 'view_all_screen', 'sourcescreen',
+ 'aniyomi_settings', 'splash_screen', 'detailpage', 'castdetailscreen',
+ 'trailerplayerscreen', 'episodescreen', 'seriesplayerscreen',
+ 'my_account_page', 'bookmark_screen', 'historypage', 'anilistscreen',
+ 'news_page', 'qualit_dialog',
+ # brand and proper nouns
+ 'app_name', 'sozo', 'aniyomi', 'cloudstream', 'telegram', 'anilist', 'mal',
+ 'poppins', 'eshonov_fakhriyor', 'amp_sozoapp',
+ # punctuation, XML entities, layout-preview samples
+ 'quot', 'amp', 'space', 'g', 'meta_separator', 'stepper_value_sample',
+ 'news_time_sample', 'episode_2_will_be_released_in',
+ 'e_g_natsume_yuujinchou', 'plugin_failed_entry', 'plugin_error_other',
+ 'country_language', 'search_results_for',
+}
+FORMAT = re.compile(r'%(?:\d+\$)?[.\d]*[a-zA-Z]')
+
+
+def load(directory):
+ """name -> the text that carries the format specifiers."""
+ out = {}
+ for path in sorted(directory.glob('strings*.xml')):
+ root = ET.parse(path).getroot()
+ for node in root.findall('string'):
+ out[node.get('name')] = ''.join(node.itertext())
+ for node in root.findall('plurals'):
+ item = node.find("item[@quantity='other']") or node.find('item')
+ if item is not None:
+ out[node.get('name')] = ''.join(item.itertext())
+ return out
+
+
+def main():
+ english = {k: v for k, v in load(RES / 'values').items()
+ if k not in NEVER_TRANSLATED}
+ if not english:
+ print(f'No string resources under {RES}/values', file=sys.stderr)
+ return 2
+
+ failures = 0
+ advisory = 0
+
+ for directory in sorted(RES.glob('values-*')):
+ locale = directory.name[len('values-'):]
+ if not (directory / 'strings.xml').exists():
+ continue
+ translated = load(directory)
+
+ # 1. A format specifier that changed shape is a crash on that locale.
+ for name, text in translated.items():
+ if name not in english:
+ print(f'✗ {locale}: "{name}" is not in values/')
+ failures += 1
+ continue
+ want = sorted(FORMAT.findall(english[name]))
+ got = sorted(FORMAT.findall(text))
+ if want != got:
+ print(f'✗ {locale}/{name}: format {got} does not match English {want}')
+ failures += 1
+
+ # 2. A missing key falls back to English — a gap, not a crash.
+ missing = sorted(set(english) - set(translated))
+ if missing:
+ blocks = locale in BLOCKING
+ mark = '✗' if blocks else '·'
+ print(f'{mark} {locale}: {len(missing)} string(s) still in English'
+ f'{"" if blocks else " (advisory)"}')
+ for name in missing[:15]:
+ print(f' {name}')
+ if len(missing) > 15:
+ print(f' … and {len(missing) - 15} more')
+ if blocks:
+ failures += len(missing)
+ else:
+ advisory += len(missing)
+
+ locales = len(list(RES.glob('values-*')))
+ print(f'\n{locales} translated locale(s), {len(english)} string(s) in values/.')
+ if advisory:
+ print(f'{advisory} advisory gap(s) — those render in English until the '
+ f'next translation pass.')
+ if failures:
+ print(f'✗ {failures} problem(s).')
+ return 1
+ print('✓ Every translation matches the English format specifiers.')
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main())