diff --git a/src/android/.gitignore b/src/android/.gitignore index f4e580b2..aa724b77 100644 --- a/src/android/.gitignore +++ b/src/android/.gitignore @@ -13,4 +13,3 @@ .externalNativeBuild .cxx local.properties -app/src/main/assets/hash.txt diff --git a/src/android/app/.gitignore b/src/android/app/.gitignore index 42afabfd..d12cebb5 100644 --- a/src/android/app/.gitignore +++ b/src/android/app/.gitignore @@ -1 +1,4 @@ -/build \ No newline at end of file +/build +assets/hash.txt +*.po +*.pot diff --git a/src/android/app/build.gradle.kts b/src/android/app/build.gradle.kts index dd0541a6..349747b2 100644 --- a/src/android/app/build.gradle.kts +++ b/src/android/app/build.gradle.kts @@ -10,6 +10,7 @@ plugins { alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.kotlinx.gettext) } fun String.runCommand(workingDir: File = File(".")): String? { @@ -19,7 +20,7 @@ fun String.runCommand(workingDir: File = File(".")): String? { .redirectOutput(ProcessBuilder.Redirect.PIPE) .redirectError(ProcessBuilder.Redirect.PIPE) .start() - proc.waitFor(1, TimeUnit.MINUTES) + assert(proc.waitFor(1, TimeUnit.MINUTES)) return proc.inputStream.bufferedReader().readText() } catch (e: IOException) { e.printStackTrace() @@ -204,35 +205,32 @@ abstract class ComputeCemuDataFilesHashTask : DefaultTask() { } } -val computeCemuDataFilesHashTask =tasks.register("computeCemuDataFilesHash") { - cemuDataFolder = cemuDataFilesFolder -} +val computeCemuDataFilesHashTask = + tasks.register("computeCemuDataFilesHash") { + cemuDataFolder = cemuDataFilesFolder + } tasks.preBuild.dependsOn(computeCemuDataFilesHashTask) +gettext { + potFile.set(File(projectDir, "src/messages.pot")) + keywords.set(listOf("tr", "trNoop")) +} + dependencies { - implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.kotlinx.gettext) implementation(libs.kotlinx.serialization.json) implementation(libs.androidx.activity.compose) implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.ui) implementation(libs.androidx.ui.graphics) - implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.compose.material3) androidTestImplementation(platform(libs.androidx.compose.bom)) - androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) implementation(libs.okhttp) implementation(libs.okhttp.coroutines) implementation(libs.androidx.appcompat) implementation(libs.material) - implementation(libs.androidx.navigation.fragment) implementation(libs.androidx.navigation.compose) - implementation(libs.androidx.navigation.ui) - implementation(libs.androidx.legacy.support.v4) - implementation(libs.androidx.lifecycle.viewmodel.ktx) implementation(libs.androidx.core.ktx) - testImplementation(libs.junit) - androidTestImplementation(libs.androidx.junit) - androidTestImplementation(libs.androidx.espresso.core) } diff --git a/src/android/app/src/debug/res/values/strings.xml b/src/android/app/src/debug/res/values/strings.xml index 5aa522b3..f37573ce 100644 --- a/src/android/app/src/debug/res/values/strings.xml +++ b/src/android/app/src/debug/res/values/strings.xml @@ -1,3 +1,3 @@ - Cemu debug + Cemu debug \ No newline at end of file diff --git a/src/android/app/src/main/cpp/CMakeLists.txt b/src/android/app/src/main/cpp/CMakeLists.txt index 85485b9e..8de47f35 100644 --- a/src/android/app/src/main/cpp/CMakeLists.txt +++ b/src/android/app/src/main/cpp/CMakeLists.txt @@ -12,6 +12,7 @@ add_library(CemuAndroid SHARED NativeGraphicPacks.cpp NativeInput.cpp NativeLib.cpp + NativeLocalization.cpp NativeLogging.cpp NativeSettings.cpp NativeSwkbd.cpp diff --git a/src/android/app/src/main/cpp/NativeEmulation.cpp b/src/android/app/src/main/cpp/NativeEmulation.cpp index a386cb1c..5c9c7a91 100644 --- a/src/android/app/src/main/cpp/NativeEmulation.cpp +++ b/src/android/app/src/main/cpp/NativeEmulation.cpp @@ -179,6 +179,7 @@ Java_info_cemu_cemu_nativeinterface_NativeEmulation_initializeRenderer(JNIEnv* e cemu_assert_debug(j_testSurface != nullptr); ANativewindow_Ptr testSurface(ANativeWindow_fromSurface(env, j_testSurface), &ANativeWindow_release); WindowSystem::GetWindowInfo().window_main.surface = testSurface.get(); + WindowSystem::GetWindowInfo().window_main.backend = WindowSystem::WindowHandleInfo::Backend::Android; g_renderer = std::make_unique(); WindowSystem::GetWindowInfo().window_main.surface = nullptr; }); @@ -219,6 +220,7 @@ Java_info_cemu_cemu_nativeinterface_NativeEmulation_setSurface(JNIEnv* env, [[ma JNIUtils::handleNativeException(env, [&]() { cemu_assert_debug(surface != nullptr); auto& windowHandleInfo = is_main_canvas ? WindowSystem::GetWindowInfo().canvas_main : WindowSystem::GetWindowInfo().canvas_pad; + windowHandleInfo.backend = WindowSystem::WindowHandleInfo::Backend::Android; if (windowHandleInfo.surface) { ANativeWindow_release(static_cast(windowHandleInfo.surface)); diff --git a/src/android/app/src/main/cpp/NativeInput.cpp b/src/android/app/src/main/cpp/NativeInput.cpp index a2785e4f..3e2cea2a 100644 --- a/src/android/app/src/main/cpp/NativeInput.cpp +++ b/src/android/app/src/main/cpp/NativeInput.cpp @@ -179,8 +179,9 @@ Java_info_cemu_cemu_nativeinterface_NativeInput_onTouchMove([[maybe_unused]] JNI extern "C" [[maybe_unused]] JNIEXPORT void JNICALL Java_info_cemu_cemu_nativeinterface_NativeInput_onMotion([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jlong timestamp, jfloat gyroX, jfloat gyroY, jfloat gyroZ, jfloat accelX, jfloat accelY, jfloat accelZ) { + static constexpr float METERS_PER_SECOND_SQ_TO_G = 1.f / 9.81f; float deltaTime = (timestamp - NativeInput::s_lastMotionTimestamp) * 1e-9f; - NativeInput::s_wiiUMotionHandler.processMotionSample(deltaTime, gyroX, gyroY, gyroZ, accelX * 0.098066f, -accelY * 0.098066f, -accelZ * 0.098066f); + NativeInput::s_wiiUMotionHandler.processMotionSample(deltaTime, gyroX, gyroY, gyroZ, accelX * METERS_PER_SECOND_SQ_TO_G, accelY * METERS_PER_SECOND_SQ_TO_G, accelZ * METERS_PER_SECOND_SQ_TO_G); NativeInput::s_lastMotionTimestamp = timestamp; auto& deviceMotion = InputManager::instance().m_device_motion; std::scoped_lock lock{deviceMotion.m_mutex}; diff --git a/src/android/app/src/main/cpp/NativeLocalization.cpp b/src/android/app/src/main/cpp/NativeLocalization.cpp new file mode 100644 index 00000000..2bf8d0cb --- /dev/null +++ b/src/android/app/src/main/cpp/NativeLocalization.cpp @@ -0,0 +1,42 @@ +#include "JNIUtils.h" + +namespace NativeLocalization +{ + std::unordered_map g_messages; + + std::string Translate(std::string_view msgId) + { + if (auto message = g_messages.find(msgId); message != g_messages.end()) + { + return message->second; + } + + return std::string{msgId}; + } +} // namespace NativeLocalization + +extern "C" JNIEXPORT [[maybe_unused]] void JNICALL +Java_info_cemu_cemu_nativeinterface_NativeLocalization_setTranslations(JNIEnv* env, [[maybe_unused]] jclass clazz, jobject translations) +{ + NativeLocalization::g_messages.clear(); + + jclass mapClass = env->GetObjectClass(translations); + jmethodID keySetMethodId = env->GetMethodID(mapClass, "keySet", "()Ljava/util/Set;"); + jmethodID getMethodId = env->GetMethodID(mapClass, "get", "(Ljava/lang/Object;)Ljava/lang/Object;"); + jobject keySet = env->CallObjectMethod(translations, keySetMethodId); + jclass setClass = env->GetObjectClass(keySet); + jmethodID toArrayMethodId = env->GetMethodID(setClass, "toArray", "()[Ljava/lang/Object;"); + auto keyArray = static_cast(env->CallObjectMethod(keySet, toArrayMethodId)); + jint size = env->GetArrayLength(keyArray); + + for (jint i = 0; i < size; i++) + { + auto keyJava = static_cast(env->GetObjectArrayElement(keyArray, i)); + std::string key = JNIUtils::toString(env, keyJava); + auto translationJava = static_cast(env->CallObjectMethod(translations, getMethodId, keyJava)); + std::string translation = JNIUtils::toString(env, translationJava); + NativeLocalization::g_messages[key] = translation; + } + + SetTranslationCallback(NativeLocalization::Translate); +} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/CemuApplication.kt b/src/android/app/src/main/java/info/cemu/cemu/CemuApplication.kt index d4afba7e..b25808d0 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/CemuApplication.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/CemuApplication.kt @@ -1,14 +1,21 @@ package info.cemu.cemu import android.app.Application +import android.content.res.Resources +import android.util.Log +import info.cemu.cemu.core.translation.setLanguage +import info.cemu.cemu.core.translation.setTranslations import info.cemu.cemu.nativeinterface.NativeActiveSettings.initializeActiveSettings import info.cemu.cemu.nativeinterface.NativeActiveSettings.setInternalDir import info.cemu.cemu.nativeinterface.NativeActiveSettings.setNativeLibDir import info.cemu.cemu.nativeinterface.NativeEmulation.initializeEmulation import info.cemu.cemu.nativeinterface.NativeEmulation.setDPI import info.cemu.cemu.nativeinterface.NativeGraphicPacks.refreshGraphicPacks +import info.cemu.cemu.nativeinterface.NativeLocalization import info.cemu.cemu.nativeinterface.NativeLogging.crashLog import info.cemu.cemu.nativeinterface.NativeSwkbd.initializeSwkbd +import info.cemu.cemu.settings.SettingsManager +import info.cemu.cemu.utils.parsePoFile import java.io.File import java.io.IOException import java.io.PrintWriter @@ -32,10 +39,16 @@ class CemuApplication : Application() { override fun onCreate() { super.onCreate() configureExceptionHandler() + initializeTranslations() initializeCemu() saveDataFiles() } + private fun initializeTranslations() { + setTranslations(this) + setLanguage(SettingsManager(this).guiSettings.language, this) + } + private fun saveDataFiles() { val dataFolder = File(internalCemuDataFolder) @@ -46,19 +59,13 @@ class CemuApplication : Application() { val hashFileName = "hash.txt" val hashFile = dataFolder.resolve(hashFileName) val oldHash = if (hashFile.isFile) hashFile.readText() else "invalid" - - val assetsFileStream = try { - assets.open(hashFileName) - } catch (_: IOException) { - null - } - if (assetsFileStream == null) { + val newHash = try { + assets.open(hashFileName).use { it.reader().readText() } + } catch (_: IOException) { return } - val newHash = assetsFileStream.reader().readText() - if (oldHash == newHash) { return } @@ -98,7 +105,8 @@ class CemuApplication : Application() { val outFile = dataFolder.resolve(assetFile) outFile.parentFile?.mkdirs() - assets.open(assetFile).copyTo(outFile.outputStream()) + assets.open(assetFile) + .use { asset -> outFile.outputStream().use { out -> asset.copyTo(out) } } } } diff --git a/src/android/app/src/main/java/info/cemu/cemu/MainActivity.kt b/src/android/app/src/main/java/info/cemu/cemu/MainActivity.kt index d98cd92f..0ba6e1fe 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/MainActivity.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/MainActivity.kt @@ -25,19 +25,20 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.documentfile.provider.DocumentFile import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController import info.cemu.cemu.about.AboutCemuRoute import info.cemu.cemu.about.aboutCemuNavigation +import info.cemu.cemu.core.components.ActivityContent +import info.cemu.cemu.core.translation.TranslatableContent +import info.cemu.cemu.core.translation.tr import info.cemu.cemu.emulation.EmulationActivity import info.cemu.cemu.gamelist.GameListRoute import info.cemu.cemu.gamelist.gameListNavigation import info.cemu.cemu.graphicpacks.GraphicPacksRoute import info.cemu.cemu.graphicpacks.graphicPacksNavigation -import info.cemu.cemu.guicore.components.ActivityContent import info.cemu.cemu.nativeinterface.NativeActiveSettings import info.cemu.cemu.nativeinterface.NativeGameTitles.Game import info.cemu.cemu.nativeinterface.NativeSettings @@ -52,8 +53,10 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { - ActivityContent { - MainNav() + TranslatableContent { + ActivityContent { + MainNav() + } } } } @@ -131,7 +134,7 @@ private fun GameListToolBarActionsMenu( ) { Icon( imageVector = Icons.Filled.MoreVert, - contentDescription = stringResource(R.string.more_options) + contentDescription = null ) } DropdownMenu( @@ -140,27 +143,27 @@ private fun GameListToolBarActionsMenu( ) { DropdownMenuItem( onClick = goToSettings, - text = stringResource(R.string.settings) + text = tr("Settings") ) DropdownMenuItem( onClick = goToGraphicPacks, - text = stringResource(R.string.graphic_packs) + text = tr("Graphic packs") ) DropdownMenuItem( onClick = goToTitleManager, - text = stringResource(R.string.title_manager) + text = tr("Title manager") ) DropdownMenuItem( onClick = { openCemuFolder(context) }, - text = stringResource(R.string.open_cemu_folder) + text = tr("Open Cemu folder") ) DropdownMenuItem( onClick = { shareLogFile(context) }, - text = stringResource(R.string.log_file_share_label), + text = tr("Share log file"), ) DropdownMenuItem( onClick = goToAboutCemu, - text = stringResource(R.string.about_cemu), + text = tr("About Cemu"), ) } } @@ -170,7 +173,7 @@ private fun shareLogFile(context: Context) { val logFile = File(NativeActiveSettings.getUserDataPath()).resolve(logFileName) if (!logFile.isFile) { - Toast.makeText(context, R.string.log_file_not_available, Toast.LENGTH_LONG).show() + Toast.makeText(context, tr("Log file doesn't exist"), Toast.LENGTH_LONG).show() return } @@ -205,6 +208,6 @@ private fun openCemuFolder(context: Context) { ) context.startActivity(intent) } catch (activityNotFoundException: ActivityNotFoundException) { - Toast.makeText(context, R.string.failed_to_open_cemu_folder, Toast.LENGTH_LONG).show() + Toast.makeText(context, tr("Could not open Cemu folder"), Toast.LENGTH_LONG).show() } } diff --git a/src/android/app/src/main/java/info/cemu/cemu/about/AboutCemuScreen.kt b/src/android/app/src/main/java/info/cemu/cemu/about/AboutCemuScreen.kt index 9ce5f293..3a1fd304 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/about/AboutCemuScreen.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/about/AboutCemuScreen.kt @@ -23,13 +23,14 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import info.cemu.cemu.BuildConfig import info.cemu.cemu.R -import info.cemu.cemu.guicore.components.ScreenContent +import info.cemu.cemu.core.components.ScreenContent +import info.cemu.cemu.core.translation.tr // TODO @Composable fun AboutCemuScreen(navigateBack: () -> Unit) { ScreenContent( - appBarText = stringResource(R.string.about_cemu), + appBarText = tr("About Cemu"), navigateBack = navigateBack, contentModifier = Modifier .fillMaxWidth() @@ -38,16 +39,16 @@ fun AboutCemuScreen(navigateBack: () -> Unit) { ) { AboutSection { Text( - text = stringResource(R.string.cemu), + text = stringResource(R.string.app_name), fontSize = 32.sp, ) Text( - text = stringResource(R.string.cemu_version, BuildConfig.VERSION_NAME), + text = tr("Version: {0}", BuildConfig.VERSION_NAME), fontSize = 18.sp, ) Text( - text = stringResource( - R.string.original_authors, + text = tr( + "Original authors: {0}", stringResource(R.string.cemu_original_authors) ), fontSize = 18.sp, @@ -56,19 +57,19 @@ fun AboutCemuScreen(navigateBack: () -> Unit) { } AboutSection { Text( - text = stringResource(R.string.cemu_description), + text = tr("Cemu is a Wii U emulator.\n\nWii and Wii U are trademarks of Nintendo.\nCemu is not affiliated with Nintendo."), fontSize = 18.sp ) } AboutSection { Text( - text = stringResource(R.string.used_libraries), + text = tr("Used libraries:"), fontSize = 18.sp, ) UsedLibraries.forEach { Library(it) } - Text(stringResource(R.string.ih264_library_description)) + Text(tr("Modified ih264 from Android project")) } } } @@ -77,7 +78,7 @@ fun AboutCemuScreen(navigateBack: () -> Unit) { fun CemuWebsite() { Text( buildAnnotatedString { - append(stringResource(R.string.website)) + append(tr("Website:")) append(" ") withLink( LinkAnnotation.Url( diff --git a/src/android/app/src/main/java/info/cemu/cemu/guicore/components/ActivityContent.kt b/src/android/app/src/main/java/info/cemu/cemu/core/components/ActivityContent.kt similarity index 84% rename from src/android/app/src/main/java/info/cemu/cemu/guicore/components/ActivityContent.kt rename to src/android/app/src/main/java/info/cemu/cemu/core/components/ActivityContent.kt index b6ab21ac..4a7fc42c 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/guicore/components/ActivityContent.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/core/components/ActivityContent.kt @@ -1,11 +1,11 @@ -package info.cemu.cemu.guicore.components +package info.cemu.cemu.core.components import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import info.cemu.cemu.guicore.theme.CemuTheme +import info.cemu.cemu.core.theme.CemuTheme @Composable fun ActivityContent(content: @Composable () -> Unit) { diff --git a/src/android/app/src/main/java/info/cemu/cemu/guicore/components/Button.kt b/src/android/app/src/main/java/info/cemu/cemu/core/components/Button.kt similarity index 97% rename from src/android/app/src/main/java/info/cemu/cemu/guicore/components/Button.kt rename to src/android/app/src/main/java/info/cemu/cemu/core/components/Button.kt index ba89bc3a..9aa9f4bb 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/guicore/components/Button.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/core/components/Button.kt @@ -1,4 +1,4 @@ -package info.cemu.cemu.guicore.components +package info.cemu.cemu.core.components import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth diff --git a/src/android/app/src/main/java/info/cemu/cemu/guicore/components/DrawerLayout.kt b/src/android/app/src/main/java/info/cemu/cemu/core/components/DrawerLayout.kt similarity index 96% rename from src/android/app/src/main/java/info/cemu/cemu/guicore/components/DrawerLayout.kt rename to src/android/app/src/main/java/info/cemu/cemu/core/components/DrawerLayout.kt index d66426a8..3035cb37 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/guicore/components/DrawerLayout.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/core/components/DrawerLayout.kt @@ -1,4 +1,4 @@ -package info.cemu.cemu.guicore.components +package info.cemu.cemu.core.components import android.content.Context import android.util.AttributeSet diff --git a/src/android/app/src/main/java/info/cemu/cemu/guicore/components/FilledSearchToolbar.kt b/src/android/app/src/main/java/info/cemu/cemu/core/components/FilledSearchToolbar.kt similarity index 95% rename from src/android/app/src/main/java/info/cemu/cemu/guicore/components/FilledSearchToolbar.kt rename to src/android/app/src/main/java/info/cemu/cemu/core/components/FilledSearchToolbar.kt index 257eb8ca..769ebecc 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/guicore/components/FilledSearchToolbar.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/core/components/FilledSearchToolbar.kt @@ -1,4 +1,4 @@ -package info.cemu.cemu.guicore.components +package info.cemu.cemu.core.components import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.IntrinsicSize @@ -91,7 +91,7 @@ fun FilledSearchToolbar( ) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(R.string.exit_search_description), + contentDescription = null ) } }, @@ -153,7 +153,7 @@ fun SearchToolbarInput( ) { Icon( imageVector = Icons.Filled.Clear, - contentDescription = stringResource(R.string.clear_search), + contentDescription = null ) } } @@ -177,7 +177,7 @@ fun SearchToolbarHint( .size(24.dp), imageVector = Icons.Default.Search, tint = color, - contentDescription = null, + contentDescription = null ) Text( text = hint, diff --git a/src/android/app/src/main/java/info/cemu/cemu/guicore/components/Header.kt b/src/android/app/src/main/java/info/cemu/cemu/core/components/Header.kt similarity index 92% rename from src/android/app/src/main/java/info/cemu/cemu/guicore/components/Header.kt rename to src/android/app/src/main/java/info/cemu/cemu/core/components/Header.kt index 185f2d18..25d7065d 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/guicore/components/Header.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/core/components/Header.kt @@ -1,4 +1,4 @@ -package info.cemu.cemu.guicore.components +package info.cemu.cemu.core.components import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text diff --git a/src/android/app/src/main/java/info/cemu/cemu/guicore/components/ModifierClickable.kt b/src/android/app/src/main/java/info/cemu/cemu/core/components/ModifierClickable.kt similarity index 95% rename from src/android/app/src/main/java/info/cemu/cemu/guicore/components/ModifierClickable.kt rename to src/android/app/src/main/java/info/cemu/cemu/core/components/ModifierClickable.kt index ddd7929e..2c6efaee 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/guicore/components/ModifierClickable.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/core/components/ModifierClickable.kt @@ -1,4 +1,4 @@ -package info.cemu.cemu.guicore.components +package info.cemu.cemu.core.components import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource diff --git a/src/android/app/src/main/java/info/cemu/cemu/guicore/components/ScreenContent.kt b/src/android/app/src/main/java/info/cemu/cemu/core/components/ScreenContent.kt similarity index 97% rename from src/android/app/src/main/java/info/cemu/cemu/guicore/components/ScreenContent.kt rename to src/android/app/src/main/java/info/cemu/cemu/core/components/ScreenContent.kt index 044b3322..a061f296 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/guicore/components/ScreenContent.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/core/components/ScreenContent.kt @@ -1,6 +1,6 @@ @file:OptIn(ExperimentalMaterial3Api::class) -package info.cemu.cemu.guicore.components +package info.cemu.cemu.core.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -137,7 +137,7 @@ private fun ScreenContentGeneric( IconButton(onClick = dropUnlessResumed { navigateBack() }) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(R.string.nav_back) + contentDescription = null ) } }, diff --git a/src/android/app/src/main/java/info/cemu/cemu/guicore/components/SingleSelection.kt b/src/android/app/src/main/java/info/cemu/cemu/core/components/SingleSelection.kt similarity index 98% rename from src/android/app/src/main/java/info/cemu/cemu/guicore/components/SingleSelection.kt rename to src/android/app/src/main/java/info/cemu/cemu/core/components/SingleSelection.kt index 185873fe..3a23b762 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/guicore/components/SingleSelection.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/core/components/SingleSelection.kt @@ -1,4 +1,4 @@ -package info.cemu.cemu.guicore.components +package info.cemu.cemu.core.components import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -31,6 +31,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import info.cemu.cemu.R +import info.cemu.cemu.core.translation.tr @Composable fun SingleSelection( @@ -179,7 +180,7 @@ private fun SelectDialog( .padding(8.dp) .align(Alignment.End), ) { - Text(stringResource(R.string.cancel)) + Text(tr("Cancel")) } } } diff --git a/src/android/app/src/main/java/info/cemu/cemu/guicore/components/Slider.kt b/src/android/app/src/main/java/info/cemu/cemu/core/components/Slider.kt similarity index 97% rename from src/android/app/src/main/java/info/cemu/cemu/guicore/components/Slider.kt rename to src/android/app/src/main/java/info/cemu/cemu/core/components/Slider.kt index 8b5fb8f1..d8e0ffa5 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/guicore/components/Slider.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/core/components/Slider.kt @@ -1,4 +1,4 @@ -package info.cemu.cemu.guicore.components +package info.cemu.cemu.core.components import androidx.annotation.IntRange import androidx.compose.foundation.layout.Column diff --git a/src/android/app/src/main/java/info/cemu/cemu/guicore/components/Toggle.kt b/src/android/app/src/main/java/info/cemu/cemu/core/components/Toggle.kt similarity index 97% rename from src/android/app/src/main/java/info/cemu/cemu/guicore/components/Toggle.kt rename to src/android/app/src/main/java/info/cemu/cemu/core/components/Toggle.kt index 8ba2712c..eeb84ed8 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/guicore/components/Toggle.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/core/components/Toggle.kt @@ -1,4 +1,4 @@ -package info.cemu.cemu.guicore.components +package info.cemu.cemu.core.components import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement diff --git a/src/android/app/src/main/java/info/cemu/cemu/guicore/format/Formatters.kt b/src/android/app/src/main/java/info/cemu/cemu/core/format/Formatters.kt similarity index 89% rename from src/android/app/src/main/java/info/cemu/cemu/guicore/format/Formatters.kt rename to src/android/app/src/main/java/info/cemu/cemu/core/format/Formatters.kt index c9d4896a..aecc140b 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/guicore/format/Formatters.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/core/format/Formatters.kt @@ -1,4 +1,4 @@ -package info.cemu.cemu.guicore.format +package info.cemu.cemu.core.format import android.text.format.Formatter import androidx.compose.runtime.Composable diff --git a/src/android/app/src/main/java/info/cemu/cemu/core/nativeenummapper/GameTitlesEnums.kt b/src/android/app/src/main/java/info/cemu/cemu/core/nativeenummapper/GameTitlesEnums.kt new file mode 100644 index 00000000..5a50dbbd --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/core/nativeenummapper/GameTitlesEnums.kt @@ -0,0 +1,16 @@ +package info.cemu.cemu.core.nativeenummapper + +import info.cemu.cemu.core.translation.tr +import info.cemu.cemu.nativeinterface.NativeGameTitles + +fun regionToString(region: Int): String = when (region) { + NativeGameTitles.CONSOLE_REGION_JPN -> tr("Japan") + NativeGameTitles.CONSOLE_REGION_USA -> tr("USA") + NativeGameTitles.CONSOLE_REGION_EUR -> tr("Europe") + NativeGameTitles.CONSOLE_REGION_AUS_DEPR -> tr("Australia") + NativeGameTitles.CONSOLE_REGION_CHN -> tr("China") + NativeGameTitles.CONSOLE_REGION_KOR -> tr("Korea") + NativeGameTitles.CONSOLE_REGION_TWN -> tr("Taiwan") + NativeGameTitles.CONSOLE_REGION_AUTO -> tr("Auto") + else -> tr("Many") +} diff --git a/src/android/app/src/main/java/info/cemu/cemu/core/nativeenummapper/InputEnums.kt b/src/android/app/src/main/java/info/cemu/cemu/core/nativeenummapper/InputEnums.kt new file mode 100644 index 00000000..f635f8ae --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/core/nativeenummapper/InputEnums.kt @@ -0,0 +1,13 @@ +package info.cemu.cemu.core.nativeenummapper + +import info.cemu.cemu.core.translation.tr +import info.cemu.cemu.nativeinterface.NativeInput + +fun controllerTypeToString(type: Int) = when (type) { + NativeInput.EMULATED_CONTROLLER_TYPE_DISABLED -> tr("Disabled") + NativeInput.EMULATED_CONTROLLER_TYPE_VPAD -> tr("Wii U GamePad") + NativeInput.EMULATED_CONTROLLER_TYPE_PRO -> tr("Wii U Pro Controller") + NativeInput.EMULATED_CONTROLLER_TYPE_WIIMOTE -> tr("Wiimote") + NativeInput.EMULATED_CONTROLLER_TYPE_CLASSIC -> tr("Wii U Classic Controller") + else -> throw IllegalArgumentException("Invalid controller type: $type") +} diff --git a/src/android/app/src/main/java/info/cemu/cemu/guicore/theme/Theme.kt b/src/android/app/src/main/java/info/cemu/cemu/core/theme/Theme.kt similarity index 96% rename from src/android/app/src/main/java/info/cemu/cemu/guicore/theme/Theme.kt rename to src/android/app/src/main/java/info/cemu/cemu/core/theme/Theme.kt index 4f033aa0..4f7f867c 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/guicore/theme/Theme.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/core/theme/Theme.kt @@ -1,4 +1,4 @@ -package info.cemu.cemu.guicore.theme +package info.cemu.cemu.core.theme import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme diff --git a/src/android/app/src/main/java/info/cemu/cemu/core/translation/Translation.kt b/src/android/app/src/main/java/info/cemu/cemu/core/translation/Translation.kt new file mode 100644 index 00000000..a130d65e --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/core/translation/Translation.kt @@ -0,0 +1,94 @@ +package info.cemu.cemu.core.translation + +import android.content.Context +import android.util.Log +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.runtime.staticCompositionLocalOf +import info.cemu.cemu.nativeinterface.NativeLocalization +import info.cemu.cemu.utils.parsePoFile +import name.kropp.kotlinx.gettext.Gettext +import name.kropp.kotlinx.gettext.Locale +import name.kropp.kotlinx.gettext.load +import java.io.IOException +import java.text.MessageFormat + +private var I18n = Gettext.Fallback + +private abstract class Translation(val locale: Locale) { + abstract fun load(context: Context) +} + +private class AssetTranslation(locale: Locale, private val assetFile: String) : + Translation(locale) { + override fun load(context: Context) { + try { + val poFileContent = context.assets.open(assetFile).use { source -> source.readBytes() } + I18n = Gettext.load(locale, poFileContent.inputStream()) + NativeLocalization.setTranslations(parsePoFile(poFileContent.inputStream())) + } catch (_: IOException) { + Log.e("CemuTranslations", "failed to load translations from asset: $assetFile") + return + } + } +} + +private const val TRANSLATIONS_FOLDER = "translations" +private const val TRANSLATIONS_FILE_NAME = "cemu.po" +const val DEFAULT_LANGUAGE = "en" + +private val DefaultTranslation = object : Translation(Locale(DEFAULT_LANGUAGE)) { + override fun load(context: Context) { + I18n = Gettext.Fallback + NativeLocalization.setTranslations(emptyMap()) + } +} + +private var Translations = listOf(DefaultTranslation) + +data class Language(val code: String, val displayName: String) + +fun getAvailableLanguages() = + Translations.map { Language(it.locale.language, it.locale.getDisplayName(it.locale)) } + +fun setLanguage(languageCode: String, context: Context) { + val translation = Translations.firstOrNull { it.locale.language == languageCode } ?: return + translation.load(context) + CurrentLanguage = translation.locale.language +} + +fun setTranslations(context: Context) { + val assetTranslations = context.assets.list(TRANSLATIONS_FOLDER)?.filter { language -> + if (language == DEFAULT_LANGUAGE) + return@filter false + + context.assets.list("$TRANSLATIONS_FOLDER/$language") + ?.contains(TRANSLATIONS_FILE_NAME) + ?: false + }?.map { language -> + val locale = Locale(language) + val assetFile = "$TRANSLATIONS_FOLDER/$language/$TRANSLATIONS_FILE_NAME" + AssetTranslation(locale, assetFile) + } ?: listOf() + + Translations = listOf(DefaultTranslation).plus(assetTranslations) +} + +fun trNoop(text: String) = text + +fun tr(text: String) = I18n.tr(text) + +fun tr(text: String, vararg args: Any): String = MessageFormat.format(I18n.tr(text), *args) + +private var CurrentLanguage by mutableStateOf(DEFAULT_LANGUAGE) +private val LocalAppLanguage = staticCompositionLocalOf { DEFAULT_LANGUAGE } + +@Composable +fun TranslatableContent(content: @Composable () -> Unit) { + CompositionLocalProvider(LocalAppLanguage provides CurrentLanguage) { + content() + } +} diff --git a/src/android/app/src/main/java/info/cemu/cemu/emulation/EmulationActivity.kt b/src/android/app/src/main/java/info/cemu/cemu/emulation/EmulationActivity.kt index ded35356..5524adda 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/emulation/EmulationActivity.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/emulation/EmulationActivity.kt @@ -17,7 +17,6 @@ import android.widget.LinearLayout import android.widget.Toast import androidx.activity.OnBackPressedCallback import androidx.annotation.Keep -import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.core.view.WindowCompat @@ -27,6 +26,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.textfield.TextInputLayout import info.cemu.cemu.BuildConfig import info.cemu.cemu.R +import info.cemu.cemu.core.translation.tr import info.cemu.cemu.databinding.ActivityEmulationBinding import info.cemu.cemu.databinding.LayoutSideMenuCheckboxItemBinding import info.cemu.cemu.databinding.LayoutSideMenuEmulationBinding @@ -65,7 +65,7 @@ class EmulationActivity : AppCompatActivity() { NativeEmulation.setSurface(surfaceHolder.surface, isMainCanvas) surfaceSet = true } catch (exception: NativeException) { - onEmulationError(getString(R.string.failed_create_surface_error, exception.message)) + onEmulationError(tr(">Failed creating surface: {0}", exception.message!!)) } } @@ -171,10 +171,12 @@ class EmulationActivity : AppCompatActivity() { } private fun LayoutSideMenuTextItemBinding.configure( + label: String, isEnabled: Boolean = true, onClick: () -> Unit, ) { setEnabled(isEnabled) + this.label = label textItem.setOnClickListener { onClick() binding.drawerLayout.close() @@ -182,9 +184,11 @@ class EmulationActivity : AppCompatActivity() { } private fun LayoutSideMenuCheckboxItemBinding.configure( + label: String, initialCheckedStatus: Boolean = false, onCheckChanged: (Boolean) -> Unit, ) { + this.label = label checkbox.isChecked = initialCheckedStatus checkboxItem.setOnClickListener { checkbox.isChecked = !checkbox.isChecked @@ -195,25 +199,41 @@ class EmulationActivity : AppCompatActivity() { private fun LayoutSideMenuEmulationBinding.configureSideMenu() { val isInputOverlayEnabled = overlaySettings.isOverlayEnabled - - enableMotionCheckbox.configure(onCheckChanged = ::setMotionEnabled) - replaceTvWithPadCheckbox.configure(onCheckChanged = NativeEmulation::setReplaceTVWithPadView) - showPadCheckbox.configure(onCheckChanged = ::setPadViewVisibility) - showInputOverlayCheckbox.configure(initialCheckedStatus = isInputOverlayEnabled) { showInputOverlay -> - editInputsMenuItem.setEnabled(showInputOverlay) - resetInputOverlayMenuItem.setEnabled(showInputOverlay) - inputOverlaySurfaceView.setVisible(showInputOverlay) - } - editInputsMenuItem.configure(isEnabled = isInputOverlayEnabled) { - binding.editInputsLayout.visibility = View.VISIBLE - binding.finishEditInputsButton.visibility = View.VISIBLE - binding.moveInputsButton.performClick() - } + enableMotionCheckbox.configure( + label = tr("Enable motion"), + onCheckChanged = ::setMotionEnabled + ) + replaceTvWithPadCheckbox.configure( + label = tr("Replace TV with PAD"), + onCheckChanged = NativeEmulation::setReplaceTVWithPadView + ) + showPadCheckbox.configure( + label = tr(text = "Show PAD"), + onCheckChanged = ::setPadViewVisibility + ) + showInputOverlayCheckbox.configure( + tr(text = "Show input overlay"), + initialCheckedStatus = isInputOverlayEnabled, + onCheckChanged = { showInputOverlay -> + editInputsMenuItem.setEnabled(showInputOverlay) + resetInputOverlayMenuItem.setEnabled(showInputOverlay) + inputOverlaySurfaceView.setVisible(showInputOverlay) + } + ) + editInputsMenuItem.configure( + label = tr("Edit inputs"), + isEnabled = isInputOverlayEnabled, + onClick = { + binding.editInputsLayout.visibility = View.VISIBLE + binding.finishEditInputsButton.visibility = View.VISIBLE + binding.moveInputsButton.performClick() + }) resetInputOverlayMenuItem.configure( + tr(text = "Reset input overlay"), isEnabled = isInputOverlayEnabled, onClick = inputOverlaySurfaceView::resetInputs ) - exitMenuItem.configure(onClick = onBackPressedDispatcher::onBackPressed) + exitMenuItem.configure(tr("Exit"), onClick = onBackPressedDispatcher::onBackPressed) } @SuppressLint("ClickableViewAccessibility") @@ -233,7 +253,7 @@ class EmulationActivity : AppCompatActivity() { } binding.resizeInputsButton.alpha = 0.5f binding.moveInputsButton.alpha = 1.0f - toastMessage(R.string.input_mode_edit_position) + toastMessage(tr("Edit input positions")) inputOverlaySurfaceView.setInputMode(InputOverlaySurfaceView.InputMode.EDIT_POSITION) } binding.resizeInputsButton.setOnClickListener { _ -> @@ -242,14 +262,15 @@ class EmulationActivity : AppCompatActivity() { } binding.moveInputsButton.alpha = 0.5f binding.resizeInputsButton.alpha = 1.0f - toastMessage(R.string.input_mode_edit_size) + toastMessage(tr("Edit input size")) inputOverlaySurfaceView.setInputMode(InputOverlaySurfaceView.InputMode.EDIT_SIZE) } + binding.finishEditInputsButton.text = tr("Done") binding.finishEditInputsButton.setOnClickListener { _ -> inputOverlaySurfaceView.setInputMode(InputOverlaySurfaceView.InputMode.DEFAULT) binding.finishEditInputsButton.visibility = View.GONE binding.editInputsLayout.visibility = View.GONE - toastMessage(R.string.input_mode_default) + toastMessage(tr("Exited input edit mode")) } if (emulationScreenSettings.isDrawerButtonVisible) { @@ -267,12 +288,7 @@ class EmulationActivity : AppCompatActivity() { testSurface.release() testSurfaceTexture.release() } catch (exception: NativeException) { - onEmulationError( - getString( - R.string.failed_initialize_renderer_error, - exception.message - ) - ) + onEmulationError(tr("Failed to initialize renderer: {0}", exception.message!!)) return } @@ -303,9 +319,9 @@ class EmulationActivity : AppCompatActivity() { mainCanvas.setOnTouchListener(CanvasOnTouchListener(isTV = true)) } - private fun toastMessage(@StringRes toastTextResId: Int) { + private fun toastMessage(text: String) { toast?.cancel() - toast = Toast.makeText(this, toastTextResId, Toast.LENGTH_SHORT) + toast = Toast.makeText(this, text, Toast.LENGTH_SHORT) .also { it.show() } } @@ -315,10 +331,10 @@ class EmulationActivity : AppCompatActivity() { return } val errorMessage = when (result) { - NativeEmulation.START_GAME_ERROR_GAME_BASE_FILES_NOT_FOUND -> getString(R.string.game_not_found) - NativeEmulation.START_GAME_ERROR_NO_DISC_KEY -> getString(R.string.no_disk_key) - NativeEmulation.START_GAME_ERROR_NO_TITLE_TIK -> getString(R.string.no_title_tik) - else -> getString(R.string.game_files_unknown_error, launchPath) + NativeEmulation.START_GAME_ERROR_GAME_BASE_FILES_NOT_FOUND -> tr("Unable to launch game because the base files were not found.") + NativeEmulation.START_GAME_ERROR_NO_DISC_KEY -> tr("Could not decrypt title. Make sure that keys.txt contains the correct disc key for this title.") + NativeEmulation.START_GAME_ERROR_NO_TITLE_TIK -> tr("Could not decrypt title because title.tik is missing.") + else -> tr("Unable to launch game\nPath: {0}", launchPath) } onEmulationError(errorMessage) } @@ -358,18 +374,18 @@ class EmulationActivity : AppCompatActivity() { private fun showExitConfirmationDialog() { val builder = MaterialAlertDialogBuilder(this) - builder.setTitle(R.string.exit_confirmation_title) - .setMessage(R.string.exit_confirm_message) - .setPositiveButton(R.string.yes) { _, _ -> quit() } - .setNegativeButton(R.string.no) { _, _ -> } + builder.setTitle(tr("Exit confirmation")) + .setMessage(tr("Are you sure you want to exit?")) + .setPositiveButton(tr("Yes")) { _, _ -> quit() } + .setNegativeButton(tr("No")) { _, _ -> } .show() } private fun onEmulationError(emulationError: String?) { val builder = MaterialAlertDialogBuilder(this) - builder.setTitle(R.string.error) + builder.setTitle(tr("Error")) .setMessage(emulationError) - .setNeutralButton(R.string.quit) { _, _ -> } + .setNeutralButton(tr("Quit")) { _, _ -> } .setOnDismissListener { _ -> quit() } .show() } @@ -415,7 +431,7 @@ class EmulationActivity : AppCompatActivity() { val dialog = MaterialAlertDialogBuilder(emulationActivity) .setView(inputEditTextLayout) .setCancelable(false) - .setPositiveButton(R.string.done) { _, _ -> }.show() + .setPositiveButton(tr("Done")) { _, _ -> }.show() val doneButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE)!! doneButton.isEnabled = false doneButton.setOnClickListener { _ -> inputEditText.onFinishedEdit() } diff --git a/src/android/app/src/main/java/info/cemu/cemu/emulation/EmulationTextInputEditText.kt b/src/android/app/src/main/java/info/cemu/cemu/emulation/EmulationTextInputEditText.kt index 7a00e531..ffa8dcd7 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/emulation/EmulationTextInputEditText.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/emulation/EmulationTextInputEditText.kt @@ -8,6 +8,7 @@ import android.util.AttributeSet import android.view.inputmethod.EditorInfo import com.google.android.material.R import com.google.android.material.textfield.TextInputEditText +import info.cemu.cemu.core.translation.tr import info.cemu.cemu.nativeinterface.NativeSwkbd import java.util.regex.Pattern @@ -34,6 +35,8 @@ class EmulationTextInputEditText @JvmOverloads constructor( private var onTextChangedListener: ((CharSequence) -> Unit)? = null init { + hint = tr("Input text") + appendFilter { source: CharSequence, _, _, _, _, _ -> if (INPUT_PATTERN.matcher(source).matches()) null else "" } diff --git a/src/android/app/src/main/java/info/cemu/cemu/gamelist/GameDetailsScreen.kt b/src/android/app/src/main/java/info/cemu/cemu/gamelist/GameDetailsScreen.kt index 17d652a3..756220f4 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/gamelist/GameDetailsScreen.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/gamelist/GameDetailsScreen.kt @@ -8,13 +8,12 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import info.cemu.cemu.R -import info.cemu.cemu.guicore.components.ScreenContent -import info.cemu.cemu.guicore.nativeenummapper.regionToStringId +import info.cemu.cemu.core.components.ScreenContent +import info.cemu.cemu.core.translation.tr +import info.cemu.cemu.nativeinterface.NativeGameTitles import info.cemu.cemu.nativeinterface.NativeGameTitles.Game import java.time.LocalDate import java.time.format.DateTimeFormatter @@ -26,7 +25,7 @@ fun GameDetailsScreen(game: Game?, navigateBack: () -> Unit) { return ScreenContent( - appBarText = stringResource(R.string.about_title), + appBarText = tr("About title"), contentModifier = Modifier.padding(16.dp), contentVerticalArrangement = Arrangement.spacedBy(16.dp), navigateBack = navigateBack, @@ -41,24 +40,24 @@ fun GameDetails(game: Game) { game = game, modifier = Modifier.size(128.dp), ) - TitleDetailsEntry(entryName = stringResource(R.string.title_name), entryData = game.name) - TitleDetailsEntry(entryName = stringResource(R.string.title_id), entryData = game.titleId) - TitleDetailsEntry(entryName = stringResource(R.string.version), entryData = game.version) - TitleDetailsEntry(entryName = stringResource(R.string.dlc), entryData = game.dlc) + TitleDetailsEntry(entryName = tr("Title name"), entryData = game.name) + TitleDetailsEntry(entryName = tr("Title ID"), entryData = game.titleId) + TitleDetailsEntry(entryName = tr("Version"), entryData = game.version) + TitleDetailsEntry(entryName = tr("DLC"), entryData = game.dlc) TitleDetailsEntry( - entryName = stringResource(R.string.title_time_played), + entryName = tr("You've played"), entryData = getTimePlayed(game) ) TitleDetailsEntry( - entryName = stringResource(R.string.title_last_played), + entryName = tr("Last played"), entryData = getLastPlayedDate(game) ) TitleDetailsEntry( - entryName = stringResource(R.string.title_region), - entryData = stringResource(regionToStringId(game.region)) + entryName = tr("Region"), + entryData = regionToString(game.region) ) TitleDetailsEntry( - entryName = stringResource(R.string.title_path), + entryName = tr("Path"), entryData = game.path ) } @@ -66,15 +65,15 @@ fun GameDetails(game: Game) { @ReadOnlyComposable @Composable -fun getTimePlayed(game: Game): String { +private fun getTimePlayed(game: Game): String { if (game.minutesPlayed == 0) { - return stringResource(R.string.never_played) + return tr("Never played") } if (game.minutesPlayed < 60) { - return stringResource(R.string.minutes_played, game.minutesPlayed) + return tr("Minutes: {0}", game.minutesPlayed) } - return stringResource( - R.string.hours_minutes_played, + return tr( + "Hours: {0} Minutes: {1}", game.minutesPlayed / 60, game.minutesPlayed % 60 ) @@ -88,7 +87,7 @@ private val DateFormatter = DateTimeFormatter.ofLocalizedDate( @Composable private fun getLastPlayedDate(game: Game): String { if (game.lastPlayedYear.toInt() == 0) { - return stringResource(R.string.never_played) + return tr("Never played") } val lastPlayedDate = LocalDate.of( game.lastPlayedYear.toInt(), @@ -99,7 +98,7 @@ private fun getLastPlayedDate(game: Game): String { } @Composable -fun TitleDetailsEntry(entryName: String, entryData: T?) { +private fun TitleDetailsEntry(entryName: String, entryData: T?) { Column { Text( text = entryName, @@ -112,3 +111,16 @@ fun TitleDetailsEntry(entryName: String, entryData: T?) { ) } } + +fun regionToString(region: Int): String = when (region) { + NativeGameTitles.CONSOLE_REGION_JPN -> tr("Japan") + NativeGameTitles.CONSOLE_REGION_USA -> tr("USA") + NativeGameTitles.CONSOLE_REGION_EUR -> tr("Europe") + NativeGameTitles.CONSOLE_REGION_AUS_DEPR -> tr("Australia") + NativeGameTitles.CONSOLE_REGION_CHN -> tr("China") + NativeGameTitles.CONSOLE_REGION_KOR -> tr("Korea") + NativeGameTitles.CONSOLE_REGION_TWN -> tr("Taiwan") + NativeGameTitles.CONSOLE_REGION_AUTO -> tr("Auto") + else -> tr("Many") +} + diff --git a/src/android/app/src/main/java/info/cemu/cemu/gamelist/GameIcon.kt b/src/android/app/src/main/java/info/cemu/cemu/gamelist/GameIcon.kt index 5a8e0b5c..d2d66ed6 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/gamelist/GameIcon.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/gamelist/GameIcon.kt @@ -34,13 +34,13 @@ fun GameIcon( Image( modifier = modifier.iconBorder(borderColor), bitmap = game.icon, - contentDescription = stringResource(R.string.game_icon), + contentDescription = null ) } else { Icon( modifier = modifier.iconBorder(borderColor), painter = painterResource(R.drawable.ic_question_mark), - contentDescription = stringResource(R.string.game_icon_empty), + contentDescription = null ) } } \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/gamelist/GameProfileEditScreen.kt b/src/android/app/src/main/java/info/cemu/cemu/gamelist/GameProfileEditScreen.kt index 07ddabc5..0c69221a 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/gamelist/GameProfileEditScreen.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/gamelist/GameProfileEditScreen.kt @@ -4,14 +4,12 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import info.cemu.cemu.R -import info.cemu.cemu.guicore.components.Header -import info.cemu.cemu.guicore.components.ScreenContent -import info.cemu.cemu.guicore.components.SingleSelection -import info.cemu.cemu.guicore.components.Toggle -import info.cemu.cemu.guicore.nativeenummapper.cpuModeToStringId +import info.cemu.cemu.core.components.Header +import info.cemu.cemu.core.components.ScreenContent +import info.cemu.cemu.core.components.SingleSelection +import info.cemu.cemu.core.components.Toggle +import info.cemu.cemu.core.translation.tr import info.cemu.cemu.nativeinterface.NativeGameTitles @Composable @@ -21,15 +19,15 @@ fun GameProfileEditScreen(game: NativeGameTitles.Game?, navigateBack: () -> Unit val titleId = game.titleId ScreenContent( - appBarText = stringResource(R.string.edit_game_profile), + appBarText = tr("Edit game profile"), navigateBack = navigateBack, contentModifier = Modifier.padding(16.dp), contentVerticalArrangement = Arrangement.spacedBy(16.dp), ) { Header(text = game.name) Toggle( - label = stringResource(R.string.load_shared_libraries), - description = stringResource(R.string.load_shared_libraries_description), + label = tr("Load shared libraries"), + description = tr("Load libraries from the cafeLibs directory"), initialCheckedState = { NativeGameTitles.isLoadingSharedLibrariesForTitleEnabled( titleId @@ -43,8 +41,8 @@ fun GameProfileEditScreen(game: NativeGameTitles.Game?, navigateBack: () -> Unit }, ) Toggle( - label = stringResource(R.string.shader_multiplication_accuracy), - description = stringResource(R.string.shader_multiplication_accuracy_description), + label = tr("Shader multiplication accuracy"), + description = tr("Controls the accuracy of floating point multiplication in shaders"), initialCheckedState = { NativeGameTitles.isShaderMultiplicationAccuracyForTitleEnabled( titleId @@ -58,7 +56,7 @@ fun GameProfileEditScreen(game: NativeGameTitles.Game?, navigateBack: () -> Unit }, ) SingleSelection( - label = stringResource(R.string.cpu_mode), + label = tr("CPU mode"), initialChoice = { NativeGameTitles.getCpuModeForTitle(titleId) }, choices = listOf( NativeGameTitles.CPU_MODE_SINGLECOREINTERPRETER, @@ -66,11 +64,11 @@ fun GameProfileEditScreen(game: NativeGameTitles.Game?, navigateBack: () -> Unit NativeGameTitles.CPU_MODE_MULTICORERECOMPILER, NativeGameTitles.CPU_MODE_AUTO ), - choiceToString = { cpuMode -> stringResource(cpuModeToStringId(cpuMode)) }, + choiceToString = { cpuMode -> cpuModeToString(cpuMode) }, onChoiceChanged = { cpuMode -> NativeGameTitles.setCpuModeForTitle(titleId, cpuMode) } ) SingleSelection( - label = stringResource(R.string.thread_quantum), + label = tr("Thread quantum"), initialChoice = { NativeGameTitles.getThreadQuantumForTitle(titleId) }, choices = NativeGameTitles.THREAD_QUANTUM_VALUES.toList(), choiceToString = { it.toString() }, @@ -80,3 +78,10 @@ fun GameProfileEditScreen(game: NativeGameTitles.Game?, navigateBack: () -> Unit ) } } + +private fun cpuModeToString(cpuMode: Int): String = when (cpuMode) { + NativeGameTitles.CPU_MODE_SINGLECOREINTERPRETER -> tr("Single-core interpreter") + NativeGameTitles.CPU_MODE_SINGLECORERECOMPILER -> tr("Single-core recompiler") + NativeGameTitles.CPU_MODE_MULTICORERECOMPILER -> tr("Multi-core recompiler") + else -> tr("Auto (recommended)") +} diff --git a/src/android/app/src/main/java/info/cemu/cemu/gamelist/GamesListScreen.kt b/src/android/app/src/main/java/info/cemu/cemu/gamelist/GamesListScreen.kt index ad7abfda..946682fd 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/gamelist/GamesListScreen.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/gamelist/GamesListScreen.kt @@ -1,5 +1,5 @@ @file:OptIn( - ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class, + ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class, ) package info.cemu.cemu.gamelist @@ -54,7 +54,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.asAndroidBitmap import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -63,8 +62,9 @@ import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import info.cemu.cemu.R +import info.cemu.cemu.core.components.FilledSearchToolbar +import info.cemu.cemu.core.translation.tr import info.cemu.cemu.emulation.EmulationActivity -import info.cemu.cemu.guicore.components.FilledSearchToolbar import info.cemu.cemu.nativeinterface.NativeGameTitles import info.cemu.cemu.nativeinterface.NativeGameTitles.Game import kotlinx.coroutines.delay @@ -105,7 +105,7 @@ fun GamesListScreen( topBar = { FilledSearchToolbar( actions = toolbarActions, - hint = stringResource(R.string.search_games), + hint = tr("Search games"), query = gameSearchQuery, onValueChange = gameListViewModel::setFilterText ) @@ -132,7 +132,7 @@ fun GamesListScreen( games = games, setFavorite = gameListViewModel::setGameTitleFavorite, deleteShaderCaches = { - coroutineScope.launch { snackbarHostState.showSnackbar(context.getString(R.string.shader_caches_removed_notification)) } + coroutineScope.launch { snackbarHostState.showSnackbar(tr("Shader caches removed")) } gameListViewModel.removeShadersForGame(it) }, startGame = startGame, @@ -143,9 +143,7 @@ fun GamesListScreen( context, game, onFailedToCreateShortCut = { - val errorMessage = - context.getString(R.string.shortcut_not_supported) - coroutineScope.launch { snackbarHostState.showSnackbar(errorMessage) } + coroutineScope.launch { snackbarHostState.showSnackbar(tr("Device doesn't support creating shortcuts")) } } ) }, @@ -221,16 +219,16 @@ fun ShaderCachesConfirmationDialog( ) { AlertDialog( title = { - Text(stringResource(R.string.remove_shader_caches)) + Text(tr("Remove shader caches")) }, text = { - Text(stringResource(R.string.remove_shader_caches_message, gameName)) + Text(tr("Remove the shader caches for {0}?", gameName)) }, dismissButton = { TextButton( onClick = onDismissRequest ) { - Text(stringResource(R.string.no)) + Text(tr("No")) } }, onDismissRequest = onDismissRequest, @@ -238,7 +236,7 @@ fun ShaderCachesConfirmationDialog( TextButton( onClick = onConfirm ) { - Text(stringResource(R.string.yes)) + Text(tr("Yes")) } } ) @@ -283,7 +281,7 @@ fun GameListItem( .background(MaterialTheme.colorScheme.surfaceVariant), painter = painterResource(R.drawable.ic_favorite), tint = MaterialTheme.colorScheme.primary, - contentDescription = stringResource(R.string.game_favorite_description), + contentDescription = null ) } } @@ -346,29 +344,29 @@ fun GameContextMenu( } GameContextMenuItem( onClick = { onIsFavoriteChanged(!game.isFavorite) }, - text = stringResource(R.string.game_favorite), + text = tr("Favorite"), trailingIcon = { Checkbox(checked = game.isFavorite, onCheckedChange = null) } ) GameContextMenuItem( onClick = onEditGameProfile, - text = stringResource(R.string.edit_game_profile) + text = tr("Edit game profile") ) GameContextMenuItem( enabled = gameTitleHasCaches, onClick = { onRemoveShaderCaches() }, - text = stringResource(R.string.remove_shader_caches) + text = tr("Remove shader caches") ) GameContextMenuItem( onClick = onAboutTitle, - text = stringResource(R.string.about_title), + text = tr("About title"), ) GameContextMenuItem( onClick = onCreateShortcut, - text = stringResource(R.string.create_shortcut) + text = tr("Create shortcut") ) } } diff --git a/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksScreen.kt b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksScreen.kt index 095fd4c9..a42f2b20 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksScreen.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/graphicpacks/GraphicPacksScreen.kt @@ -2,7 +2,6 @@ package info.cemu.cemu.graphicpacks -import androidx.annotation.StringRes import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box @@ -50,18 +49,17 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.dropUnlessResumed import androidx.lifecycle.viewmodel.compose.viewModel import info.cemu.cemu.R -import info.cemu.cemu.guicore.components.DefaultAppBarTitle -import info.cemu.cemu.guicore.components.ScreenContentLazy -import info.cemu.cemu.guicore.components.SearchToolbarInput -import info.cemu.cemu.guicore.components.SingleSelection +import info.cemu.cemu.core.components.DefaultAppBarTitle +import info.cemu.cemu.core.components.ScreenContentLazy +import info.cemu.cemu.core.components.SearchToolbarInput +import info.cemu.cemu.core.components.SingleSelection +import info.cemu.cemu.core.translation.tr import kotlinx.coroutines.launch @@ -83,14 +81,13 @@ fun GraphicPacksRootSectionScreen( val context = LocalContext.current downloadStatus?.let { status -> - downloadDialogText = - downloadStatusToDialogTextStringId(status)?.let { stringResource(it) } + downloadDialogText = downloadStatusToDialogTextString(status) LaunchedEffect(status) { graphicPacksListViewModel.downloadStatusRead() val downloadNotificationText = - downloadStatusToNotificationStringId(status)?.let(context::getString) - ?: return@LaunchedEffect + downloadStatusToNotificationString(status) ?: return@LaunchedEffect + snackbarScope.launch { snackbarHostState.currentSnackbarData?.dismiss() snackbarHostState.showSnackbar(downloadNotificationText) @@ -128,10 +125,10 @@ fun GraphicPacksRootSectionScreen( SearchToolbarInput( value = query, onValueChange = graphicPacksListViewModel::setFilterText, - hint = stringResource(R.string.search_graphic_packs), + hint = tr("Search graphic packs"), ) } else { - DefaultAppBarTitle(stringResource(R.string.graphic_packs)) + DefaultAppBarTitle(tr("Graphic packs")) } } }, @@ -159,20 +156,18 @@ fun GraphicPacksRootSectionScreen( } } -@StringRes -private fun downloadStatusToDialogTextStringId(downloadStatus: GraphicPacksDownloadStatus?): Int? = +private fun downloadStatusToDialogTextString(downloadStatus: GraphicPacksDownloadStatus?): String? = when (downloadStatus) { - GraphicPacksDownloadStatus.CheckingForUpdates -> R.string.checking_version_download_text - GraphicPacksDownloadStatus.Downloading -> R.string.graphic_packs_download_text + GraphicPacksDownloadStatus.CheckingForUpdates -> tr("Checking version") + GraphicPacksDownloadStatus.Downloading -> tr("Downloading graphic packs") else -> null } -@StringRes -private fun downloadStatusToNotificationStringId(downloadStatus: GraphicPacksDownloadStatus?): Int? = +private fun downloadStatusToNotificationString(downloadStatus: GraphicPacksDownloadStatus?): String? = when (downloadStatus) { - GraphicPacksDownloadStatus.Error -> R.string.download_graphic_packs_error_text - GraphicPacksDownloadStatus.FinishedDownloading -> R.string.download_graphic_packs_finish_text - GraphicPacksDownloadStatus.NoUpdatesAvailable -> R.string.graphic_packs_no_updates_text + GraphicPacksDownloadStatus.Error -> tr("Failed to download graphic packs") + GraphicPacksDownloadStatus.FinishedDownloading -> tr("Downloaded latest graphic packs") + GraphicPacksDownloadStatus.NoUpdatesAvailable -> tr("No updates available") else -> null } @@ -183,7 +178,7 @@ fun GraphicPacksDownloadDialog( ) { AlertDialog( title = { - Text(text = stringResource(R.string.graphic_packs_download_dialog_title)) + Text(text = tr("Graphic packs download")) }, text = { Column(modifier = Modifier.padding(vertical = 8.dp)) { @@ -203,7 +198,7 @@ fun GraphicPacksDownloadDialog( onCancelRequest() } ) { - Text(stringResource(R.string.cancel)) + Text(tr("Cancel")) } } ) @@ -223,7 +218,7 @@ fun GraphicPacksRootSectionActions( ) { Icon( imageVector = Icons.Filled.Search, - contentDescription = stringResource(R.string.search_graphic_packs) + contentDescription = null ) } IconButton( @@ -231,7 +226,7 @@ fun GraphicPacksRootSectionActions( ) { Icon( painter = painterResource(R.drawable.ic_download), - contentDescription = stringResource(R.string.download_graphic_packs) + contentDescription = null ) } } @@ -241,7 +236,7 @@ fun GraphicPacksRootSectionActions( ) { Icon( imageVector = Icons.Filled.MoreVert, - contentDescription = stringResource(R.string.more_options) + contentDescription = null ) } DropdownMenu( @@ -253,7 +248,7 @@ fun GraphicPacksRootSectionActions( installedOnlyValueChange(!installedOnlyChecked) }, text = { - Text(text = stringResource(R.string.graphic_packs_installed_only)) + Text(text = tr("Installed only")) }, trailingIcon = { Checkbox( @@ -301,7 +296,7 @@ fun GraphicPacksSectionScreen( graphicPackNodeNavigate: (GraphicPackNode) -> Unit, graphicPackSectionNode: GraphicPackSectionNode, ) { - val appBarText = graphicPackSectionNode.name ?: stringResource(R.string.graphic_packs) + val appBarText = graphicPackSectionNode.name ?: tr("Graphic packs") ScreenContentLazy( appBarText = appBarText, navigateBack = navigateBack, @@ -318,7 +313,7 @@ fun GraphicPackDataScreen( navigateBack: () -> Unit, graphicPackDataViewModel: GraphicPackDataViewModel, ) { - val appBarText = graphicPackDataViewModel.name ?: stringResource(R.string.graphic_packs) + val appBarText = graphicPackDataViewModel.name ?: tr("Graphic packs") val enabled by graphicPackDataViewModel.enabled.collectAsState() val presets by graphicPackDataViewModel.presets.collectAsState() @@ -331,7 +326,7 @@ fun GraphicPackDataScreen( modifier = Modifier.padding(8.dp), verticalAlignment = Alignment.CenterVertically ) { - Text(text = stringResource(R.string.graphic_pack_enabled_checkbox_label)) + Text(text = tr("Enabled")) Switch( modifier = Modifier.padding(horizontal = 8.dp), checked = enabled, @@ -348,7 +343,7 @@ fun GraphicPackDataScreen( items(items = presets) { SingleSelection( modifier = Modifier.animateItem(), - label = it.category ?: stringResource(R.string.active_preset_category), + label = it.category ?: tr("Active preset"), choices = it.presets, choice = it.activePreset, onChoiceChanged = { activePreset -> @@ -395,7 +390,7 @@ fun GraphicPackDataListItemIcon(isEnabled: Boolean) { .size(16.dp), imageVector = Icons.Filled.Check, tint = contentColorFor(MaterialTheme.colorScheme.primary), - contentDescription = stringResource(R.string.graphic_pack_is_enabled), + contentDescription = null ) } } @@ -427,7 +422,7 @@ fun GraphicPackListItemIcon( Icon( modifier = Modifier.size(28.dp), painter = painter, - contentDescription = null, + contentDescription = null ) if (showExtraInfo) { Box(modifier = Modifier.align(Alignment.BottomEnd)) { diff --git a/src/android/app/src/main/java/info/cemu/cemu/guicore/nativeenummapper/GameTitlesEnums.kt b/src/android/app/src/main/java/info/cemu/cemu/guicore/nativeenummapper/GameTitlesEnums.kt deleted file mode 100644 index 223e603a..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/guicore/nativeenummapper/GameTitlesEnums.kt +++ /dev/null @@ -1,26 +0,0 @@ -package info.cemu.cemu.guicore.nativeenummapper - -import androidx.annotation.StringRes -import info.cemu.cemu.R -import info.cemu.cemu.nativeinterface.NativeGameTitles - -@StringRes -fun cpuModeToStringId(cpuMode: Int): Int = when (cpuMode) { - NativeGameTitles.CPU_MODE_SINGLECOREINTERPRETER -> R.string.cpu_mode_single_core_interpreter - NativeGameTitles.CPU_MODE_SINGLECORERECOMPILER -> R.string.cpu_mode_single_core_recompiler - NativeGameTitles.CPU_MODE_MULTICORERECOMPILER -> R.string.cpu_mode_multi_core_recompiler - else -> R.string.cpu_mode_auto -} - -@StringRes -fun regionToStringId(region: Int): Int = when (region) { - NativeGameTitles.CONSOLE_REGION_JPN -> R.string.console_region_japan - NativeGameTitles.CONSOLE_REGION_USA -> R.string.console_region_usa - NativeGameTitles.CONSOLE_REGION_EUR -> R.string.console_region_europe - NativeGameTitles.CONSOLE_REGION_AUS_DEPR -> R.string.console_region_australia - NativeGameTitles.CONSOLE_REGION_CHN -> R.string.console_region_china - NativeGameTitles.CONSOLE_REGION_KOR -> R.string.console_region_korea - NativeGameTitles.CONSOLE_REGION_TWN -> R.string.console_region_taiwan - NativeGameTitles.CONSOLE_REGION_AUTO -> R.string.console_region_auto - else -> R.string.console_region_many -} diff --git a/src/android/app/src/main/java/info/cemu/cemu/guicore/nativeenummapper/InputEnums.kt b/src/android/app/src/main/java/info/cemu/cemu/guicore/nativeenummapper/InputEnums.kt deleted file mode 100644 index 30c16944..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/guicore/nativeenummapper/InputEnums.kt +++ /dev/null @@ -1,127 +0,0 @@ -package info.cemu.cemu.guicore.nativeenummapper - -import androidx.annotation.StringRes -import info.cemu.cemu.R -import info.cemu.cemu.nativeinterface.NativeInput - -@StringRes -fun controllerTypeToStringId(type: Int) = when (type) { - NativeInput.EMULATED_CONTROLLER_TYPE_DISABLED -> R.string.disabled - NativeInput.EMULATED_CONTROLLER_TYPE_VPAD -> R.string.vpad_controller - NativeInput.EMULATED_CONTROLLER_TYPE_PRO -> R.string.pro_controller - NativeInput.EMULATED_CONTROLLER_TYPE_WIIMOTE -> R.string.wiimote_controller - NativeInput.EMULATED_CONTROLLER_TYPE_CLASSIC -> R.string.classic_controller - else -> throw IllegalArgumentException("Invalid controller type: $type") -} - -@StringRes -fun proControllerButtonToStringId(buttonId: Int) = when (buttonId) { - NativeInput.PRO_BUTTON_A -> R.string.button_a - NativeInput.PRO_BUTTON_B -> R.string.button_b - NativeInput.PRO_BUTTON_X -> R.string.button_x - NativeInput.PRO_BUTTON_Y -> R.string.button_y - NativeInput.PRO_BUTTON_L -> R.string.button_l - NativeInput.PRO_BUTTON_R -> R.string.button_r - NativeInput.PRO_BUTTON_ZL -> R.string.button_zl - NativeInput.PRO_BUTTON_ZR -> R.string.button_zr - NativeInput.PRO_BUTTON_PLUS -> R.string.button_plus - NativeInput.PRO_BUTTON_MINUS -> R.string.button_minus - NativeInput.PRO_BUTTON_HOME -> R.string.button_home - NativeInput.PRO_BUTTON_UP -> R.string.button_up - NativeInput.PRO_BUTTON_DOWN -> R.string.button_down - NativeInput.PRO_BUTTON_LEFT -> R.string.button_left - NativeInput.PRO_BUTTON_RIGHT -> R.string.button_right - NativeInput.PRO_BUTTON_STICKL -> R.string.button_stickl - NativeInput.PRO_BUTTON_STICKR -> R.string.button_stickr - NativeInput.PRO_BUTTON_STICKL_UP -> R.string.button_stickl_up - NativeInput.PRO_BUTTON_STICKL_DOWN -> R.string.button_stickl_down - NativeInput.PRO_BUTTON_STICKL_LEFT -> R.string.button_stickl_left - NativeInput.PRO_BUTTON_STICKL_RIGHT -> R.string.button_stickl_right - NativeInput.PRO_BUTTON_STICKR_UP -> R.string.button_stickr_up - NativeInput.PRO_BUTTON_STICKR_DOWN -> R.string.button_stickr_down - NativeInput.PRO_BUTTON_STICKR_LEFT -> R.string.button_stickr_left - NativeInput.PRO_BUTTON_STICKR_RIGHT -> R.string.button_stickr_right - else -> throw IllegalArgumentException("Invalid buttonId $buttonId for Pro controller type") -} - -@StringRes -fun classicControllerButtonToStringId(buttonId: Int) = when (buttonId) { - NativeInput.CLASSIC_BUTTON_A -> R.string.button_a - NativeInput.CLASSIC_BUTTON_B -> R.string.button_b - NativeInput.CLASSIC_BUTTON_X -> R.string.button_x - NativeInput.CLASSIC_BUTTON_Y -> R.string.button_y - NativeInput.CLASSIC_BUTTON_L -> R.string.button_l - NativeInput.CLASSIC_BUTTON_R -> R.string.button_r - NativeInput.CLASSIC_BUTTON_ZL -> R.string.button_zl - NativeInput.CLASSIC_BUTTON_ZR -> R.string.button_zr - NativeInput.CLASSIC_BUTTON_PLUS -> R.string.button_plus - NativeInput.CLASSIC_BUTTON_MINUS -> R.string.button_minus - NativeInput.CLASSIC_BUTTON_HOME -> R.string.button_home - NativeInput.CLASSIC_BUTTON_UP -> R.string.button_up - NativeInput.CLASSIC_BUTTON_DOWN -> R.string.button_down - NativeInput.CLASSIC_BUTTON_LEFT -> R.string.button_left - NativeInput.CLASSIC_BUTTON_RIGHT -> R.string.button_right - NativeInput.CLASSIC_BUTTON_STICKL_UP -> R.string.button_stickl_up - NativeInput.CLASSIC_BUTTON_STICKL_DOWN -> R.string.button_stickl_down - NativeInput.CLASSIC_BUTTON_STICKL_LEFT -> R.string.button_stickl_left - NativeInput.CLASSIC_BUTTON_STICKL_RIGHT -> R.string.button_stickl_right - NativeInput.CLASSIC_BUTTON_STICKR_UP -> R.string.button_stickr_up - NativeInput.CLASSIC_BUTTON_STICKR_DOWN -> R.string.button_stickr_down - NativeInput.CLASSIC_BUTTON_STICKR_LEFT -> R.string.button_stickr_left - NativeInput.CLASSIC_BUTTON_STICKR_RIGHT -> R.string.button_stickr_right - else -> throw IllegalArgumentException("Invalid buttonId $buttonId for Classic controller type") -} - -@StringRes -fun vpadButtonToStringId(buttonId: Int) = when (buttonId) { - NativeInput.VPAD_BUTTON_A -> R.string.button_a - NativeInput.VPAD_BUTTON_B -> R.string.button_b - NativeInput.VPAD_BUTTON_X -> R.string.button_x - NativeInput.VPAD_BUTTON_Y -> R.string.button_y - NativeInput.VPAD_BUTTON_L -> R.string.button_l - NativeInput.VPAD_BUTTON_R -> R.string.button_r - NativeInput.VPAD_BUTTON_ZL -> R.string.button_zl - NativeInput.VPAD_BUTTON_ZR -> R.string.button_zr - NativeInput.VPAD_BUTTON_PLUS -> R.string.button_plus - NativeInput.VPAD_BUTTON_MINUS -> R.string.button_minus - NativeInput.VPAD_BUTTON_UP -> R.string.button_up - NativeInput.VPAD_BUTTON_DOWN -> R.string.button_down - NativeInput.VPAD_BUTTON_LEFT -> R.string.button_left - NativeInput.VPAD_BUTTON_RIGHT -> R.string.button_right - NativeInput.VPAD_BUTTON_STICKL -> R.string.button_stickl - NativeInput.VPAD_BUTTON_STICKR -> R.string.button_stickr - NativeInput.VPAD_BUTTON_STICKL_UP -> R.string.button_stickl_up - NativeInput.VPAD_BUTTON_STICKL_DOWN -> R.string.button_stickl_down - NativeInput.VPAD_BUTTON_STICKL_LEFT -> R.string.button_stickl_left - NativeInput.VPAD_BUTTON_STICKL_RIGHT -> R.string.button_stickl_right - NativeInput.VPAD_BUTTON_STICKR_UP -> R.string.button_stickr_up - NativeInput.VPAD_BUTTON_STICKR_DOWN -> R.string.button_stickr_down - NativeInput.VPAD_BUTTON_STICKR_LEFT -> R.string.button_stickr_left - NativeInput.VPAD_BUTTON_STICKR_RIGHT -> R.string.button_stickr_right - NativeInput.VPAD_BUTTON_MIC -> R.string.button_mic - NativeInput.VPAD_BUTTON_SCREEN -> R.string.button_screen - NativeInput.VPAD_BUTTON_HOME -> R.string.button_home - else -> throw IllegalArgumentException("Invalid buttonId $buttonId for VPAD controller type") -} - -@StringRes -fun wiimoteButtonItToStringId(buttonId: Int) = when (buttonId) { - NativeInput.WIIMOTE_BUTTON_A -> R.string.button_a - NativeInput.WIIMOTE_BUTTON_B -> R.string.button_b - NativeInput.WIIMOTE_BUTTON_1 -> R.string.button_1 - NativeInput.WIIMOTE_BUTTON_2 -> R.string.button_2 - NativeInput.WIIMOTE_BUTTON_NUNCHUCK_Z -> R.string.button_nunchuck_z - NativeInput.WIIMOTE_BUTTON_NUNCHUCK_C -> R.string.button_nunchuck_c - NativeInput.WIIMOTE_BUTTON_PLUS -> R.string.button_plus - NativeInput.WIIMOTE_BUTTON_MINUS -> R.string.button_minus - NativeInput.WIIMOTE_BUTTON_UP -> R.string.button_up - NativeInput.WIIMOTE_BUTTON_DOWN -> R.string.button_down - NativeInput.WIIMOTE_BUTTON_LEFT -> R.string.button_left - NativeInput.WIIMOTE_BUTTON_RIGHT -> R.string.button_right - NativeInput.WIIMOTE_BUTTON_NUNCHUCK_UP -> R.string.button_nunchuck_up - NativeInput.WIIMOTE_BUTTON_NUNCHUCK_DOWN -> R.string.button_nunchuck_down - NativeInput.WIIMOTE_BUTTON_NUNCHUCK_LEFT -> R.string.button_nunchuck_left - NativeInput.WIIMOTE_BUTTON_NUNCHUCK_RIGHT -> R.string.button_nunchuck_right - NativeInput.WIIMOTE_BUTTON_HOME -> R.string.button_home - else -> throw IllegalArgumentException("Invalid buttonId $buttonId for Wiimote controller type") -} diff --git a/src/android/app/src/main/java/info/cemu/cemu/guicore/nativeenummapper/SettingsEnums.kt b/src/android/app/src/main/java/info/cemu/cemu/guicore/nativeenummapper/SettingsEnums.kt deleted file mode 100644 index 5b1e7102..00000000 --- a/src/android/app/src/main/java/info/cemu/cemu/guicore/nativeenummapper/SettingsEnums.kt +++ /dev/null @@ -1,66 +0,0 @@ -package info.cemu.cemu.guicore.nativeenummapper - -import androidx.annotation.StringRes -import info.cemu.cemu.R -import info.cemu.cemu.nativeinterface.NativeSettings - -@StringRes -fun channelsToStringId(channels: Int) = when (channels) { - NativeSettings.AUDIO_CHANNELS_MONO -> R.string.mono - NativeSettings.AUDIO_CHANNELS_STEREO -> R.string.stereo - NativeSettings.AUDIO_CHANNELS_SURROUND -> R.string.surround - else -> throw IllegalArgumentException("Invalid channels type: $channels") -} - -@StringRes -fun consoleLanguageToStringId(channels: Int): Int = when (channels) { - NativeSettings.CONSOLE_LANGUAGE_JAPANESE -> R.string.console_language_japanese - NativeSettings.CONSOLE_LANGUAGE_ENGLISH -> R.string.console_language_english - NativeSettings.CONSOLE_LANGUAGE_FRENCH -> R.string.console_language_french - NativeSettings.CONSOLE_LANGUAGE_GERMAN -> R.string.console_language_german - NativeSettings.CONSOLE_LANGUAGE_ITALIAN -> R.string.console_language_italian - NativeSettings.CONSOLE_LANGUAGE_SPANISH -> R.string.console_language_spanish - NativeSettings.CONSOLE_LANGUAGE_CHINESE -> R.string.console_language_chinese - NativeSettings.CONSOLE_LANGUAGE_KOREAN -> R.string.console_language_korean - NativeSettings.CONSOLE_LANGUAGE_DUTCH -> R.string.console_language_dutch - NativeSettings.CONSOLE_LANGUAGE_PORTUGUESE -> R.string.console_language_portuguese - NativeSettings.CONSOLE_LANGUAGE_RUSSIAN -> R.string.console_language_russian - NativeSettings.CONSOLE_LANGUAGE_TAIWANESE -> R.string.console_language_taiwanese - else -> throw IllegalArgumentException("Invalid console language: $channels") -} - -@StringRes -fun vsyncModeToStringId(vsyncMode: Int) = when (vsyncMode) { - NativeSettings.VSYNC_MODE_OFF -> R.string.off - NativeSettings.VSYNC_MODE_DOUBLE_BUFFERING -> R.string.double_buffering - NativeSettings.VSYNC_MODE_TRIPLE_BUFFERING -> R.string.triple_buffering - else -> throw IllegalArgumentException("Invalid vsync mode: $vsyncMode") -} - -@StringRes -fun fullscreenScalingModeToStringId(fullscreenScaling: Int) = when (fullscreenScaling) { - NativeSettings.FULLSCREEN_SCALING_KEEP_ASPECT_RATIO -> R.string.keep_aspect_ratio - NativeSettings.FULLSCREEN_SCALING_STRETCH -> R.string.stretch - else -> throw IllegalArgumentException("Invalid fullscreen scaling mode: $fullscreenScaling") -} - -@StringRes -fun scalingFilterToStringId(scalingFilter: Int) = when (scalingFilter) { - NativeSettings.SCALING_FILTER_BILINEAR_FILTER -> R.string.bilinear - NativeSettings.SCALING_FILTER_BICUBIC_FILTER -> R.string.bicubic - NativeSettings.SCALING_FILTER_BICUBIC_HERMITE_FILTER -> R.string.hermite - NativeSettings.SCALING_FILTER_NEAREST_NEIGHBOR_FILTER -> R.string.nearest_neighbor - else -> throw IllegalArgumentException("Invalid scaling filter: $scalingFilter") -} - -@StringRes -fun overlayScreenPositionToStringId(overlayScreenPosition: Int) = when (overlayScreenPosition) { - NativeSettings.OVERLAY_SCREEN_POSITION_DISABLED -> R.string.overlay_position_disabled - NativeSettings.OVERLAY_SCREEN_POSITION_TOP_LEFT -> R.string.overlay_position_top_left - NativeSettings.OVERLAY_SCREEN_POSITION_TOP_CENTER -> R.string.overlay_position_top_center - NativeSettings.OVERLAY_SCREEN_POSITION_TOP_RIGHT -> R.string.overlay_position_top_right - NativeSettings.OVERLAY_SCREEN_POSITION_BOTTOM_LEFT -> R.string.overlay_position_bottom_left - NativeSettings.OVERLAY_SCREEN_POSITION_BOTTOM_CENTER -> R.string.overlay_position_bottom_center - NativeSettings.OVERLAY_SCREEN_POSITION_BOTTOM_RIGHT -> R.string.overlay_position_bottom_right - else -> throw IllegalArgumentException("Invalid overlay position: $overlayScreenPosition") -} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/input/SensorManager.kt b/src/android/app/src/main/java/info/cemu/cemu/input/SensorManager.kt index ac36a053..e24c89d2 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/input/SensorManager.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/input/SensorManager.kt @@ -19,9 +19,9 @@ class SensorManager(context: Context) : SensorEventListener { private var deviceRotationProvider = { Surface.ROTATION_0 } private val hasMotionData = accelerometer != null && gyroscope != null - private var accelX = 0f - private var accelY = 0f - private var accelZ = 0f + private var gyroX = 0f + private var gyroY = 0f + private var gyroZ = 0f private var isListening = false fun startListening() { @@ -47,20 +47,21 @@ class SensorManager(context: Context) : SensorEventListener { sensorManager.unregisterListener(this) } + override fun onSensorChanged(event: SensorEvent) { val values = event.values - if (event.sensor.type == Sensor.TYPE_ACCELEROMETER) { - val accelValues = getSensorEventValues(values) - accelX = accelValues.first - accelY = accelValues.second - accelZ = accelValues.third + if (event.sensor.type == Sensor.TYPE_GYROSCOPE) { + val gyroValues = getSensorEventValues(values) + gyroX = gyroValues.first + gyroY = gyroValues.second + gyroZ = gyroValues.third return } - if (event.sensor.type != Sensor.TYPE_GYROSCOPE) { + if (event.sensor.type != Sensor.TYPE_ACCELEROMETER) { return } - val (gyroX, gyroY, gyroZ) = getSensorEventValues(values) - onMotion(event.timestamp, gyroX, gyroY, gyroZ, accelX, accelY, accelZ) + val (accelX, accelY, accelZ) = getSensorEventValues(values) + onMotion(event.timestamp, gyroX, gyroY, gyroZ, accelX, accelZ, -accelY) } override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) { diff --git a/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeException.kt b/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeException.kt index c3d9ac20..a1982492 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeException.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeException.kt @@ -1,3 +1,3 @@ package info.cemu.cemu.nativeinterface -class NativeException(message: String?) : RuntimeException(message) +class NativeException(message: String) : RuntimeException(message) diff --git a/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeLocalization.kt b/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeLocalization.kt new file mode 100644 index 00000000..daa4c2fb --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/nativeinterface/NativeLocalization.kt @@ -0,0 +1,6 @@ +package info.cemu.cemu.nativeinterface + +object NativeLocalization { + @JvmStatic + external fun setTranslations(translations: Map) +} diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/SettingsHomeScreen.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/SettingsHomeScreen.kt index 67ede0aa..a7fb625b 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/SettingsHomeScreen.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/SettingsHomeScreen.kt @@ -1,12 +1,10 @@ package info.cemu.cemu.settings import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.dropUnlessResumed -import info.cemu.cemu.R -import info.cemu.cemu.guicore.components.Button -import info.cemu.cemu.guicore.components.ScreenContent +import info.cemu.cemu.core.components.Button +import info.cemu.cemu.core.components.ScreenContent +import info.cemu.cemu.core.translation.tr data class SettingsHomeScreenActions( val goToGeneralSettings: () -> Unit, @@ -19,27 +17,27 @@ data class SettingsHomeScreenActions( @Composable fun SettingsHomeScreen(navigateBack: () -> Unit, actions: SettingsHomeScreenActions) { ScreenContent( - appBarText = stringResource(R.string.settings), + appBarText = tr("Settings"), navigateBack = navigateBack, ) { Button( - label = stringResource(R.string.general_settings), + label = tr("General settings"), onClick = dropUnlessResumed(block = actions.goToGeneralSettings) ) Button( - label = stringResource(R.string.input_settings), + label = tr("Input settings"), onClick = dropUnlessResumed(block = actions.goToInputSettings) ) Button( - label = stringResource(R.string.graphics_settings), + label = tr("Graphics settings"), onClick = dropUnlessResumed(block = actions.goToGraphicsSettings) ) Button( - label = stringResource(R.string.audio_settings), + label = tr("Audio settings"), onClick = dropUnlessResumed(block = actions.goToAudioSettings) ) Button( - label = stringResource(R.string.overlay_settings), + label = tr("Overlay settings"), onClick = dropUnlessResumed(block = actions.goToOverlaySettings) ) } diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/SettingsManager.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/SettingsManager.kt index 105bd832..82dd66b0 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/SettingsManager.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/SettingsManager.kt @@ -2,6 +2,7 @@ package info.cemu.cemu.settings import android.content.Context import android.content.SharedPreferences +import info.cemu.cemu.core.translation.DEFAULT_LANGUAGE class EmulationScreenSettings( var isDrawerButtonVisible: Boolean, @@ -25,6 +26,26 @@ class EmulationScreenSettings( } } +class GuiSettings(var language: String) { + companion object { + private const val LANGUAGE_KEY = "LANGUAGE" + } + + constructor(sharedPreferences: SharedPreferences) : this( + language = sharedPreferences.getString( + LANGUAGE_KEY, + null + ) ?: DEFAULT_LANGUAGE, + ) + + fun save(sharedPreferences: SharedPreferences) { + sharedPreferences.edit().apply { + putString(LANGUAGE_KEY, language) + apply() + } + } +} + class SettingsManager(context: Context) { private val sharedPreferences: SharedPreferences = context.getSharedPreferences(SETTINGS_NAME, Context.MODE_PRIVATE) @@ -33,6 +54,9 @@ class SettingsManager(context: Context) { get() = EmulationScreenSettings(sharedPreferences) set(value) = value.save(sharedPreferences) + var guiSettings: GuiSettings + get() = GuiSettings(sharedPreferences) + set(value) = value.save(sharedPreferences) companion object { private const val SETTINGS_NAME = "SETTINGS" diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/audio/AudoSettingsScreen.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/audio/AudoSettingsScreen.kt index 9bea3f53..996b0a87 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/audio/AudoSettingsScreen.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/audio/AudoSettingsScreen.kt @@ -3,11 +3,11 @@ package info.cemu.cemu.settings.audio import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import info.cemu.cemu.R -import info.cemu.cemu.guicore.components.ScreenContent -import info.cemu.cemu.guicore.components.SingleSelection -import info.cemu.cemu.guicore.components.Slider -import info.cemu.cemu.guicore.components.Toggle -import info.cemu.cemu.guicore.nativeenummapper.channelsToStringId +import info.cemu.cemu.core.components.ScreenContent +import info.cemu.cemu.core.components.SingleSelection +import info.cemu.cemu.core.components.Slider +import info.cemu.cemu.core.components.Toggle +import info.cemu.cemu.core.translation.tr import info.cemu.cemu.nativeinterface.NativeSettings private const val AUDIO_LATENCY_STEPS = 22 @@ -15,11 +15,11 @@ private const val AUDIO_LATENCY_STEPS = 22 @Composable fun AudioSettingsScreen(navigateBack: () -> Unit) { ScreenContent( - appBarText = stringResource(R.string.audio_settings), + appBarText = tr("Audio settings"), navigateBack = navigateBack, ) { Slider( - label = stringResource(R.string.audio_latency), + label = tr("Latency"), initialValue = NativeSettings::getAudioLatency, valueFrom = 0, steps = AUDIO_LATENCY_STEPS, @@ -28,16 +28,16 @@ fun AudioSettingsScreen(navigateBack: () -> Unit) { labelFormatter = { "${it}ms" } ) Toggle( - label = stringResource(R.string.tv_audio), - description = stringResource(R.string.tv_audio_description), + label = tr("TV"), + description = tr("Enable audio output for the Wii U TV"), initialCheckedState = { NativeSettings.getAudioDeviceEnabled(true) }, onCheckedChanged = { NativeSettings.setAudioDeviceEnabled(it, true) } ) SingleSelection( - label = stringResource(R.string.tv_channels), + label = tr("TV channels"), initialChoice = { NativeSettings.getAudioDeviceChannels(true) }, onChoiceChanged = { NativeSettings.setAudioDeviceChannels(it, true) }, - choiceToString = { stringResource(channelsToStringId(it)) }, + choiceToString = { channelsToString(it) }, choices = listOf( NativeSettings.AUDIO_CHANNELS_MONO, NativeSettings.AUDIO_CHANNELS_STEREO, @@ -45,7 +45,7 @@ fun AudioSettingsScreen(navigateBack: () -> Unit) { ), ) Slider( - label = stringResource(R.string.tv_volume), + label = tr("TV volume"), initialValue = { NativeSettings.getAudioDeviceVolume(true) }, valueFrom = NativeSettings.AUDIO_MIN_VOLUME, valueTo = NativeSettings.AUDIO_MAX_VOLUME, @@ -53,22 +53,22 @@ fun AudioSettingsScreen(navigateBack: () -> Unit) { labelFormatter = { "$it%" } ) Toggle( - label = stringResource(R.string.gamepad_audio), - description = stringResource(R.string.gamepad_audio_description), + label = tr("Gamepad"), + description = tr("Enable audio output for the Wii U Gamepad"), initialCheckedState = { NativeSettings.getAudioDeviceEnabled(false) }, onCheckedChanged = { NativeSettings.setAudioDeviceEnabled(false, it) } ) SingleSelection( - label = stringResource(R.string.gamepad_channels), + label = tr("Gamepad channels"), initialChoice = { NativeSettings.getAudioDeviceChannels(false) }, onChoiceChanged = { NativeSettings.setAudioDeviceChannels(it, false) }, - choiceToString = { stringResource(channelsToStringId(it)) }, + choiceToString = { channelsToString(it) }, choices = listOf( NativeSettings.AUDIO_CHANNELS_STEREO, ), ) Slider( - label = stringResource(R.string.gamepad_volume), + label = tr("Gamepad volume"), initialValue = { NativeSettings.getAudioDeviceVolume(false) }, valueFrom = NativeSettings.AUDIO_MIN_VOLUME, valueTo = NativeSettings.AUDIO_MAX_VOLUME, @@ -76,4 +76,11 @@ fun AudioSettingsScreen(navigateBack: () -> Unit) { labelFormatter = { "$it%" } ) } -} \ No newline at end of file +} + +fun channelsToString(channels: Int) = when (channels) { + NativeSettings.AUDIO_CHANNELS_MONO -> tr("Mono") + NativeSettings.AUDIO_CHANNELS_STEREO -> tr("Stereo") + NativeSettings.AUDIO_CHANNELS_SURROUND -> tr("Surround") + else -> throw IllegalArgumentException("Invalid channels type: $channels") +} diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/customdrivers/CustomDriversScreen.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/customdrivers/CustomDriversScreen.kt index 383632ed..40f566b8 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/customdrivers/CustomDriversScreen.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/customdrivers/CustomDriversScreen.kt @@ -4,7 +4,6 @@ import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.animateContentSize import androidx.compose.foundation.basicMarquee -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope @@ -17,7 +16,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.AlertDialog import androidx.compose.material3.Card import androidx.compose.material3.Icon @@ -45,7 +43,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.viewmodel.compose.viewModel import info.cemu.cemu.R -import info.cemu.cemu.guicore.components.ScreenContentLazy +import info.cemu.cemu.core.components.ScreenContentLazy +import info.cemu.cemu.core.translation.tr import kotlinx.coroutines.launch @Composable @@ -66,9 +65,9 @@ fun CustomDriversScreen( customDriversViewModel.installDriver(context, uri) { installStatus -> val message = when (installStatus) { - DriverInstallStatus.AlreadyInstalled -> "Driver already installed" - DriverInstallStatus.ErrorInstalling -> "Failed to install driver" - DriverInstallStatus.Installed -> "Driver installed successfully" + DriverInstallStatus.AlreadyInstalled -> tr("Driver already installed") + DriverInstallStatus.ErrorInstalling -> tr("Failed to install driver") + DriverInstallStatus.Installed -> tr("Driver installed successfully") } coroutineScope.launch { @@ -80,13 +79,13 @@ fun CustomDriversScreen( ScreenContentLazy( snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, - appBarText = "Custom drivers", + appBarText = tr("Custom drivers"), navigateBack = navigateBack, actions = { IconButton(onClick = { customDriversInstallLauncher.launch(arrayOf("application/zip")) }) { Icon( imageVector = Icons.Filled.Add, - contentDescription = "Add custom driver", + contentDescription = null ) } }, @@ -114,14 +113,14 @@ fun CustomDriversScreen( private fun DriverInstallProgressDialog() { AlertDialog( title = { - Text("Installing") + Text(tr("Installing")) }, text = { Column( modifier = Modifier.padding(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - Text("Installing driver in progress") + Text(tr("Installing driver in progress")) LinearProgressIndicator( modifier = Modifier .fillMaxWidth() @@ -138,7 +137,7 @@ private fun DriverInstallProgressDialog() { @Composable private fun SystemDriverListItem(selected: Boolean, onSelect: () -> Unit) { DriverListItem( - driverLabel = "System driver", + driverLabel = tr("System driver"), selected = selected, onSelect = onSelect ) @@ -157,13 +156,13 @@ private fun CustomDriverListItem(driver: Driver, onDelete: () -> Unit, onSelect: Icon( modifier = Modifier.rotate(if (showDriverInfo) 180f else 0f), imageVector = Icons.Filled.ArrowDropDown, - contentDescription = "Show driver metadata" + contentDescription = null ) } IconButton(onClick = onDelete) { Icon( imageVector = Icons.Filled.Delete, - contentDescription = stringResource(R.string.remove_game_path), + contentDescription = null ) } } @@ -215,12 +214,12 @@ private fun DriverMetadataInfo(metadata: DriverMetadata) { .fillMaxWidth() .padding(8.dp) ) { - DriverMetadataInfo("Description", metadata.description) - DriverMetadataInfo("Author", metadata.author) - DriverMetadataInfo("Package version", metadata.packageVersion) - DriverMetadataInfo("Vendor", metadata.vendor) - DriverMetadataInfo("Driver version", metadata.driverVersion) - DriverMetadataInfo("Min api", metadata.minApi) + DriverMetadataInfo(tr("Description"), metadata.description) + DriverMetadataInfo(tr("Author"), metadata.author) + DriverMetadataInfo(tr("Package version"), metadata.packageVersion) + DriverMetadataInfo(tr("Vendor"), metadata.vendor) + DriverMetadataInfo(tr("Driver version"), metadata.driverVersion) + DriverMetadataInfo(tr("Min api"), metadata.minApi) } } diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/gamespath/GamePathsScreen.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/gamespath/GamePathsScreen.kt index d1b2b3d2..82ae0fce 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/gamespath/GamePathsScreen.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/gamespath/GamePathsScreen.kt @@ -25,12 +25,11 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.viewmodel.compose.viewModel -import info.cemu.cemu.R -import info.cemu.cemu.guicore.components.ScreenContentLazy +import info.cemu.cemu.core.components.ScreenContentLazy +import info.cemu.cemu.core.translation.tr import kotlinx.coroutines.launch @Composable @@ -55,7 +54,7 @@ fun GamePathsScreen( if (gamesPaths.contains(gamesPath)) { coroutineScope.launch { snackbarHostState.currentSnackbarData?.dismiss() - snackbarHostState.showSnackbar(context.getString(R.string.games_path_already_added)) + snackbarHostState.showSnackbar(tr("Games path already added")) } return@rememberLauncherForActivityResult } @@ -63,13 +62,13 @@ fun GamePathsScreen( } ScreenContentLazy( snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, - appBarText = stringResource(R.string.game_paths_settings), + appBarText = tr("Game paths"), navigateBack = navigateBack, actions = { IconButton(onClick = { launcher.launch(null) }) { Icon( imageVector = Icons.Filled.Add, - contentDescription = stringResource(R.string.add_game_path), + contentDescription = null ) } }, @@ -110,7 +109,7 @@ fun GamePathsListItem( ) { Icon( imageVector = Icons.Filled.Delete, - contentDescription = stringResource(R.string.remove_game_path), + contentDescription = null ) } } diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/general/GeneralSettingsScreen.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/general/GeneralSettingsScreen.kt index eb76c824..1e2c835c 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/general/GeneralSettingsScreen.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/general/GeneralSettingsScreen.kt @@ -1,15 +1,14 @@ package info.cemu.cemu.settings.general import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource +import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.dropUnlessResumed import androidx.lifecycle.viewmodel.compose.viewModel -import info.cemu.cemu.R -import info.cemu.cemu.guicore.components.Button -import info.cemu.cemu.guicore.components.ScreenContent -import info.cemu.cemu.guicore.components.SingleSelection -import info.cemu.cemu.guicore.components.Toggle -import info.cemu.cemu.guicore.nativeenummapper.consoleLanguageToStringId +import info.cemu.cemu.core.components.Button +import info.cemu.cemu.core.components.ScreenContent +import info.cemu.cemu.core.components.SingleSelection +import info.cemu.cemu.core.components.Toggle +import info.cemu.cemu.core.translation.tr import info.cemu.cemu.nativeinterface.NativeSettings @Composable @@ -18,20 +17,29 @@ fun GeneralSettingsScreen( goToGamePathsSettings: () -> Unit, generalSettingsViewModel: GeneralSettingsViewModel = viewModel(factory = GeneralSettingsViewModel.Factory), ) { + val context = LocalContext.current + ScreenContent( - appBarText = stringResource(R.string.general_settings), + appBarText = tr("General settings"), navigateBack = navigateBack, ) { Button( - label = stringResource(R.string.add_game_path), - description = stringResource(R.string.games_folder_description), + label = "Add game path", + description = tr("Add the root directory of your game(s). It will scan all directories in it for games"), onClick = dropUnlessResumed { goToGamePathsSettings() }, ) SingleSelection( - label = stringResource(R.string.console_language), + label = tr("Language"), + initialChoice = { generalSettingsViewModel.guiSettings.language }, + onChoiceChanged = { generalSettingsViewModel.setLanguage(language = it, context) }, + choiceToString = { generalSettingsViewModel.languageToDisplayNameMap[it] ?: it }, + choices = generalSettingsViewModel.languages, + ) + SingleSelection( + label = tr("Console language"), initialChoice = NativeSettings::getConsoleLanguage, onChoiceChanged = NativeSettings::setConsoleLanguage, - choiceToString = { stringResource(consoleLanguageToStringId(it)) }, + choiceToString = { consoleLanguageToString(it) }, choices = listOf( NativeSettings.CONSOLE_LANGUAGE_JAPANESE, NativeSettings.CONSOLE_LANGUAGE_ENGLISH, @@ -48,12 +56,28 @@ fun GeneralSettingsScreen( ), ) Toggle( - label = stringResource(R.string.side_menu_button), - description = stringResource(R.string.side_menu_button_description), + label = tr("Side menu button"), + description = tr("Show the emulation side menu button"), initialCheckedState = { generalSettingsViewModel.emulationScreenSettings.isDrawerButtonVisible }, onCheckedChanged = { generalSettingsViewModel.emulationScreenSettings.isDrawerButtonVisible = it } ) } +} + +private fun consoleLanguageToString(channels: Int): String = when (channels) { + NativeSettings.CONSOLE_LANGUAGE_JAPANESE -> tr("Japanese") + NativeSettings.CONSOLE_LANGUAGE_ENGLISH -> tr("English") + NativeSettings.CONSOLE_LANGUAGE_FRENCH -> tr("French") + NativeSettings.CONSOLE_LANGUAGE_GERMAN -> tr("German") + NativeSettings.CONSOLE_LANGUAGE_ITALIAN -> tr("Italian") + NativeSettings.CONSOLE_LANGUAGE_SPANISH -> tr("Spanish") + NativeSettings.CONSOLE_LANGUAGE_CHINESE -> tr("Chinese") + NativeSettings.CONSOLE_LANGUAGE_KOREAN -> tr("Korean") + NativeSettings.CONSOLE_LANGUAGE_DUTCH -> tr("Dutch") + NativeSettings.CONSOLE_LANGUAGE_PORTUGUESE -> tr("Portuguese") + NativeSettings.CONSOLE_LANGUAGE_RUSSIAN -> tr("Russian") + NativeSettings.CONSOLE_LANGUAGE_TAIWANESE -> tr("Taiwanese") + else -> throw IllegalArgumentException("Invalid console language: $channels") } \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/general/GeneralSettingsViewModel.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/general/GeneralSettingsViewModel.kt index c71fad12..cab4614c 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/general/GeneralSettingsViewModel.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/general/GeneralSettingsViewModel.kt @@ -1,20 +1,39 @@ package info.cemu.cemu.settings.general +import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory import info.cemu.cemu.CemuApplication +import info.cemu.cemu.core.translation.getAvailableLanguages +import info.cemu.cemu.settings.EmulationScreenSettings +import info.cemu.cemu.settings.GuiSettings import info.cemu.cemu.settings.SettingsManager -class GeneralSettingsViewModel( - private val settingsManager: SettingsManager, -) : ViewModel() { - val emulationScreenSettings = settingsManager.emulationScreenSettings +class GeneralSettingsViewModel(private val settingsManager: SettingsManager) : ViewModel() { + val languages: List + val languageToDisplayNameMap: Map + val emulationScreenSettings: EmulationScreenSettings + val guiSettings: GuiSettings + + init { + this.emulationScreenSettings = settingsManager.emulationScreenSettings + this.guiSettings = settingsManager.guiSettings + val availableLanguages = getAvailableLanguages() + languages = availableLanguages.map { it.code } + languageToDisplayNameMap = availableLanguages.associateBy({ it.code }, { it.displayName }) + } + + fun setLanguage(language: String, context: Context) { + info.cemu.cemu.core.translation.setLanguage(language, context) + guiSettings.language = language + } override fun onCleared() { super.onCleared() settingsManager.emulationScreenSettings = emulationScreenSettings + settingsManager.guiSettings = guiSettings } companion object { diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/graphics/GraphicsSettingsScreen.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/graphics/GraphicsSettingsScreen.kt index 403b6d76..c15f5ece 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/graphics/GraphicsSettingsScreen.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/graphics/GraphicsSettingsScreen.kt @@ -2,15 +2,11 @@ package info.cemu.cemu.settings.graphics import androidx.compose.runtime.Composable import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.res.stringResource -import info.cemu.cemu.R -import info.cemu.cemu.guicore.components.Button -import info.cemu.cemu.guicore.components.ScreenContent -import info.cemu.cemu.guicore.components.SingleSelection -import info.cemu.cemu.guicore.components.Toggle -import info.cemu.cemu.guicore.nativeenummapper.fullscreenScalingModeToStringId -import info.cemu.cemu.guicore.nativeenummapper.scalingFilterToStringId -import info.cemu.cemu.guicore.nativeenummapper.vsyncModeToStringId +import info.cemu.cemu.core.components.Button +import info.cemu.cemu.core.components.ScreenContent +import info.cemu.cemu.core.components.SingleSelection +import info.cemu.cemu.core.components.Toggle +import info.cemu.cemu.core.translation.tr import info.cemu.cemu.nativeinterface.NativeEmulation import info.cemu.cemu.nativeinterface.NativeSettings @@ -27,26 +23,26 @@ fun GraphicsSettingsScreen(navigateBack: () -> Unit, goToCustomDriversSettings: rememberSaveable { NativeEmulation.supportsLoadingCustomDriver() } ScreenContent( - appBarText = stringResource(R.string.general_settings), + appBarText = tr("Graphics settings"), navigateBack = navigateBack, ) { if (supportsLoadingCustomDrivers) { Button( - label = stringResource(R.string.custom_drivers), + label = tr("Custom drivers"), onClick = goToCustomDriversSettings ) } Toggle( - label = stringResource(R.string.async_shader_compile), - description = stringResource(R.string.async_shader_compile_description), + label = tr("Async shader compile"), + description = tr("Enables async shader and pipeline compilation. Reduces stutter at the cost of objects not rendering for a short time.\nVulkan only"), initialCheckedState = NativeSettings::getAsyncShaderCompile, onCheckedChanged = NativeSettings::setAsyncShaderCompile, ) SingleSelection( - label = stringResource(R.string.vsync), + label = tr("VSync"), initialChoice = NativeSettings::getVsyncMode, onChoiceChanged = NativeSettings::setVsyncMode, - choiceToString = { stringResource(vsyncModeToStringId(it)) }, + choiceToString = { vsyncModeToString(it) }, choices = listOf( NativeSettings.VSYNC_MODE_OFF, NativeSettings.VSYNC_MODE_DOUBLE_BUFFERING, @@ -54,34 +50,55 @@ fun GraphicsSettingsScreen(navigateBack: () -> Unit, goToCustomDriversSettings: ), ) Toggle( - label = stringResource(R.string.accurate_barriers), - description = stringResource(R.string.accurate_barriers_description), + label = tr("Accurate barriers"), + description = tr("Disabling the accurate barriers option will lead to flickering graphics but may improve performance. It is highly recommended to leave it turned on"), initialCheckedState = NativeSettings::getAccurateBarriers, onCheckedChanged = NativeSettings::setAccurateBarriers, ) SingleSelection( - label = stringResource(R.string.fullscreen_scaling), + label = tr("Fullscreen scaling"), initialChoice = NativeSettings::getFullscreenScaling, onChoiceChanged = NativeSettings::setFullscreenScaling, - choiceToString = { stringResource(fullscreenScalingModeToStringId(it)) }, + choiceToString = { fullscreenScalingModeToString(it) }, choices = listOf( NativeSettings.FULLSCREEN_SCALING_KEEP_ASPECT_RATIO, NativeSettings.FULLSCREEN_SCALING_STRETCH ), ) SingleSelection( - label = stringResource(R.string.upscale_filter), + label = tr("Upscale filter"), initialChoice = NativeSettings::getUpscalingFilter, onChoiceChanged = NativeSettings::setUpscalingFilter, - choiceToString = { stringResource(scalingFilterToStringId(it)) }, + choiceToString = { scalingFilterToString(it) }, choices = SCALING_FILTER_CHOICES, ) SingleSelection( - label = stringResource(R.string.downscale_filter), + label = tr("Downscale filter"), initialChoice = NativeSettings::getDownscalingFilter, onChoiceChanged = NativeSettings::setDownscalingFilter, - choiceToString = { stringResource(scalingFilterToStringId(it)) }, + choiceToString = { scalingFilterToString(it) }, choices = SCALING_FILTER_CHOICES, ) } +} + +private fun scalingFilterToString(scalingFilter: Int) = when (scalingFilter) { + NativeSettings.SCALING_FILTER_BILINEAR_FILTER -> tr("Bilinear") + NativeSettings.SCALING_FILTER_BICUBIC_FILTER -> tr("Bicubic") + NativeSettings.SCALING_FILTER_BICUBIC_HERMITE_FILTER -> tr("Hermite") + NativeSettings.SCALING_FILTER_NEAREST_NEIGHBOR_FILTER -> tr("Nearest neighbor") + else -> throw IllegalArgumentException("Invalid scaling filter: $scalingFilter") +} + +private fun vsyncModeToString(vsyncMode: Int) = when (vsyncMode) { + NativeSettings.VSYNC_MODE_OFF -> tr("Off") + NativeSettings.VSYNC_MODE_DOUBLE_BUFFERING -> tr("Double buffering") + NativeSettings.VSYNC_MODE_TRIPLE_BUFFERING -> tr("Triple buffering") + else -> throw IllegalArgumentException("Invalid vsync mode: $vsyncMode") +} + +private fun fullscreenScalingModeToString(fullscreenScaling: Int) = when (fullscreenScaling) { + NativeSettings.FULLSCREEN_SCALING_KEEP_ASPECT_RATIO -> tr("Keep aspect ratio") + NativeSettings.FULLSCREEN_SCALING_STRETCH -> tr("Stretch") + else -> throw IllegalArgumentException("Invalid fullscreen scaling mode: $fullscreenScaling") } \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/input/ClassicControllerInputs.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/input/ClassicControllerInputs.kt index 816a7582..709c23b4 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/input/ClassicControllerInputs.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/input/ClassicControllerInputs.kt @@ -1,9 +1,7 @@ package info.cemu.cemu.settings.input import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import info.cemu.cemu.R -import info.cemu.cemu.guicore.nativeenummapper.classicControllerButtonToStringId +import info.cemu.cemu.core.translation.tr import info.cemu.cemu.nativeinterface.NativeInput @Composable @@ -19,13 +17,13 @@ fun ClassicControllerInputs( InputItemsGroup( groupName = groupName, inputIds = inputIds, - inputIdToString = { stringResource(classicControllerButtonToStringId(it)) }, + inputIdToString = ::classicControllerButtonToString, onInputClick = onInputClick, controlsMapping = controlsMapping, ) } InputItemsGroup( - groupName = stringResource(R.string.buttons), + groupName = tr("Buttons"), inputIds = listOf( NativeInput.CLASSIC_BUTTON_A, NativeInput.CLASSIC_BUTTON_B, @@ -41,7 +39,7 @@ fun ClassicControllerInputs( ) ) InputItemsGroup( - groupName = stringResource(R.string.d_pad), + groupName = tr("D-pad"), inputIds = listOf( NativeInput.CLASSIC_BUTTON_UP, NativeInput.CLASSIC_BUTTON_DOWN, @@ -50,7 +48,7 @@ fun ClassicControllerInputs( ) ) InputItemsGroup( - groupName = stringResource(R.string.left_axis), + groupName = tr("Left Axis"), inputIds = listOf( NativeInput.CLASSIC_BUTTON_STICKL_UP, NativeInput.CLASSIC_BUTTON_STICKL_DOWN, @@ -59,7 +57,7 @@ fun ClassicControllerInputs( ) ) InputItemsGroup( - groupName = stringResource(R.string.right_axis), + groupName = tr("Right Axis"), inputIds = listOf( NativeInput.CLASSIC_BUTTON_STICKR_UP, NativeInput.CLASSIC_BUTTON_STICKR_DOWN, @@ -68,3 +66,30 @@ fun ClassicControllerInputs( ) ) } + +private fun classicControllerButtonToString(buttonId: Int) = when (buttonId) { + NativeInput.CLASSIC_BUTTON_A -> "A" + NativeInput.CLASSIC_BUTTON_B -> "B" + NativeInput.CLASSIC_BUTTON_X -> "X" + NativeInput.CLASSIC_BUTTON_Y -> "Y" + NativeInput.CLASSIC_BUTTON_L -> "L" + NativeInput.CLASSIC_BUTTON_R -> "R" + NativeInput.CLASSIC_BUTTON_ZL -> "ZL" + NativeInput.CLASSIC_BUTTON_ZR -> "ZR" + NativeInput.CLASSIC_BUTTON_PLUS -> "+" + NativeInput.CLASSIC_BUTTON_MINUS -> "-" + NativeInput.CLASSIC_BUTTON_HOME -> tr("home") + NativeInput.CLASSIC_BUTTON_UP -> tr("up") + NativeInput.CLASSIC_BUTTON_DOWN -> tr("down") + NativeInput.CLASSIC_BUTTON_LEFT -> tr("left") + NativeInput.CLASSIC_BUTTON_RIGHT -> tr("right") + NativeInput.CLASSIC_BUTTON_STICKL_UP -> tr("up") + NativeInput.CLASSIC_BUTTON_STICKL_DOWN -> tr("down") + NativeInput.CLASSIC_BUTTON_STICKL_LEFT -> tr("left") + NativeInput.CLASSIC_BUTTON_STICKL_RIGHT -> tr("right") + NativeInput.CLASSIC_BUTTON_STICKR_UP -> tr("up") + NativeInput.CLASSIC_BUTTON_STICKR_DOWN -> tr("down") + NativeInput.CLASSIC_BUTTON_STICKR_LEFT -> tr("left") + NativeInput.CLASSIC_BUTTON_STICKR_RIGHT -> tr("right") + else -> throw IllegalArgumentException("Invalid buttonId $buttonId for Classic controller type") +} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/input/ControllerInputSettingsScreen.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/input/ControllerInputSettingsScreen.kt index 8a8d4dc4..ca466a6e 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/input/ControllerInputSettingsScreen.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/input/ControllerInputSettingsScreen.kt @@ -28,22 +28,20 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.lifecycle.viewmodel.MutableCreationExtras import androidx.lifecycle.viewmodel.compose.viewModel import com.google.android.material.dialog.MaterialAlertDialogBuilder -import info.cemu.cemu.R -import info.cemu.cemu.guicore.components.ScreenContent -import info.cemu.cemu.guicore.components.SingleSelection -import info.cemu.cemu.guicore.nativeenummapper.controllerTypeToStringId +import info.cemu.cemu.core.components.ScreenContent +import info.cemu.cemu.core.components.SingleSelection +import info.cemu.cemu.core.nativeenummapper.controllerTypeToString +import info.cemu.cemu.core.translation.tr import info.cemu.cemu.nativeinterface.NativeInput import kotlinx.coroutines.launch import androidx.compose.material3.Button as MaterialButton - @Composable fun ControllerInputSettingsScreen( navigateBack: () -> Unit, @@ -74,13 +72,13 @@ fun ControllerInputSettingsScreen( ScreenContent( snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, - appBarText = stringResource(R.string.controller_numbered, controllerIndex + 1), + appBarText = tr("Controller {0}", controllerIndex + 1), navigateBack = navigateBack, ) { SingleSelection( isChoiceEnabled = controllersViewModel::isControllerTypeAllowed, - label = stringResource(R.string.emulated_controller), + label = tr("Emulated controller"), initialChoice = { controllerType }, choices = listOf( NativeInput.EMULATED_CONTROLLER_TYPE_DISABLED, @@ -89,7 +87,7 @@ fun ControllerInputSettingsScreen( NativeInput.EMULATED_CONTROLLER_TYPE_CLASSIC, NativeInput.EMULATED_CONTROLLER_TYPE_WIIMOTE ), - choiceToString = { stringResource(controllerTypeToStringId(it)) }, + choiceToString = { controllerTypeToString(it) }, onChoiceChanged = controllersViewModel::setControllerType ) @@ -100,12 +98,12 @@ fun ControllerInputSettingsScreen( controllersViewModel.refreshAvailableControllers { coroutineScope.launch { snackbarHostState.currentSnackbarData?.dismiss() - snackbarHostState.showSnackbar(context.getString(R.string.no_controllers_available)) + snackbarHostState.showSnackbar(tr("No controllers available")) } } } ) { - Text(stringResource(R.string.controller_setup_all_inputs)) + Text(tr("Setup all inputs")) } } @@ -166,7 +164,7 @@ private fun ControllerSelectDialog( top = 16.dp, bottom = 8.dp ), - text = stringResource(R.string.controller_select_dialog_title), + text = tr("Select a controller"), fontSize = 24.sp, ) Column( @@ -195,23 +193,23 @@ private fun ControllerSelectDialog( .padding(8.dp) .align(Alignment.End), ) { - Text(stringResource(R.string.cancel)) + Text(tr("Cancel")) } } } } -fun openInputDialog( +private fun openInputDialog( context: Context, buttonName: String, onClear: () -> Unit, mapKeyEvent: (KeyEvent) -> Unit, tryMapMotionEvent: (MotionEvent) -> Boolean, ) { - MaterialAlertDialogBuilder(context).setTitle(R.string.inputBindingDialogTitle) - .setMessage(context.getString(R.string.inputBindingDialogMessage, buttonName)) - .setNeutralButton(context.getString(R.string.clear)) { _, _ -> onClear() } - .setNegativeButton(context.getString(R.string.cancel)) { _, _ -> } + MaterialAlertDialogBuilder(context).setTitle(tr("Input binding")) + .setMessage(tr("Trigger an input to bind it to {0}", buttonName)) + .setNeutralButton(tr("Clear")) { _, _ -> onClear() } + .setNegativeButton(tr("Cancel")) { _, _ -> } .show() .also { alertDialog -> alertDialog.requireViewById(android.R.id.message).apply { diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/input/InputItemGroup.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/input/InputItemGroup.kt index 45361bab..ad68cd79 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/input/InputItemGroup.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/input/InputItemGroup.kt @@ -10,13 +10,13 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import info.cemu.cemu.guicore.components.Header +import info.cemu.cemu.core.components.Header @Composable fun InputItemsGroup( groupName: String, inputIds: List, - inputIdToString: @Composable (Int) -> String, + inputIdToString: (Int) -> String, onInputClick: (String, Int) -> Unit, controlsMapping: Map, ) { diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/input/InputSettingsScreen.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/input/InputSettingsScreen.kt index 4c2a03d4..c21cd851 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/input/InputSettingsScreen.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/input/InputSettingsScreen.kt @@ -2,12 +2,11 @@ package info.cemu.cemu.settings.input import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.dropUnlessResumed -import info.cemu.cemu.R -import info.cemu.cemu.guicore.components.Button -import info.cemu.cemu.guicore.components.ScreenContent -import info.cemu.cemu.guicore.nativeenummapper.controllerTypeToStringId +import info.cemu.cemu.core.components.Button +import info.cemu.cemu.core.components.ScreenContent +import info.cemu.cemu.core.nativeenummapper.controllerTypeToString +import info.cemu.cemu.core.translation.tr import info.cemu.cemu.nativeinterface.NativeInput data class InputSettingsScreenActions( @@ -23,20 +22,20 @@ fun InputSettingsScreen(navigateBack: () -> Unit, actions: InputSettingsScreenAc } } ScreenContent( - appBarText = stringResource(R.string.input_settings), + appBarText = tr("Input settings"), navigateBack = navigateBack, ) { Button( - label = stringResource(R.string.input_overlay_settings), + label = tr("Input overlay settings"), onClick = dropUnlessResumed { actions.goToInputOverlaySettings() }, ) controllers.forEach { controllerTypePair -> val (controllerIndex, controllerEmulatedType) = controllerTypePair Button( - label = stringResource(R.string.controller_numbered, controllerIndex + 1), - description = stringResource( - R.string.emulated_controller_with_type, - stringResource(controllerTypeToStringId(controllerEmulatedType)) + label = tr("Controller {0}", controllerIndex + 1), + description = tr( + "Emulated controller: {0}", + controllerTypeToString(controllerEmulatedType) ), onClick = dropUnlessResumed { actions.goToControllerSettings(controllerIndex) }, ) diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/input/ProControllerInputs.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/input/ProControllerInputs.kt index 89342569..96f61db3 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/input/ProControllerInputs.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/input/ProControllerInputs.kt @@ -1,9 +1,7 @@ package info.cemu.cemu.settings.input import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import info.cemu.cemu.R -import info.cemu.cemu.guicore.nativeenummapper.proControllerButtonToStringId +import info.cemu.cemu.core.translation.tr import info.cemu.cemu.nativeinterface.NativeInput @Composable @@ -19,13 +17,13 @@ fun ProControllerInputs( InputItemsGroup( groupName = groupName, inputIds = inputIds, - inputIdToString = { stringResource(proControllerButtonToStringId(it)) }, + inputIdToString = { proControllerButtonToString(it) }, onInputClick = onInputClick, controlsMapping = controlsMapping, ) } InputItemsGroup( - groupName = stringResource(R.string.buttons), + groupName = tr("Buttons"), inputIds = listOf( NativeInput.PRO_BUTTON_A, NativeInput.PRO_BUTTON_B, @@ -41,7 +39,7 @@ fun ProControllerInputs( ) ) InputItemsGroup( - groupName = stringResource(R.string.d_pad), + groupName = tr("D-pad"), inputIds = listOf( NativeInput.PRO_BUTTON_UP, NativeInput.PRO_BUTTON_DOWN, @@ -50,7 +48,7 @@ fun ProControllerInputs( ) ) InputItemsGroup( - groupName = stringResource(R.string.left_axis), + groupName = tr("Left Axis"), inputIds = listOf( NativeInput.PRO_BUTTON_STICKL, NativeInput.PRO_BUTTON_STICKL_UP, @@ -60,7 +58,7 @@ fun ProControllerInputs( ) ) InputItemsGroup( - groupName = stringResource(R.string.right_axis), + groupName = tr("Right Axis"), inputIds = listOf( NativeInput.PRO_BUTTON_STICKR, NativeInput.PRO_BUTTON_STICKR_UP, @@ -70,3 +68,32 @@ fun ProControllerInputs( ) ) } + +fun proControllerButtonToString(buttonId: Int) = when (buttonId) { + NativeInput.PRO_BUTTON_A -> "A" + NativeInput.PRO_BUTTON_B -> "B" + NativeInput.PRO_BUTTON_X -> "X" + NativeInput.PRO_BUTTON_Y -> "Y" + NativeInput.PRO_BUTTON_L -> "L" + NativeInput.PRO_BUTTON_R -> "R" + NativeInput.PRO_BUTTON_ZL -> "ZL" + NativeInput.PRO_BUTTON_ZR -> "ZR" + NativeInput.PRO_BUTTON_PLUS -> "+" + NativeInput.PRO_BUTTON_MINUS -> "-" + NativeInput.PRO_BUTTON_HOME -> "home" + NativeInput.PRO_BUTTON_UP -> tr("up") + NativeInput.PRO_BUTTON_DOWN -> tr("down") + NativeInput.PRO_BUTTON_LEFT -> tr("left") + NativeInput.PRO_BUTTON_RIGHT -> tr("right") + NativeInput.PRO_BUTTON_STICKL -> tr("click") + NativeInput.PRO_BUTTON_STICKR -> tr("click") + NativeInput.PRO_BUTTON_STICKL_UP -> tr("up") + NativeInput.PRO_BUTTON_STICKL_DOWN -> tr("down") + NativeInput.PRO_BUTTON_STICKL_LEFT -> tr("left") + NativeInput.PRO_BUTTON_STICKL_RIGHT -> tr("right") + NativeInput.PRO_BUTTON_STICKR_UP -> tr("up") + NativeInput.PRO_BUTTON_STICKR_DOWN -> tr("down") + NativeInput.PRO_BUTTON_STICKR_LEFT -> tr("left") + NativeInput.PRO_BUTTON_STICKR_RIGHT -> tr("right") + else -> throw IllegalArgumentException("Invalid buttonId $buttonId for Pro controller type") +} \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/input/VPADInputs.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/input/VPADInputs.kt index 338e2a65..5a485d05 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/input/VPADInputs.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/input/VPADInputs.kt @@ -1,10 +1,8 @@ package info.cemu.cemu.settings.input import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import info.cemu.cemu.R -import info.cemu.cemu.guicore.components.Toggle -import info.cemu.cemu.guicore.nativeenummapper.vpadButtonToStringId +import info.cemu.cemu.core.components.Toggle +import info.cemu.cemu.core.translation.tr import info.cemu.cemu.nativeinterface.NativeInput @Composable @@ -21,13 +19,13 @@ fun VPADInputs( InputItemsGroup( groupName = groupName, inputIds = inputIds, - inputIdToString = { stringResource(vpadButtonToStringId(it)) }, + inputIdToString = { vpadButtonToString(it) }, onInputClick = onInputClick, controlsMapping = controlsMapping, ) } InputItemsGroup( - groupName = stringResource(R.string.buttons), + groupName = tr("Buttons"), inputIds = listOf( NativeInput.VPAD_BUTTON_A, NativeInput.VPAD_BUTTON_B, @@ -42,7 +40,7 @@ fun VPADInputs( ) ) InputItemsGroup( - groupName = stringResource(R.string.d_pad), + groupName = tr("D-pad"), inputIds = listOf( NativeInput.VPAD_BUTTON_UP, NativeInput.VPAD_BUTTON_DOWN, @@ -51,7 +49,7 @@ fun VPADInputs( ) ) InputItemsGroup( - groupName = stringResource(R.string.left_axis), + groupName = tr("Left Axis"), inputIds = listOf( NativeInput.VPAD_BUTTON_STICKL, NativeInput.VPAD_BUTTON_STICKL_UP, @@ -61,7 +59,7 @@ fun VPADInputs( ) ) InputItemsGroup( - groupName = stringResource(R.string.right_axis), + groupName = tr("Right Axis"), inputIds = listOf( NativeInput.VPAD_BUTTON_STICKR, NativeInput.VPAD_BUTTON_STICKR_UP, @@ -71,7 +69,7 @@ fun VPADInputs( ) ) InputItemsGroup( - groupName = stringResource(R.string.extra), + groupName = tr("Extra"), inputIds = listOf( NativeInput.VPAD_BUTTON_MIC, NativeInput.VPAD_BUTTON_HOME, @@ -79,9 +77,41 @@ fun VPADInputs( ) ) Toggle( - label = stringResource(R.string.toggle_screen), - description = stringResource(R.string.toggle_screen_description), + label = tr("Toggle screen"), + description = tr("Makes the \"show screen\" button toggle between the TV and gamepad screens"), initialCheckedState = { NativeInput.getVPADScreenToggle(controllerIndex) }, onCheckedChanged = { NativeInput.setVPADScreenToggle(controllerIndex, it) } ) } + + +fun vpadButtonToString(buttonId: Int) = when (buttonId) { + NativeInput.VPAD_BUTTON_A -> "A" + NativeInput.VPAD_BUTTON_B -> "B" + NativeInput.VPAD_BUTTON_X -> "X" + NativeInput.VPAD_BUTTON_Y -> "Y" + NativeInput.VPAD_BUTTON_L -> "L" + NativeInput.VPAD_BUTTON_R -> "R" + NativeInput.VPAD_BUTTON_ZL -> "ZL" + NativeInput.VPAD_BUTTON_ZR -> "ZR" + NativeInput.VPAD_BUTTON_PLUS -> "+" + NativeInput.VPAD_BUTTON_MINUS -> "-" + NativeInput.VPAD_BUTTON_UP -> tr("up") + NativeInput.VPAD_BUTTON_DOWN -> tr("down") + NativeInput.VPAD_BUTTON_LEFT -> tr("left") + NativeInput.VPAD_BUTTON_RIGHT -> tr("right") + NativeInput.VPAD_BUTTON_STICKL -> tr("click") + NativeInput.VPAD_BUTTON_STICKR -> tr("click") + NativeInput.VPAD_BUTTON_STICKL_UP -> tr("up") + NativeInput.VPAD_BUTTON_STICKL_DOWN -> tr("down") + NativeInput.VPAD_BUTTON_STICKL_LEFT -> tr("left") + NativeInput.VPAD_BUTTON_STICKL_RIGHT -> tr("right") + NativeInput.VPAD_BUTTON_STICKR_UP -> tr("up") + NativeInput.VPAD_BUTTON_STICKR_DOWN -> tr("down") + NativeInput.VPAD_BUTTON_STICKR_LEFT -> tr("left") + NativeInput.VPAD_BUTTON_STICKR_RIGHT -> tr("right") + NativeInput.VPAD_BUTTON_MIC -> tr("blow mic") + NativeInput.VPAD_BUTTON_SCREEN -> tr("show screen") + NativeInput.VPAD_BUTTON_HOME -> tr("home") + else -> throw IllegalArgumentException("Invalid buttonId $buttonId for VPAD controller type") +} diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/input/WiimoteControllerInputs.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/input/WiimoteControllerInputs.kt index d2f8c9eb..fedbba96 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/input/WiimoteControllerInputs.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/input/WiimoteControllerInputs.kt @@ -1,9 +1,7 @@ package info.cemu.cemu.settings.input import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import info.cemu.cemu.R -import info.cemu.cemu.guicore.nativeenummapper.wiimoteButtonItToStringId +import info.cemu.cemu.core.translation.tr import info.cemu.cemu.nativeinterface.NativeInput @Composable @@ -19,13 +17,13 @@ fun WiimoteControllerInputs( InputItemsGroup( groupName = groupName, inputIds = inputIds, - inputIdToString = { stringResource(wiimoteButtonItToStringId(it)) }, + inputIdToString = { wiimoteButtonItToString(it) }, onInputClick = onInputClick, controlsMapping = controlsMapping, ) } InputItemsGroup( - groupName = stringResource(R.string.buttons), + groupName = tr("Buttons"), inputIds = listOf( NativeInput.WIIMOTE_BUTTON_A, NativeInput.WIIMOTE_BUTTON_B, @@ -39,7 +37,7 @@ fun WiimoteControllerInputs( ) ) InputItemsGroup( - groupName = stringResource(R.string.nunchuck), + groupName = tr("Nunchuck"), inputIds = listOf( NativeInput.WIIMOTE_BUTTON_UP, NativeInput.WIIMOTE_BUTTON_DOWN, @@ -48,7 +46,7 @@ fun WiimoteControllerInputs( ) ) InputItemsGroup( - groupName = stringResource(R.string.right_axis), + groupName = tr("Right Axis"), inputIds = listOf( NativeInput.WIIMOTE_BUTTON_NUNCHUCK_UP, NativeInput.WIIMOTE_BUTTON_NUNCHUCK_DOWN, @@ -57,3 +55,24 @@ fun WiimoteControllerInputs( ) ) } + +fun wiimoteButtonItToString(buttonId: Int) = when (buttonId) { + NativeInput.WIIMOTE_BUTTON_A -> "A" + NativeInput.WIIMOTE_BUTTON_B -> "B" + NativeInput.WIIMOTE_BUTTON_1 -> "1" + NativeInput.WIIMOTE_BUTTON_2 -> "2" + NativeInput.WIIMOTE_BUTTON_NUNCHUCK_Z -> "Z" + NativeInput.WIIMOTE_BUTTON_NUNCHUCK_C -> "C" + NativeInput.WIIMOTE_BUTTON_PLUS -> "+" + NativeInput.WIIMOTE_BUTTON_MINUS -> "-" + NativeInput.WIIMOTE_BUTTON_UP -> tr("up") + NativeInput.WIIMOTE_BUTTON_DOWN -> tr("down") + NativeInput.WIIMOTE_BUTTON_LEFT -> tr("left") + NativeInput.WIIMOTE_BUTTON_RIGHT -> tr("right") + NativeInput.WIIMOTE_BUTTON_NUNCHUCK_UP -> tr("up") + NativeInput.WIIMOTE_BUTTON_NUNCHUCK_DOWN -> tr("down") + NativeInput.WIIMOTE_BUTTON_NUNCHUCK_LEFT -> tr("left") + NativeInput.WIIMOTE_BUTTON_NUNCHUCK_RIGHT -> tr("right") + NativeInput.WIIMOTE_BUTTON_HOME -> tr("home") + else -> throw IllegalArgumentException("Invalid buttonId $buttonId for Wiimote controller type") +} diff --git a/src/android/app/src/main/java/info/cemu/cemu/settings/inputoverlay/InputOverlaySettingsScreen.kt b/src/android/app/src/main/java/info/cemu/cemu/settings/inputoverlay/InputOverlaySettingsScreen.kt index 6d5fabf9..be2ef960 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/settings/inputoverlay/InputOverlaySettingsScreen.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/settings/inputoverlay/InputOverlaySettingsScreen.kt @@ -1,13 +1,12 @@ package info.cemu.cemu.settings.inputoverlay import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource import androidx.lifecycle.viewmodel.compose.viewModel -import info.cemu.cemu.R -import info.cemu.cemu.guicore.components.ScreenContent -import info.cemu.cemu.guicore.components.SingleSelection -import info.cemu.cemu.guicore.components.Slider -import info.cemu.cemu.guicore.components.Toggle +import info.cemu.cemu.core.components.ScreenContent +import info.cemu.cemu.core.components.SingleSelection +import info.cemu.cemu.core.components.Slider +import info.cemu.cemu.core.components.Toggle +import info.cemu.cemu.core.translation.tr import info.cemu.cemu.nativeinterface.NativeInput private val ControllerIndexChoices = (0.. Unit) { var overlayPosition by rememberSaveable { mutableIntStateOf(NativeSettings.getOverlayPosition()) } var notificationsPosition by rememberSaveable { mutableIntStateOf(NativeSettings.getNotificationsPosition()) } ScreenContent( - appBarText = stringResource(R.string.overlay_settings), + appBarText = tr("Overlay settings"), navigateBack = navigateBack, ) { - Header(stringResource(R.string.overlay)) + Header(tr("Overlay")) SingleSelection( - label = stringResource(R.string.overlay_position), + label = tr("Position"), choice = overlayPosition, choices = OverlayPositionChoices, - choiceToString = { stringResource(overlayScreenPositionToStringId(it)) }, + choiceToString = { overlayScreenPositionToString(it) }, onChoiceChanged = { overlayPosition = it NativeSettings.setOverlayPosition(it) @@ -50,12 +46,12 @@ fun OverlaySettingsScreen(navigateBack: () -> Unit) { ) if (overlayPosition != NativeSettings.OVERLAY_SCREEN_POSITION_DISABLED) OverlaySettings() - Header(stringResource(R.string.notifications)) + Header(tr("Notifications")) SingleSelection( - label = stringResource(R.string.overlay_position), + label = tr("Position"), choice = notificationsPosition, choices = OverlayPositionChoices, - choiceToString = { stringResource(overlayScreenPositionToStringId(it)) }, + choiceToString = { overlayScreenPositionToString(it) }, onChoiceChanged = { notificationsPosition = it NativeSettings.setNotificationsPosition(it) @@ -69,7 +65,7 @@ fun OverlaySettingsScreen(navigateBack: () -> Unit) { @Composable private fun OverlaySettings() { Slider( - label = stringResource(R.string.overlay_text_scale), + label = tr("Scale"), initialValue = NativeSettings::getOverlayTextScalePercentage, steps = OVERLAY_TEXT_SCALE_STEPS, valueFrom = NativeSettings.OVERLAY_TEXT_SCALE_MIN, @@ -78,32 +74,32 @@ private fun OverlaySettings() { labelFormatter = { "${it}%" } ) Toggle( - label = stringResource(R.string.fps), - description = stringResource(R.string.fps_overlay_description), + label = tr("FPS"), + description = tr("The number of frames per second. Average over last 5 seconds"), initialCheckedState = NativeSettings::isOverlayFPSEnabled, onCheckedChanged = NativeSettings::setOverlayFPSEnabled, ) Toggle( - label = stringResource(R.string.draw_calls_per_frame), - description = stringResource(R.string.draw_calls_per_frame_overlay_description), + label = tr("Draw calls per frame"), + description = tr("The number of draw calls per frame. Average over last 5 seconds"), initialCheckedState = NativeSettings::isOverlayDrawCallsPerFrameEnabled, onCheckedChanged = NativeSettings::setOverlayDrawCallsPerFrameEnabled, ) Toggle( - label = stringResource(R.string.cpu_usage), - description = stringResource(R.string.cpu_usage_overlay_description), + label = tr("CPU usage"), + description = tr("CPU usage of Cemu in percent"), initialCheckedState = NativeSettings::isOverlayCPUUsageEnabled, onCheckedChanged = NativeSettings::setOverlayCPUUsageEnabled, ) Toggle( - label = stringResource(R.string.ram_usage), - description = stringResource(R.string.ram_usage_overlay_description), + label = tr("RAM usage"), + description = tr("Cemu RAM usage in MB"), initialCheckedState = NativeSettings::isOverlayRAMUsageEnabled, onCheckedChanged = NativeSettings::setOverlayRAMUsageEnabled, ) Toggle( - label = stringResource(R.string.debug), - description = stringResource(R.string.debug_overlay_description), + label = tr("Debug"), + description = tr("Displays internal debug information (Vulkan only)"), initialCheckedState = NativeSettings::isOverlayDebugEnabled, onCheckedChanged = NativeSettings::setOverlayDebugEnabled, ) @@ -112,7 +108,7 @@ private fun OverlaySettings() { @Composable private fun NotificationSettings() { Slider( - label = stringResource(R.string.notifications_text_scale), + label = tr("Scale"), initialValue = NativeSettings::getNotificationsTextScalePercentage, steps = OVERLAY_TEXT_SCALE_STEPS, valueFrom = NativeSettings.OVERLAY_TEXT_SCALE_MIN, @@ -121,21 +117,32 @@ private fun NotificationSettings() { labelFormatter = { "$it%" } ) Toggle( - label = stringResource(R.string.controller_profiles), - description = stringResource(R.string.controller_profiles_notification_description), + label = tr("Controller profiles"), + description = tr("Displays the active controller profile when starting a game"), initialCheckedState = NativeSettings::isNotificationControllerProfilesEnabled, onCheckedChanged = NativeSettings::setNotificationControllerProfilesEnabled, ) Toggle( - label = stringResource(R.string.shader_compiler), - description = stringResource(R.string.shader_compiler_notification_description), + label = tr("Shader compiler"), + description = tr("Shows a notification after shaders have been compiled"), initialCheckedState = NativeSettings::isNotificationShaderCompilerEnabled, onCheckedChanged = NativeSettings::setNotificationShaderCompilerEnabled, ) Toggle( - label = stringResource(R.string.friend_list), - description = stringResource(R.string.friend_list_notification_description), + label = tr("Friend list"), + description = tr("Shows friend list related data if online"), initialCheckedState = NativeSettings::isNotificationFriendListEnabled, onCheckedChanged = NativeSettings::setNotificationFriendListEnabled, ) +} + +private fun overlayScreenPositionToString(overlayScreenPosition: Int) = when (overlayScreenPosition) { + NativeSettings.OVERLAY_SCREEN_POSITION_DISABLED -> tr("Disabled") + NativeSettings.OVERLAY_SCREEN_POSITION_TOP_LEFT -> tr("Top left") + NativeSettings.OVERLAY_SCREEN_POSITION_TOP_CENTER -> tr("Top center") + NativeSettings.OVERLAY_SCREEN_POSITION_TOP_RIGHT -> tr("Top right") + NativeSettings.OVERLAY_SCREEN_POSITION_BOTTOM_LEFT -> tr("Bottom left") + NativeSettings.OVERLAY_SCREEN_POSITION_BOTTOM_CENTER -> tr("Bottom center") + NativeSettings.OVERLAY_SCREEN_POSITION_BOTTOM_RIGHT -> tr("Bottom right") + else -> throw IllegalArgumentException("Invalid overlay position: $overlayScreenPosition") } \ No newline at end of file diff --git a/src/android/app/src/main/java/info/cemu/cemu/titlemanager/TitleManagerScreen.kt b/src/android/app/src/main/java/info/cemu/cemu/titlemanager/TitleManagerScreen.kt index 027f79d1..4e809d98 100644 --- a/src/android/app/src/main/java/info/cemu/cemu/titlemanager/TitleManagerScreen.kt +++ b/src/android/app/src/main/java/info/cemu/cemu/titlemanager/TitleManagerScreen.kt @@ -8,7 +8,6 @@ package info.cemu.cemu.titlemanager import android.content.Intent import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.annotation.StringRes import androidx.compose.animation.animateContentSize import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.layout.Arrangement @@ -22,7 +21,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.TextField import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.filled.Check @@ -45,6 +43,7 @@ import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TextButton +import androidx.compose.material3.TextField import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -59,7 +58,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -67,11 +65,13 @@ import androidx.compose.ui.unit.sp import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.viewmodel.compose.viewModel import info.cemu.cemu.R -import info.cemu.cemu.guicore.components.ScreenContentLazy -import info.cemu.cemu.guicore.format.formatBytes -import info.cemu.cemu.guicore.nativeenummapper.regionToStringId +import info.cemu.cemu.core.components.ScreenContentLazy +import info.cemu.cemu.core.format.formatBytes +import info.cemu.cemu.core.nativeenummapper.regionToString +import info.cemu.cemu.core.translation.tr import info.cemu.cemu.nativeinterface.NativeGameTitles import kotlinx.coroutines.launch +import java.text.MessageFormat @Composable fun TitleManagerScreen( @@ -91,10 +91,10 @@ fun TitleManagerScreen( val currentCompressProgress by titleListViewModel.compressProgress.collectAsState() val titleToBeDeleted by titleListViewModel.titleToBeDeleted.collectAsState() - fun showNotificationMessage(@StringRes stringId: Int) { + fun showNotificationMessage(text: String) { coroutineScope.launch { snackbarHostState.currentSnackbarData?.dismiss() - snackbarHostState.showSnackbar(context.getString(stringId)) + snackbarHostState.showSnackbar(text) } } @@ -103,11 +103,11 @@ fun TitleManagerScreen( context = context, titleInstallCallbacks = object : TitleInstallCallbacks { override fun onInstallFinished() { - showNotificationMessage(R.string.install_title_finished) + showNotificationMessage(tr("Finished installing")) } override fun onError() { - showNotificationMessage(R.string.install_title_error) + showNotificationMessage(tr("Error installing")) } } ) @@ -125,7 +125,7 @@ fun TitleManagerScreen( titleListViewModel.queueTitleToInstall( titlePath = documentFile.uri, onInvalidTitle = { - showNotificationMessage(R.string.install_title_invalid_title) + showNotificationMessage(tr("Invalid title")) }) } @@ -136,38 +136,38 @@ fun TitleManagerScreen( context = context, uri = uri, onFinished = { - showNotificationMessage(R.string.wua_convert_finished) + showNotificationMessage(tr("Finished converting")) }, onError = { - showNotificationMessage(R.string.wua_convert_error) + showNotificationMessage(tr("Error while converting")) } ) } ScreenContentLazy( snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, - appBarText = stringResource(R.string.title_manager_screen_label), + appBarText = tr("Title manager"), navigateBack = navigateBack, actions = { IconButton(onClick = { titleListViewModel.refresh() - showNotificationMessage(R.string.titles_refreshing_notification) + showNotificationMessage(tr("Refreshing titles")) }) { Icon( imageVector = Icons.Filled.Refresh, - contentDescription = stringResource(R.string.title_manager_refresh_titles_description), + contentDescription = null ) } IconButton(onClick = { showFilterSheet = true }) { Icon( painter = painterResource(R.drawable.ic_filter), - contentDescription = stringResource(R.string.title_manager_filter_titles_description), + contentDescription = null ) } IconButton(onClick = { installTitleLauncher.launch(null) }) { Icon( painter = painterResource(R.drawable.ic_add), - contentDescription = stringResource(R.string.title_manager_install_title_description) + contentDescription = null ) } } @@ -181,11 +181,11 @@ fun TitleManagerScreen( context = context, deleteCallbacks = object : TitleDeleteCallbacks { override fun onDeleteFinished() { - showNotificationMessage(R.string.title_entry_delete_notification) + showNotificationMessage(tr("Deleted title entry")) } override fun onError() { - showNotificationMessage(R.string.title_entry_failed_delete_notification) + showNotificationMessage(tr("Failed to delete title entry")) } }) }, @@ -245,7 +245,7 @@ private fun TitleCompressProgressDialog(bytesWritten: Long?, onCancel: () -> Uni var showCancelConfirmDialog by remember { mutableStateOf(false) } AlertDialog( - title = { Text(stringResource(R.string.wua_convert_title)) }, + title = { Text(tr("Compressing title")) }, text = { Column( modifier = Modifier.padding(8.dp), @@ -253,12 +253,7 @@ private fun TitleCompressProgressDialog(bytesWritten: Long?, onCancel: () -> Uni ) { LinearProgressIndicator() if (bytesWritten != null) - Text( - stringResource( - R.string.wua_convert_current_progress, - bytesWritten.formatBytes() - ) - ) + Text(tr("Current progress: {0}", bytesWritten.formatBytes())) } }, onDismissRequest = {}, @@ -266,24 +261,24 @@ private fun TitleCompressProgressDialog(bytesWritten: Long?, onCancel: () -> Uni dismissButton = { TextButton( onClick = { showCancelConfirmDialog = true }, - content = { Text(stringResource(R.string.cancel)) }, + content = { Text(tr("Cancel")) }, ) } ) if (showCancelConfirmDialog) AlertDialog( - title = { Text(stringResource(R.string.wua_convert_cancel_title)) }, - text = { Text(stringResource(R.string.wua_convert_cancel_text)) }, + title = { Text(tr("Cancel compressing title")) }, + text = { Text(tr("Do you really want to cancel compressing the title?")) }, onDismissRequest = { showCancelConfirmDialog = false }, confirmButton = { TextButton(onClick = onCancel) { - Text(stringResource(R.string.yes)) + Text(tr("Yes")) } }, dismissButton = { TextButton(onClick = { showCancelConfirmDialog = false }) { - Text(stringResource(R.string.no)) + Text(tr("No")) } } ) @@ -306,11 +301,11 @@ private fun TitleCompressConfirmationDialog( Text( modifier = Modifier.padding(bottom = 4.dp), fontSize = 14.sp, - text = entryPrintPath ?: stringResource(R.string.wua_convert_title_not_installed) + text = entryPrintPath ?: tr("Not installed") ) } AlertDialog( - title = { Text(stringResource(R.string.wua_convert_confirmation_title)) }, + title = { Text(tr("Confirmation")) }, text = { Column( modifier = Modifier @@ -321,28 +316,28 @@ private fun TitleCompressConfirmationDialog( modifier = Modifier.padding(vertical = 8.dp), fontSize = 16.sp, fontWeight = FontWeight.Bold, - text = stringResource(R.string.wua_convert_confirmation_description), + text = tr("The following content will be converted to a compressed Wii U archive file (.wua)"), ) EntryInfo( - stringResource(R.string.wua_convert_base_game_title_entry), + tr("Base game"), compressTitleInfo.basePrintPath ) EntryInfo( - stringResource(R.string.wua_convert_update_title_entry), + tr("Update"), compressTitleInfo.updatePrintPath ) EntryInfo( - stringResource(R.string.wua_convert_dlc_title_entry), + tr("DLC"), compressTitleInfo.aocPrintPath ) } }, onDismissRequest = onDismiss, confirmButton = { - TextButton(onClick = onConfirm) { Text(stringResource(R.string.ok)) } + TextButton(onClick = onConfirm) { Text(tr("OK")) } }, dismissButton = { - TextButton(onClick = onDismiss) { Text(stringResource(R.string.cancel)) } + TextButton(onClick = onDismiss) { Text(tr("Cancel")) } } ) } @@ -355,7 +350,7 @@ private fun TitleInstallProgressDialog( var showCancelConfirmDialog by remember { mutableStateOf(false) } AlertDialog( - title = { Text(stringResource(R.string.install_title_progress_title)) }, + title = { Text(tr("Installing title")) }, text = { val progressModifiers = Modifier .fillMaxWidth() @@ -365,7 +360,7 @@ private fun TitleInstallProgressDialog( LinearProgressIndicator(modifier = progressModifiers) Text( modifier = Modifier.padding(8.dp), - text = stringResource(R.string.install_title_parsing_content) + text = tr("Parsing title content…") ) } else { val (bytesWritten, maxBytes) = progress @@ -386,26 +381,26 @@ private fun TitleInstallProgressDialog( confirmButton = {}, dismissButton = { TextButton(onClick = { showCancelConfirmDialog = true }) { - Text(stringResource(R.string.cancel)) + Text(tr("Cancel")) } } ) if (showCancelConfirmDialog) AlertDialog( - title = { Text(stringResource(R.string.install_title_cancel_title)) }, + title = { Text(tr("Cancel installing title")) }, text = { - Text(stringResource(R.string.install_title_cancel_text)) + Text(tr("Do you really want to cancel the installation process?\n\nCanceling the process will delete the applied files.")) }, onDismissRequest = { showCancelConfirmDialog = false }, confirmButton = { TextButton(onClick = onCancel) { - Text(stringResource(R.string.yes)) + Text(tr("Yes")) } }, dismissButton = { TextButton(onClick = { showCancelConfirmDialog = false }) { - Text(stringResource(R.string.no)) + Text(tr("No")) } } ) @@ -418,15 +413,18 @@ private fun TitleInstallConfirmDialog( onDismissRequest: () -> Unit, onConfirm: () -> Unit, ) { + val errorMessage = when (error) { - is NativeGameTitles.TitleExistsError.DifferentType -> stringResource( - R.string.title_exists_error_different_type, + is NativeGameTitles.TitleExistsError.DifferentType -> tr( + """It seems that there is already a title installed at the target location but it has a different type. +Currently installed: '{0}' Installing: '{1}' +Do you still want to continue with the installation? It will replace the currently installed title.""", error.oldType, error.toInstallType ) - NativeGameTitles.TitleExistsError.NewVersion -> stringResource(R.string.title_exists_error_new_version) - NativeGameTitles.TitleExistsError.SameVersion -> stringResource(R.string.title_exists_error_same_version) + NativeGameTitles.TitleExistsError.NewVersion -> tr("It seems that a newer version is already installed, do you still want to install the older version?") + NativeGameTitles.TitleExistsError.SameVersion -> tr("It seems that the selected title is already installed, do you want to reinstall it?") NativeGameTitles.TitleExistsError.None -> { onConfirm() @@ -441,7 +439,7 @@ private fun TitleInstallConfirmDialog( contentDescription = null ) }, - title = { Text(stringResource(R.string.warning)) }, + title = { Text(tr("Warning")) }, text = { Text( text = errorMessage, @@ -450,10 +448,10 @@ private fun TitleInstallConfirmDialog( }, onDismissRequest = onDismissRequest, confirmButton = { - TextButton(onClick = onConfirm) { Text(stringResource(R.string.yes)) } + TextButton(onClick = onConfirm) { Text(tr("Yes")) } }, dismissButton = { - TextButton(onClick = onDismissRequest) { Text(stringResource(R.string.no)) } + TextButton(onClick = onDismissRequest) { Text(tr("No")) } } ) } @@ -477,26 +475,26 @@ private fun TitleFilterBottomSheet( singleLine = true, value = filter.query, onValueChange = titleListViewModel::setFilterQuery, - label = { Text(stringResource(R.string.title_manager_search_label)) } + label = { Text(tr("Search titles")) } ) FilterRow( - filterRowLabel = stringResource(R.string.title_manager_filter_types_label), + filterRowLabel = tr("Types"), filterValues = EntryType.entries.map { (it to (it in filter.types)) }, - valueToLabel = { stringResource(entryTypeToStringId(it)) }, + valueToLabel = { entryTypeToString(it) }, onFilterAdded = titleListViewModel.typesFilter::add, onFilterRemoved = titleListViewModel.typesFilter::remove, ) FilterRow( - filterRowLabel = stringResource(R.string.title_manager_filter_formats_label), + filterRowLabel = tr("Formats"), filterValues = EntryFormat.entries.map { (it to (it in filter.formats)) }, - valueToLabel = { stringResource(formatToStringId(it)) }, + valueToLabel = { formatToString(it) }, onFilterAdded = titleListViewModel.formatsFilter::add, onFilterRemoved = titleListViewModel.formatsFilter::remove, ) FilterRow( - filterRowLabel = stringResource(R.string.title_manager_filter_locations_label), + filterRowLabel = tr("Locations"), filterValues = EntryPath.entries.map { (it to (it in filter.paths)) }, - valueToLabel = { stringResource(pathToStringId(it)) }, + valueToLabel = { pathToString(it) }, onFilterAdded = titleListViewModel.pathsFilter::add, onFilterRemoved = titleListViewModel.pathsFilter::remove, ) @@ -526,7 +524,7 @@ private fun FilterRow( Icon( modifier = Modifier.rotate(if (showOptions) 180f else 0f), imageVector = Icons.Filled.ArrowDropDown, - contentDescription = null, + contentDescription = null ) } if (showOptions) @@ -609,7 +607,7 @@ private fun TitleEntryListItem( Icon( modifier = Modifier.rotate(if (showTitleInfo) 180f else 0f), imageVector = Icons.Filled.ArrowDropDown, - contentDescription = stringResource(R.string.title_manager_show_info_description) + contentDescription = null ) } } @@ -649,16 +647,16 @@ private fun TitleDropDownMenu( IconButton(onClick = { expandMenu = true }) { Icon( imageVector = Icons.Filled.MoreVert, - contentDescription = stringResource(R.string.title_dropdown_menu_description) + contentDescription = null ) } DropdownMenu( expanded = expandMenu, onDismissRequest = { expandMenu = false }) { - DropdownMenuItem(stringResource(R.string.delete), onDeleteClicked) + DropdownMenuItem(tr("Delete"), onDeleteClicked) if (titleEntry.type != EntryType.Save && titleEntry.format != EntryFormat.WUA) DropdownMenuItem( - stringResource(R.string.wua_convert_action_label), + tr("Convert to WUA"), onCompressClicked ) } @@ -675,20 +673,14 @@ private fun DeleteTitleProgressDialog(titleEntry: TitleEntry) { ) }, title = { - Text(stringResource(R.string.delete_title_progress_dialog_title)) + Text(tr("Deleting")) }, text = { - val titleEntryInfo = stringResource( - R.string.title_entry_delete_info, - titleEntry.name, - stringResource(regionToStringId(titleEntry.region)), - stringResource(entryTypeToStringId(titleEntry.type)) - ) Column( modifier = Modifier.padding(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - Text(stringResource(R.string.delete_title_title_info, titleEntryInfo)) + Text(tr("Deleting: {0}", getTitleEntryInfo(titleEntry))) LinearProgressIndicator( modifier = Modifier .fillMaxWidth() @@ -715,20 +707,14 @@ private fun DeleteTitleConfirmationDialog( contentDescription = null ) }, - title = { Text(stringResource(R.string.warning)) }, + title = { Text(tr("Warning")) }, text = { - val titleEntryInfo = stringResource( - R.string.title_entry_delete_info, - titleEntry.name, - stringResource(regionToStringId(titleEntry.region)), - stringResource(entryTypeToStringId(titleEntry.type)) - ) Column( modifier = Modifier.padding(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - Text(stringResource(R.string.title_entry_delete_confirmation)) - Text(titleEntryInfo) + Text(tr("Are you really sure you want to delete the following entry?")) + Text(getTitleEntryInfo(titleEntry)) } }, onDismissRequest = onDismissRequest, @@ -738,15 +724,22 @@ private fun DeleteTitleConfirmationDialog( onDismissRequest() onConfirmDelete() }, - content = { Text(stringResource(R.string.yes)) }, + content = { Text(tr("Yes")) }, ) }, dismissButton = { - TextButton(onClick = onDismissRequest) { Text(stringResource(R.string.no)) } + TextButton(onClick = onDismissRequest) { Text(tr("No")) } } ) } +private fun getTitleEntryInfo(titleEntry: TitleEntry) = MessageFormat.format( + "[{0}] [{1}] [{2}]", + titleEntry.name, + regionToString(titleEntry.region), + entryTypeToString(titleEntry.type) +) + @Composable private fun TitleEntryIcon(entryType: EntryType, modifier: Modifier = Modifier) { val iconId = when (entryType) { @@ -759,7 +752,7 @@ private fun TitleEntryIcon(entryType: EntryType, modifier: Modifier = Modifier) Icon( modifier = modifier, painter = painterResource(iconId), - contentDescription = stringResource(entryTypeToStringId(entryType)) + contentDescription = null ) } @@ -771,29 +764,29 @@ private fun TitleEntryData(titleEntry: TitleEntry) { .padding(8.dp) ) { TitleEntryInfo( - name = stringResource(R.string.title_info_id_label), + name = tr("Title ID"), value = formatTitleId(titleEntry.titleId) ) TitleEntryInfo( - name = stringResource(R.string.title_info_type_label), - value = stringResource(entryTypeToStringId(titleEntry.type)), + name = tr("Type"), + value = entryTypeToString(titleEntry.type), ) TitleEntryInfo( - name = stringResource(R.string.title_info_version_label), + name = tr("Version"), value = titleEntry.version.toString(), ) TitleEntryInfo( - name = stringResource(R.string.title_info_region_label), - value = stringResource(regionToStringId(titleEntry.region)), + name = tr("Region"), + value = regionToString(titleEntry.region), ) TitleEntryInfo( - name = stringResource(R.string.title_info_format_label), - value = stringResource(formatToStringId(titleEntry.format)), + name = tr("Format"), + value = formatToString(titleEntry.format), ) TitleEntryInfo( - name = stringResource(R.string.title_info_location_label), - value = if (titleEntry.isInMLC) stringResource(R.string.location_mlc) - else stringResource(R.string.location_game_paths) + name = tr("Location"), + value = if (titleEntry.isInMLC) tr("MLC") + else tr("Game paths") ) } } @@ -822,29 +815,26 @@ private fun TitleEntryInfo(name: String, value: String) { ) } -@StringRes -private fun formatToStringId(entryFormat: EntryFormat) = when (entryFormat) { - EntryFormat.Folder -> R.string.entry_format_folder - EntryFormat.WUD -> R.string.entry_format_wud - EntryFormat.NUS -> R.string.entry_format_nus - EntryFormat.WUA -> R.string.entry_format_wua - EntryFormat.WUHB -> R.string.entry_format_wuhb - EntryFormat.SaveFolder -> R.string.entry_format_save_folder +private fun formatToString(entryFormat: EntryFormat) = when (entryFormat) { + EntryFormat.Folder -> tr("Folder") + EntryFormat.WUD -> tr("WUD") + EntryFormat.NUS -> tr("NUS") + EntryFormat.WUA -> tr("WUA") + EntryFormat.WUHB -> tr("WUHB") + EntryFormat.SaveFolder -> tr("Save folder") } -@StringRes -private fun pathToStringId(entryPath: EntryPath) = when (entryPath) { - EntryPath.MLC -> R.string.location_mlc - EntryPath.GamePaths -> R.string.location_game_paths +private fun pathToString(entryPath: EntryPath) = when (entryPath) { + EntryPath.MLC -> tr("MLC") + EntryPath.GamePaths -> tr("Game paths") } -@StringRes -private fun entryTypeToStringId(entryType: EntryType) = when (entryType) { - EntryType.Base -> R.string.entry_type_base - EntryType.Update -> R.string.entry_type_update - EntryType.Dlc -> R.string.entry_type_dlc - EntryType.Save -> R.string.entry_type_save - EntryType.System -> R.string.entry_type_system +private fun entryTypeToString(entryType: EntryType) = when (entryType) { + EntryType.Base -> tr("Base") + EntryType.Update -> tr("Update") + EntryType.Dlc -> tr("DLC") + EntryType.Save -> tr("Save") + EntryType.System -> tr("System") } private fun formatTitleId(titleId: Long) = diff --git a/src/android/app/src/main/java/info/cemu/cemu/utils/PoFile.kt b/src/android/app/src/main/java/info/cemu/cemu/utils/PoFile.kt new file mode 100644 index 00000000..a38e7bfe --- /dev/null +++ b/src/android/app/src/main/java/info/cemu/cemu/utils/PoFile.kt @@ -0,0 +1,63 @@ +package info.cemu.cemu.utils + +import java.io.InputStream + +private enum class PoKey { MSG_ID, MSG_STR, NONE } + +fun parsePoFile(source: InputStream): Map { + val reader = source.bufferedReader() + var key = PoKey.NONE + var msgId = "" + var msg = "" + val strings = mutableMapOf() + + while (true) { + val line = reader.readLine() ?: break + + when { + line.isBlank() -> { + if (msgId.isEmpty() || msg.isEmpty()) { + continue + } + + strings[msgId] = msg + + key = PoKey.NONE + msgId = "" + msg = "" + } + + line.startsWith("msgid ") -> { + key = PoKey.MSG_ID + msgId = line.substringAfter("msgid ").unescape() + } + + line.startsWith("msgstr ") -> { + key = PoKey.MSG_STR + msg = line.substringAfter("msgstr ").unescape() + } + + // Ignore entry if it has plural forms or context, the core code doesn't use them. + line.startsWith("msgid_plural ") || line.startsWith("msgctxt ") -> { + key = PoKey.NONE + msg = "" + msgId = "" + } + + else -> when (key) { + PoKey.MSG_ID -> msgId += line.unescape() + PoKey.MSG_STR -> msg += line.unescape() + PoKey.NONE -> {} + } + } + } + + return strings +} + +private fun String.unescape(): String { + return trim() + .trim('"') + .replace("\\\"", "\"") + .replace("\\n", "\n") +} \ No newline at end of file diff --git a/src/android/app/src/main/res/layout/activity_emulation.xml b/src/android/app/src/main/res/layout/activity_emulation.xml index cd247697..c0aa9473 100644 --- a/src/android/app/src/main/res/layout/activity_emulation.xml +++ b/src/android/app/src/main/res/layout/activity_emulation.xml @@ -1,5 +1,5 @@ -