First Stab at I18N - #20

Open
ISNIT0 wants to merge 3 commits into
mainfrom
ISNIT0/i18n
Open

First Stab at I18N#20
ISNIT0 wants to merge 3 commits into
mainfrom
ISNIT0/i18n

Conversation

@ISNIT0

@ISNIT0ISNIT0 commented Jan 14, 2025

Copy link
Copy Markdown
Member
  • 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"// normalval greeting ="Hello".i18n() // magic// If device locale is frenchprint(greeting.resolve()) // prints: bonjourval subString ="Hello, {name}"print(greeting.resolve(mapOf("name" to "World!"))) // prints: Hello, World!

@ISNIT0
ISNIT0force-pushed the ISNIT0/i18n branch 4 times, most recently from a09386a to 496daadCompareJanuary 14, 2025 22:47
viewModel: StoneCameraViewModel,
previousMode: String,
nextMode: String
previousMode: TranslatableString,

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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") {

@vckttrvckttrJan 16, 2025

Copy link
Copy Markdown
Collaborator

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
Copy Markdown
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]"), "_")

@vckttrvckttrJan 16, 2025

Copy link
Copy Markdown
Collaborator

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"

@vckttrvckttrJan 16, 2025

Copy link
Copy Markdown
Collaborator

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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

duplicated logic with

@julianhartyjulianharty left a comment

Copy link
Copy Markdown
Collaborator

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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

Comment threadbuild.gradle.kts

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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 freeto 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

@ISNIT0@julianharty@vckttr
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

First Stab at I18N - #20

Open
ISNIT0 wants to merge 3 commits into
mainfrom
ISNIT0/i18n
Open

First Stab at I18N#20
ISNIT0 wants to merge 3 commits into
mainfrom
ISNIT0/i18n

Conversation

@ISNIT0

@ISNIT0ISNIT0 commented Jan 14, 2025

Copy link
Copy Markdown
Member
  • 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"// normalval greeting ="Hello".i18n() // magic// If device locale is frenchprint(greeting.resolve()) // prints: bonjourval subString ="Hello, {name}"print(greeting.resolve(mapOf("name" to "World!"))) // prints: Hello, World!

@ISNIT0
ISNIT0force-pushed the ISNIT0/i18n branch 4 times, most recently from a09386a to 496daadCompareJanuary 14, 2025 22:47
viewModel: StoneCameraViewModel,
previousMode: String,
nextMode: String
previousMode: TranslatableString,

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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") {

@vckttrvckttrJan 16, 2025

Copy link
Copy Markdown
Collaborator

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
Copy Markdown
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]"), "_")

@vckttrvckttrJan 16, 2025

Copy link
Copy Markdown
Collaborator

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"

@vckttrvckttrJan 16, 2025

Copy link
Copy Markdown
Collaborator

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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

duplicated logic with

@julianhartyjulianharty left a comment

Copy link
Copy Markdown
Collaborator

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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

Comment threadbuild.gradle.kts

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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 freeto 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

@ISNIT0@julianharty@vckttr
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

First Stab at I18N - #20

Open
ISNIT0 wants to merge 3 commits into
mainfrom
ISNIT0/i18n
Open

First Stab at I18N#20
ISNIT0 wants to merge 3 commits into
mainfrom
ISNIT0/i18n

Conversation

@ISNIT0

@ISNIT0ISNIT0 commented Jan 14, 2025

Copy link
Copy Markdown
Member
  • 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"// normalval greeting ="Hello".i18n() // magic// If device locale is frenchprint(greeting.resolve()) // prints: bonjourval subString ="Hello, {name}"print(greeting.resolve(mapOf("name" to "World!"))) // prints: Hello, World!

@ISNIT0
ISNIT0force-pushed the ISNIT0/i18n branch 4 times, most recently from a09386a to 496daadCompareJanuary 14, 2025 22:47
viewModel: StoneCameraViewModel,
previousMode: String,
nextMode: String
previousMode: TranslatableString,

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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") {

@vckttrvckttrJan 16, 2025

Copy link
Copy Markdown
Collaborator

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
Copy Markdown
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]"), "_")

@vckttrvckttrJan 16, 2025

Copy link
Copy Markdown
Collaborator

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"

@vckttrvckttrJan 16, 2025

Copy link
Copy Markdown
Collaborator

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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

duplicated logic with

@julianhartyjulianharty left a comment

Copy link
Copy Markdown
Collaborator

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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

Comment threadbuild.gradle.kts

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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 freeto 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

@ISNIT0@julianharty@vckttr
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

First Stab at I18N - #20

Open
ISNIT0 wants to merge 3 commits into
mainfrom
ISNIT0/i18n
Open

First Stab at I18N#20
ISNIT0 wants to merge 3 commits into
mainfrom
ISNIT0/i18n

Conversation

@ISNIT0

@ISNIT0ISNIT0 commented Jan 14, 2025

Copy link
Copy Markdown
Member
  • 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"// normalval greeting ="Hello".i18n() // magic// If device locale is frenchprint(greeting.resolve()) // prints: bonjourval subString ="Hello, {name}"print(greeting.resolve(mapOf("name" to "World!"))) // prints: Hello, World!

@ISNIT0
ISNIT0force-pushed the ISNIT0/i18n branch 4 times, most recently from a09386a to 496daadCompareJanuary 14, 2025 22:47
viewModel: StoneCameraViewModel,
previousMode: String,
nextMode: String
previousMode: TranslatableString,

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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") {

@vckttrvckttrJan 16, 2025

Copy link
Copy Markdown
Collaborator

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
Copy Markdown
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]"), "_")

@vckttrvckttrJan 16, 2025

Copy link
Copy Markdown
Collaborator

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"

@vckttrvckttrJan 16, 2025

Copy link
Copy Markdown
Collaborator

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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

duplicated logic with

@julianhartyjulianharty left a comment

Copy link
Copy Markdown
Collaborator

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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

Comment threadbuild.gradle.kts

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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 freeto 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

@ISNIT0@julianharty@vckttr
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

First Stab at I18N - #20

Open
ISNIT0 wants to merge 3 commits into
mainfrom
ISNIT0/i18n
Open

First Stab at I18N#20
ISNIT0 wants to merge 3 commits into
mainfrom
ISNIT0/i18n

Conversation

@ISNIT0

@ISNIT0ISNIT0 commented Jan 14, 2025

Copy link
Copy Markdown
Member
  • 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"// normalval greeting ="Hello".i18n() // magic// If device locale is frenchprint(greeting.resolve()) // prints: bonjourval subString ="Hello, {name}"print(greeting.resolve(mapOf("name" to "World!"))) // prints: Hello, World!

@ISNIT0
ISNIT0force-pushed the ISNIT0/i18n branch 4 times, most recently from a09386a to 496daadCompareJanuary 14, 2025 22:47
viewModel: StoneCameraViewModel,
previousMode: String,
nextMode: String
previousMode: TranslatableString,

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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") {

@vckttrvckttrJan 16, 2025

Copy link
Copy Markdown
Collaborator

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
Copy Markdown
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]"), "_")

@vckttrvckttrJan 16, 2025

Copy link
Copy Markdown
Collaborator

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"

@vckttrvckttrJan 16, 2025

Copy link
Copy Markdown
Collaborator

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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

duplicated logic with

@julianhartyjulianharty left a comment

Copy link
Copy Markdown
Collaborator

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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

Comment threadbuild.gradle.kts

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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 freeto 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

@ISNIT0@julianharty@vckttr
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

First Stab at I18N - #20

Open
ISNIT0 wants to merge 3 commits into
mainfrom
ISNIT0/i18n
Open

First Stab at I18N#20
ISNIT0 wants to merge 3 commits into
mainfrom
ISNIT0/i18n

Conversation

@ISNIT0

@ISNIT0ISNIT0 commented Jan 14, 2025

Copy link
Copy Markdown
Member
  • 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"// normalval greeting ="Hello".i18n() // magic// If device locale is frenchprint(greeting.resolve()) // prints: bonjourval subString ="Hello, {name}"print(greeting.resolve(mapOf("name" to "World!"))) // prints: Hello, World!

