Skip to content

Conversation

ISNIT0
Copy link
Member

@ISNIT0 ISNIT0 commented Jan 14, 2025

  • Implement a custom string internationalisation, because I hate the Android one (perhaps not a good idea, but oh well)
  • Create a Gradle task that automatically extracts i18n strings from the code, and adds them to the strings.xml file
  • Create a Gradle task that automatically translates english strings to french and spanish (for now) using an LLM
  • Add a CI job for this

How to make a string auto-internationalise:

val greeting = "Hello" // normal

val greeting = "Hello".i18n() // magic

// If device locale is french
print(greeting.resolve()) // prints: bonjour

val subString = "Hello, {name}"
print(greeting.resolve(mapOf("name" to "World!"))) // prints: Hello, World!

@ISNIT0 ISNIT0 force-pushed the ISNIT0/i18n branch 4 times, most recently from a09386a to 496daad Compare January 14, 2025 22:47
viewModel: StoneCameraViewModel,
previousMode: String,
nextMode: String
previousMode: TranslatableString,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it possible to keep original String type?


override val modeLabel
get() = "video"
get() = @Translatable "video".i18n()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks a bit too "bulky"
is there any options to use only either annotation or i18n() method?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assuming that this approach is quite bulky, there may be cases where someone simply hard codes a string without using @Translatable or calling .i18n(). This would mean that string wouldn't be considered for translation and the app would ship with hardcoded strings. How might we check for this happening to reduce the likelihood the app is shipped with hardcoded strings rather than translatable/translated strings?

val controlModeGet = viewModel.getSetting<TranslatableString>("volumeControlMode")

if (isZoomMode || controlModeGet == "Zoom") {
if (isZoomMode || controlModeGet?.resolve() == "Zoom") {
Copy link
Collaborator

@vckttr vckttr Jan 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.resolve() == "Zoom"
will it still work in different langs?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question :) @ISNIT0 ?

}

private fun sanitizeKey(key: String): String {
val sanitized = key.replace(Regex("[^a-zA-Z0-9]"), "_")
Copy link
Collaborator

@vckttr vckttr Jan 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a collision proof solution
a sanitized key for "!Test" will be equal to the key for "?Test"

Copy link
Collaborator

@vckttr vckttr Jan 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

potentially different parts of the app could have same strings in english but with different translations to other languages
so i would suggest to introduce a module prefix for keys

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, for example some strings in English are written the same but have very different meanings so the same translation might not suit each use of what is lexically a common string. I'll provide a small example: "row" as in a row in a spreadsheet is not the same word as "row" as in two people had a row, or to row a boat.

Text(text = modeLabel.uppercase(),
color = if (modeLabel == selectedMode) Color(0xFFFFCC00) else Color.White,
Text(text = modeLabel.resolve().uppercase(),
color = if (modeLabel.resolve() == selectedMode.resolve()) Color(0xFFFFCC00) else Color.White,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as i've got you can leave it as it was modeLabel == selectedMode

sourceDir.walkTopDown()
.filter { it.isFile && it.extension in listOf("kt", "java") }
.forEach { file ->
val regex = Regex("@Translatable\\s*\\\"(.*?)\\\"")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cuts the string if you put \" in the middle

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like having automated test cases that check the translation will behave as desired even for multi-line strings, etc. might be useful at checking whether these sorts of regex mistakes are dealt with?

/**
* Sanitizes a string to make it a valid XML key.
*/
private fun sanitizeKey(key: String): String {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

duplicated logic with

Copy link
Collaborator

@julianharty julianharty left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall I like this innovative approach to bootstrap translations of textual strings and it's a really nice idea. For production use I suspect the project may need suitably competent humans-in-the-loop for the translations, at least for a while.

It's probably worth another round of code reviews if you decide to refine the code based on my and other reviewer's comments. Happy to do so.

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

TranslationManager.loadTranslationsForLocale(applicationContext)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this guarantee to return a set of elements (translations) regardless of the application's context? In other words are there ever going to be cases where it wouldn't return a) something the program can run with, b) something the human user can understand?


TranslationManager.loadTranslationsForLocale(applicationContext)

if (!isChromeOS()) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dumb question and not directly part of this PR... Are we targeting ChromeOS?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the removal of android.util.log what's happening about logging?

workList.awaitAll()
Log.d("Analyzer", "All plugins completed successfully")
} catch (e: Exception) {
Log.e("Analyzer", "Error in one or more plugins", e)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again a question not directly part of this PR, but triggered when reading the code deleted above, what happens with the worklist.awaitAll() if an exception occurs in one or more of the plugins that are running in parallel?

},
renderLocation = SettingLocation.TOP,
label = "Aspect Ratio"
label = @Translatable "Aspect Ratio".i18n()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this call respond if a match isn't found for the string being translated?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's the potential for translated strings to require significant changes to screen real-estate for various reasons such as phrasing in that locale, to mistranslations, to nefarious attacks. Probably worth adding some forms of sanity checking of translated strings before they're incorporated into an app.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How long is the translation expected to take? and is the translation process intended to complete before the app is compiled? Oh, and are the translations something that are expected to be added into a git repo or simply processed on the build machine (which may be an ephemeral computer in the cloud)? I expect that auditability of translations (localisations) will be key at various times in the life of the project, sometimes long after the event that performed the translation/localisation, so auditing and maintaining easily readable snapshots might also be key to do.

abstract val resDir: DirectoryProperty

private val apiKey: String =
System.getenv("OPENAI_API_KEY") ?: error("OPENAI_API_KEY environment variable not set")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this topic has already been mentioned in discussion so this is more of a reminder that relying on OpenAI (or other similar services) might be problematic from a legal/licensing perspective as and when the app is in production use.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about adding logging to this file as a partial approach to maintaining a record of what's happening?

language: String
): String {
val languageName = when (language) {
"es" -> "Spanish"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are great from a proof-of-concept point-of-view. Worth considering supporting locales rather than raw language codes e.g. to support es_MX for Mexican Spanish, etc.

sourceDir.walkTopDown()
.filter { it.isFile && it.extension in listOf("kt", "java") }
.forEach { file ->
val regex = Regex("@Translatable\\s*\\\"(.*?)\\\"")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like having automated test cases that check the translation will behave as desired even for multi-line strings, etc. might be useful at checking whether these sorts of regex mistakes are dealt with?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants