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())