@ISNIT0
ISNIT0force-pushed the ISNIT0/i18n branch 4 times, most recently from a09386a to 496daadCompareJanuary 14, 2025 22:47
viewModel: StoneCameraViewModel,
previousMode: String,
nextMode: String
previousMode: TranslatableString,

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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") {

@vckttrvckttrJan 16, 2025

Copy link
Copy Markdown
Collaborator

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
Copy Markdown
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]"), "_")

@vckttrvckttrJan 16, 2025

Copy link
Copy Markdown
Collaborator

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"

@vckttrvckttrJan 16, 2025

Copy link
Copy Markdown
Collaborator

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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

duplicated logic with

@julianhartyjulianharty left a comment

Copy link
Copy Markdown
Collaborator

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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

Comment threadbuild.gradle.kts

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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 freeto 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

@ISNIT0@julianharty@vckttr
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

First Stab at I18N - #20

Open
ISNIT0 wants to merge 3 commits into
mainfrom
ISNIT0/i18n
Open

First Stab at I18N#20
ISNIT0 wants to merge 3 commits into
mainfrom
ISNIT0/i18n

Conversation

@ISNIT0

@ISNIT0ISNIT0 commented Jan 14, 2025

Copy link
Copy Markdown
Member
  • 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"// normalval greeting ="Hello".i18n() // magic// If device locale is frenchprint(greeting.resolve()) // prints: bonjourval subString ="Hello, {name}"print(greeting.resolve(mapOf("name" to "World!"))) // prints: Hello, World!

@ISNIT0
ISNIT0force-pushed the ISNIT0/i18n branch 4 times, most recently from a09386a to 496daadCompareJanuary 14, 2025 22:47
viewModel: StoneCameraViewModel,
previousMode: String,
nextMode: String
previousMode: TranslatableString,

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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") {

@vckttrvckttrJan 16, 2025

Copy link
Copy Markdown
Collaborator

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
Copy Markdown
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]"), "_")

@vckttrvckttrJan 16, 2025

Copy link
Copy Markdown
Collaborator

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"

@vckttrvckttrJan 16, 2025

Copy link
Copy Markdown
Collaborator

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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

duplicated logic with

@julianhartyjulianharty left a comment

Copy link
Copy Markdown
Collaborator

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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

Comment threadbuild.gradle.kts

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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 freeto 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

@ISNIT0@julianharty@vckttr
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

First Stab at I18N - #20

Open
ISNIT0 wants to merge 3 commits into
mainfrom
ISNIT0/i18n
Open

First Stab at I18N#20
ISNIT0 wants to merge 3 commits into
mainfrom
ISNIT0/i18n

Conversation

@ISNIT0

@ISNIT0ISNIT0 commented Jan 14, 2025

Copy link
Copy Markdown
Member
  • 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"// normalval greeting ="Hello".i18n() // magic// If device locale is frenchprint(greeting.resolve()) // prints: bonjourval subString ="Hello, {name}"print(greeting.resolve(mapOf("name" to "World!"))) // prints: Hello, World!

@ISNIT0
ISNIT0force-pushed the ISNIT0/i18n branch 4 times, most recently from a09386a to 496daadCompareJanuary 14, 2025 22:47
viewModel: StoneCameraViewModel,
previousMode: String,
nextMode: String
previousMode: TranslatableString,

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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") {

@vckttrvckttrJan 16, 2025

Copy link
Copy Markdown
Collaborator

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
Copy Markdown
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]"), "_")

@vckttrvckttrJan 16, 2025

Copy link
Copy Markdown
Collaborator

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"

@vckttrvckttrJan 16, 2025

Copy link
Copy Markdown
Collaborator

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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

duplicated logic with

@julianhartyjulianharty left a comment

Copy link
Copy Markdown
Collaborator

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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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.

Comment threadbuild.gradle.kts

Copy link
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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
Copy Markdown
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 freeto 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

@ISNIT0@julianharty@vckttr