-
Notifications
You must be signed in to change notification settings - Fork 0
First Stab at I18N #20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
name: Generate Translations | ||
|
||
on: | ||
push: | ||
branches: | ||
- main | ||
|
||
jobs: | ||
generate-translations: | ||
name: Update and Generate Translations | ||
runs-on: ubuntu-latest | ||
permissions: write-all | ||
|
||
steps: | ||
- name: Checkout repository | ||
uses: actions/checkout@v3 | ||
with: | ||
fetch-depth: 0 # Fetch full history for creating PRs | ||
|
||
# Set up JDK | ||
- name: Set up JDK 17 | ||
uses: actions/setup-java@v3 | ||
with: | ||
distribution: 'temurin' | ||
java-version: '17' | ||
|
||
- name: Setup Gradle | ||
uses: gradle/actions/setup-gradle@v4 | ||
with: | ||
gradle-version: wrapper | ||
cache-read-only: false | ||
|
||
# Cache Gradle dependencies | ||
- name: Cache Gradle dependencies | ||
uses: actions/cache@v3 | ||
with: | ||
path: ~/.gradle/caches | ||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} | ||
restore-keys: | | ||
${{ runner.os }}-gradle- | ||
|
||
- name: Update Strings XML | ||
run: ./gradlew updateStringsXml | ||
|
||
- name: Generate Translations | ||
env: | ||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} | ||
run: ./gradlew generateTranslations | ||
|
||
- name: Create Pull Request | ||
if: steps.changes-check.outputs.changes != 'false' | ||
uses: peter-evans/create-pull-request@v5 | ||
with: | ||
token: ${{ secrets.GITHUB_TOKEN }} | ||
title: "Update Translations ${{ github.event.pull_request.number }}" | ||
body: "Update Translations ${{ github.event.pull_request.number }}" | ||
branch: i18n-${{ github.event.pull_request.number }} | ||
base: main | ||
labels: | | ||
translations |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,3 +13,4 @@ | |
.externalNativeBuild | ||
.cxx | ||
local.properties | ||
buildSrc/build |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,6 +13,7 @@ import androidx.core.content.ContextCompat | |
import androidx.core.view.WindowCompat | ||
import androidx.core.view.WindowInsetsCompat | ||
import androidx.core.view.WindowInsetsControllerCompat | ||
import co.stonephone.stonecamera.utils.TranslationManager | ||
|
||
class MainActivity : ComponentActivity() { | ||
|
||
|
@@ -43,6 +44,8 @@ class MainActivity : ComponentActivity() { | |
override fun onCreate(savedInstanceState: Bundle?) { | ||
super.onCreate(savedInstanceState) | ||
|
||
TranslationManager.loadTranslationsForLocale(applicationContext) | ||
|
||
if (!isChromeOS()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT | ||
} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With the removal of android.util.log what's happening about logging? |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,23 +1,15 @@ | ||
// StoneCameraApp.kt | ||
@file:kotlin.OptIn(ExperimentalMaterial3Api::class) | ||
|
||
package co.stonephone.stonecamera | ||
|
||
import android.annotation.SuppressLint | ||
import android.graphics.Rect | ||
import android.util.Log | ||
import androidx.annotation.OptIn | ||
import androidx.camera.camera2.interop.ExperimentalCamera2Interop | ||
import androidx.camera.lifecycle.ProcessCameraProvider | ||
import androidx.compose.foundation.background | ||
import androidx.compose.foundation.clickable | ||
import androidx.compose.foundation.layout.* | ||
import androidx.compose.foundation.shape.CircleShape | ||
import androidx.compose.material.icons.Icons | ||
import androidx.compose.material.icons.filled.FlipCameraAndroid | ||
import androidx.compose.material3.ExperimentalMaterial3Api | ||
import androidx.compose.material3.Icon | ||
import androidx.compose.material3.IconButton | ||
import androidx.compose.material3.MaterialTheme | ||
import androidx.compose.material3.Text | ||
import androidx.compose.runtime.* | ||
|
@@ -185,8 +177,8 @@ fun StoneCameraApp( | |
) { | ||
modePlugins.forEach { modePlugin -> | ||
val modeLabel = modePlugin.modeLabel!! | ||
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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. as i've got you can leave it as it was |
||
style = MaterialTheme.typography.bodyLarge, | ||
fontWeight = FontWeight.Bold, | ||
modifier = Modifier.clickable { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,7 +28,10 @@ import co.stonephone.stonecamera.plugins.IPlugin | |
import co.stonephone.stonecamera.plugins.PluginSetting | ||
import co.stonephone.stonecamera.plugins.PluginUseCase | ||
import co.stonephone.stonecamera.utils.StoneCameraInfo | ||
import co.stonephone.stonecamera.utils.Translatable | ||
import co.stonephone.stonecamera.utils.TranslatableString | ||
import co.stonephone.stonecamera.utils.createCameraSelectorForId | ||
import co.stonephone.stonecamera.utils.i18n | ||
import co.stonephone.stonecamera.utils.selectCameraForStepZoomLevel | ||
import kotlinx.coroutines.CompletableDeferred | ||
import kotlinx.coroutines.CoroutineScope | ||
|
@@ -74,7 +77,8 @@ class StoneCameraViewModel( | |
var isPaused by mutableStateOf(false) | ||
private set | ||
|
||
var selectedMode by mutableStateOf("photo") | ||
@Translatable | ||
var selectedMode by mutableStateOf("photo".i18n()) | ||
private set | ||
|
||
private val _plugins = mutableListOf<IPlugin>() | ||
|
@@ -189,16 +193,25 @@ class StoneCameraViewModel( | |
|
||
@Suppress("UNCHECKED_CAST") | ||
return when (defaultValue) { | ||
is String -> prefs.getString(settingKey, defaultValue) as? T | ||
is TranslatableString -> prefs.getString(settingKey, defaultValue.resolve()) as? T | ||
is Float -> prefs.getFloat(settingKey, defaultValue) as? T | ||
else -> null | ||
} | ||
} | ||
|
||
// Retrieve a setting with automatic recomposition support | ||
fun <T> getSetting(settingKey: String): T? { | ||
val _value = settings[settingKey] | ||
|
||
val value = when (_value) { | ||
is TranslatableString -> _value | ||
is String -> _value.i18n() | ||
is Float -> prefs.getFloat(settingKey, _value) | ||
else -> null | ||
} | ||
|
||
@Suppress("UNCHECKED_CAST") | ||
return settings[settingKey] as? T | ||
return value as? T | ||
} | ||
|
||
// Update a setting and notify observers | ||
|
@@ -207,7 +220,7 @@ class StoneCameraViewModel( | |
|
||
when (setting) { | ||
is PluginSetting.EnumSetting -> { | ||
prefs.edit().putString(settingKey, value as String).apply() | ||
prefs.edit().putString(settingKey, (value as TranslatableString).raw).apply() | ||
} | ||
|
||
is PluginSetting.ScalarSetting -> { | ||
|
@@ -262,7 +275,7 @@ class StoneCameraViewModel( | |
/** | ||
* Switch between Photo and Video modes. | ||
*/ | ||
fun selectMode(mode: String) { | ||
fun selectMode(mode: TranslatableString) { | ||
plugins.forEach { | ||
it.onModeSelected(this, selectedMode, mode) | ||
} | ||
|
@@ -462,7 +475,6 @@ class StoneCameraViewModel( | |
try { | ||
// Await all work to finish | ||
workList.awaitAll() | ||
Log.d("Analyzer", "All plugins completed successfully") | ||
} catch (e: Exception) { | ||
Log.e("Analyzer", "Error in one or more plugins", e) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||
} finally { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,17 +17,20 @@ import androidx.compose.ui.text.font.FontWeight | |
import androidx.compose.ui.unit.dp | ||
import co.stonephone.stonecamera.MyApplication | ||
import co.stonephone.stonecamera.StoneCameraViewModel | ||
import co.stonephone.stonecamera.utils.Translatable | ||
import co.stonephone.stonecamera.utils.TranslatableString | ||
import co.stonephone.stonecamera.utils.getLargestMatchingSize | ||
import co.stonephone.stonecamera.utils.getLargestSensorSize | ||
import co.stonephone.stonecamera.utils.i18n | ||
|
||
class AspectRatioPlugin : IPlugin { | ||
override val id: String = "aspectRatioPlugin" | ||
override val name: String = "Aspect Ratio" | ||
|
||
override fun initialize(viewModel: StoneCameraViewModel) { | ||
val previewView = viewModel.previewView | ||
val aspectRatio = viewModel.getSetting<String>("aspectRatio") | ||
if (aspectRatio === "FULL") { | ||
val aspectRatio = viewModel.getSetting<TranslatableString>("aspectRatio") | ||
if (aspectRatio?.resolve() === "FULL") { | ||
previewView!!.scaleType = PreviewView.ScaleType.FILL_CENTER | ||
} else { | ||
previewView!!.scaleType = PreviewView.ScaleType.FIT_CENTER | ||
|
@@ -39,10 +42,10 @@ class AspectRatioPlugin : IPlugin { | |
imageCapture: ImageCapture.Builder | ||
): ImageCapture.Builder { | ||
// TODO "FULL" aspect ratio isn't cropping on image capture | ||
val ratioStr = viewModel.getSetting<String>("aspectRatio") ?: "16:9" | ||
val ratioStr = viewModel.getSetting<TranslatableString>("aspectRatio") ?: "16:9".i18n() | ||
val context = MyApplication.getAppContext() | ||
|
||
val ratioOrNull = parseRatioOrNull(ratioStr) | ||
val ratioOrNull = parseRatioOrNull(ratioStr.resolve()) | ||
|
||
val targetSize = if (ratioOrNull == null) { | ||
getLargestSensorSize(viewModel.camera!!, context) | ||
|
@@ -60,10 +63,10 @@ class AspectRatioPlugin : IPlugin { | |
viewModel: StoneCameraViewModel, | ||
preview: Preview.Builder | ||
): Preview.Builder { | ||
val ratioStr = viewModel.getSetting<String>("aspectRatio") ?: "16:9" | ||
val ratioStr = viewModel.getSetting<TranslatableString>("aspectRatio") ?: "16:9".i18n() | ||
|
||
val context = MyApplication.getAppContext() | ||
val ratioOrNull = parseRatioOrNull(ratioStr) | ||
val ratioOrNull = parseRatioOrNull(ratioStr.resolve()) | ||
|
||
// 1) Figure out the best "preferred size" for this ratio | ||
val targetSize = if (ratioOrNull == null) { | ||
|
@@ -151,8 +154,8 @@ class AspectRatioPlugin : IPlugin { | |
viewModel: StoneCameraViewModel, | ||
previewView: PreviewView | ||
): PreviewView { | ||
val aspectRatio = viewModel.getSetting<String>("aspectRatio") | ||
if (aspectRatio == "FULL") { | ||
val aspectRatio = viewModel.getSetting<TranslatableString>("aspectRatio") | ||
if (aspectRatio?.resolve() == "FULL") { | ||
previewView.scaleType = PreviewView.ScaleType.FILL_CENTER | ||
} else { | ||
previewView.scaleType = PreviewView.ScaleType.FIT_CENTER | ||
|
@@ -171,29 +174,29 @@ class AspectRatioPlugin : IPlugin { | |
listOf( | ||
PluginSetting.EnumSetting( | ||
key = "aspectRatio", | ||
defaultValue = "16:9", | ||
options = listOf("16:9", "4:3", "FULL"), | ||
defaultValue = @Translatable "16:9".i18n(), | ||
options = listOf(@Translatable "16:9".i18n(), @Translatable "4:3".i18n(), @Translatable "FULL".i18n()), | ||
render = { value, isSelected -> | ||
Text( | ||
text = value, | ||
text = value.resolve(), | ||
color = if (isSelected) Color(0xFFFFCC00) else Color.White, | ||
style = MaterialTheme.typography.bodyLarge, | ||
fontWeight = FontWeight.Bold, | ||
modifier = Modifier | ||
.padding(8.dp) | ||
) | ||
}, | ||
onChange = { viewModel, value -> | ||
onChange = { viewModel, _ -> | ||
val previewView = viewModel.previewView | ||
if (viewModel.getSetting<String>("aspectRatio") === "FULL") { | ||
if (viewModel.getSetting<TranslatableString>("aspectRatio")?.resolve() === "FULL") { | ||
previewView!!.scaleType = PreviewView.ScaleType.FILL_CENTER | ||
} else { | ||
previewView!!.scaleType = PreviewView.ScaleType.FIT_CENTER | ||
} | ||
viewModel.recreateUseCases() | ||
}, | ||
renderLocation = SettingLocation.TOP, | ||
label = "Aspect Ratio" | ||
label = @Translatable "Aspect Ratio".i18n() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||
) | ||
) | ||
} | ||
|
There was a problem hiding this comment.
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?