Add gettext translations

This commit is contained in:
SSimco
2025-07-20 16:38:03 +03:00
parent 8c83d88d55
commit 1f45589cea
64 changed files with 1024 additions and 1155 deletions
-1
View File
@@ -13,4 +13,3 @@
.externalNativeBuild
.cxx
local.properties
app/src/main/assets/hash.txt
+4 -1
View File
@@ -1 +1,4 @@
/build
/build
assets/hash.txt
*.po
*.pot
+12 -14
View File
@@ -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<ComputeCemuDataFilesHashTask>("computeCemuDataFilesHash") {
cemuDataFolder = cemuDataFilesFolder
}
val computeCemuDataFilesHashTask =
tasks.register<ComputeCemuDataFilesHashTask>("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)
}
@@ -1,3 +1,3 @@
<resources>
<string name="app_name">Cemu debug</string>
<string name="app_name" translatable="false">Cemu debug</string>
</resources>
@@ -12,6 +12,7 @@ add_library(CemuAndroid SHARED
NativeGraphicPacks.cpp
NativeInput.cpp
NativeLib.cpp
NativeLocalization.cpp
NativeLogging.cpp
NativeSettings.cpp
NativeSwkbd.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<VulkanRenderer>();
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<ANativeWindow*>(windowHandleInfo.surface));
+2 -1
View File
@@ -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};
@@ -0,0 +1,42 @@
#include "JNIUtils.h"
namespace NativeLocalization
{
std::unordered_map<std::string_view, std::string> 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<jobjectArray>(env->CallObjectMethod(keySet, toArrayMethodId));
jint size = env->GetArrayLength(keyArray);
for (jint i = 0; i < size; i++)
{
auto keyJava = static_cast<jstring>(env->GetObjectArrayElement(keyArray, i));
std::string key = JNIUtils::toString(env, keyJava);
auto translationJava = static_cast<jstring>(env->CallObjectMethod(translations, getMethodId, keyJava));
std::string translation = JNIUtils::toString(env, translationJava);
NativeLocalization::g_messages[key] = translation;
}
SetTranslationCallback(NativeLocalization::Translate);
}
@@ -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) } }
}
}
@@ -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()
}
}
@@ -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(
@@ -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) {
@@ -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
@@ -1,4 +1,4 @@
package info.cemu.cemu.guicore.components
package info.cemu.cemu.core.components
import android.content.Context
import android.util.AttributeSet
@@ -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,
@@ -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
@@ -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
@@ -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
)
}
},
@@ -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 <T> SelectDialog(
.padding(8.dp)
.align(Alignment.End),
) {
Text(stringResource(R.string.cancel))
Text(tr("Cancel"))
}
}
}
@@ -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

Some files were not shown because too many files have changed in this diff Show More