diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 84e7f59..767e361 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -36,10 +36,11 @@ jobs: run: ./gradlew test - name: Upload test reports if: always() - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: unitTestReports path: | TransifexNativeSDK/clitool/build/reports/tests/test/ TransifexNativeSDK/common/build/reports/tests/test/ TransifexNativeSDK/txsdk/build/reports/tests/ + if-no-files-found: warn diff --git a/README.md b/README.md index 435eaf4..be8e950 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Transifex Native Android SDK [![CI](https://github.com/transifex/transifex-java/actions/workflows/gradle.yml/badge.svg)](https://github.com/transifex/transifex-java/actions/workflows/gradle.yml) -[![Maven Central](https://img.shields.io/maven-central/v/com.transifex.txnative/txsdk?color=32c955)](https://maven-badges.herokuapp.com/maven-central/com.transifex.txnative/txsdk) +[![Maven Central](https://img.shields.io/maven-central/v/com.transifex.txnative/txsdk?color=32c955)](https://central.sonatype.com/artifact/com.transifex.txnative/txsdk) Transifex Native Android SDK is a collection of tools to easily localize your Android applications using [Transifex Native](https://www.transifex.com/native/). The Android library can fetch translations @@ -136,6 +136,43 @@ Context wrappedContext = TxNative.wrap(getApplicationContext()); wrappedContext.getString(); ``` +If you want to wrap the application context itself, you need to move the SDK's initialization from the application's `onCreate()` to `attachBaseContext()`: + +```java + @Override + protected void attachBaseContext(Context base) { + // Initialize TxNative + String token = ""; + + LocaleState localeState = new LocaleState( + base, // Use the base context instead of getApplicationContext() + // source locale + "en", + // supported locales + new String[]{"en", "el", "de", "fr", "ar", "sl", "es_ES", "es_MX"}, + null); + + TxNative.init( + // application context + base, // Use the base context instead of getApplicationContext() + // a LocaleState instance + localeState, + // token + token, + // cdsHost URL + null, + // a TxCache implementation + null, + // a MissingPolicy implementation + new AndroidMissingPolicy()); + + // Wrap the base application context + super.attachBaseContext(TxNative.wrap(base)); +} +``` + +Note though that this global wrapper can interfere with third-party libraries that use their own string resources. In that case, use "AndroidMissingPolicy" so that these libraries have their strings translated. + If you want to disable the SDK functionality, don't initialize it and don't call any `TxNative` methods. `TxNative.wrap()` will be a no-op and the context will not be wrapped. Thus, all `getString()` etc methods, won't flow through the SDK. ## Cache diff --git a/TransifexNativeSDK/app/src/main/java/com/transifex/myapplication/MyApplication.java b/TransifexNativeSDK/app/src/main/java/com/transifex/myapplication/MyApplication.java index 5740a75..37d9730 100644 --- a/TransifexNativeSDK/app/src/main/java/com/transifex/myapplication/MyApplication.java +++ b/TransifexNativeSDK/app/src/main/java/com/transifex/myapplication/MyApplication.java @@ -1,6 +1,7 @@ package com.transifex.myapplication; import android.app.Application; +import android.content.Context; import android.content.Intent; import com.transifex.txnative.LocaleState; @@ -20,36 +21,50 @@ public void onCreate() { // https://stackoverflow.com/questions/55265834/change-locale-not-work-after-migrate-to-androidx/58004553#58004553 // AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); + // Start a service just for testing purposes + Intent serviceIntent = new Intent(this, SimpleIntentService.class); + SimpleIntentService.enqueueWork(this, serviceIntent); + + // Uncomment to use strings as served by Android prefixed with "test: " +// TxNative.setTestMode(true); + + // Uncomment, to disable styling of strings with HTML markup such as + // R.string.styled_text_not_escaped +// TxNative.setSupportSpannable(false); + + // Fetch all translations from CDS + TxNative.fetchTranslations(null, null); + } + + @Override + protected void attachBaseContext(Context base) { // Initialize TxNative String token = null; // The app locales entered here should match the ones in `resConfigs` in gradle, so that // multi locale support works for newer Androids. - LocaleState localeState = new LocaleState(getApplicationContext(), + LocaleState localeState = new LocaleState(base, "en", new String[]{"en", "el", "de", "fr", "ar", "sl"}, null); + TxNative.init( - getApplicationContext(), // application context + base, // application context localeState, // a LocaleState instance token, // token null, // cdsHost URL null, // a TxCache implementation null); // a MissingPolicy implementation - // Uncomment to use strings as served by Android prefixed with "test: " -// TxNative.setTestMode(true); - - // Uncomment, to disable styling of strings with HTML markup such as - // R.string.styled_text_not_escaped -// TxNative.setSupportSpannable(false); + // OPTIONAL: + // Wrap the application's base context to allow TxNative to intercept all string resource + // requests (e.g. from getApplicationContext().getString()). + // Warning: This global wrapper can interfere with third-party libraries that use their + // own string resources. Use "AndroidMissingPolicy" so that these libraries have their + // strings translated. + super.attachBaseContext(TxNative.wrap(base)); - // Fetch all translations from CDS - TxNative.fetchTranslations(null, null); - - // Start a service just for testing purposes - Intent serviceIntent = new Intent(this, SimpleIntentService.class); - SimpleIntentService.enqueueWork(this, serviceIntent); + // SAFER: Do not wrap the application's base context. + // super.attachBaseContext(base); } - } diff --git a/TransifexNativeSDK/app/src/main/java/com/transifex/myapplication/SimpleIntentService.java b/TransifexNativeSDK/app/src/main/java/com/transifex/myapplication/SimpleIntentService.java index 37f1c8d..cff8670 100644 --- a/TransifexNativeSDK/app/src/main/java/com/transifex/myapplication/SimpleIntentService.java +++ b/TransifexNativeSDK/app/src/main/java/com/transifex/myapplication/SimpleIntentService.java @@ -24,8 +24,8 @@ public static void enqueueWork(Context context, Intent work) { @Override protected void onHandleWork(@NonNull Intent intent) { - // Make sure that you use getBaseContext() and not getApplicationContext() - String success = getBaseContext().getResources().getString(R.string.success); + // Make sure that you do not use an app context via getApplicationContext().getString() + String success = getString(R.string.success); Log.d(TAG, "Service status: " + success); } diff --git a/TransifexNativeSDK/build.gradle b/TransifexNativeSDK/build.gradle index 273ff30..657096d 100644 --- a/TransifexNativeSDK/build.gradle +++ b/TransifexNativeSDK/build.gradle @@ -15,11 +15,11 @@ buildscript { 'androidxEspressoCore' : '3.5.1' ] ext { - sdkVersionCode = 10 // version code for txsdk - sdkVersion = '1.3.0' // version for txsdk and common + sdkVersionCode = 11 // version code for txsdk + sdkVersion = '1.4.0' // version for txsdk and common pomGroupID = "com.transifex.txnative" // pom group id for txsdk and common - cliVersion = '1.3.0' // clitool version + cliVersion = '1.4.0' // clitool version } repositories { google() @@ -32,7 +32,7 @@ buildscript { } plugins { - id "io.github.gradle-nexus.publish-plugin" version "1.3.0" + id "io.github.gradle-nexus.publish-plugin" version "2.0.0" id 'com.android.application' version '8.1.0' apply false id 'com.android.library' version '8.1.0' apply false id 'org.jetbrains.kotlin.android' version '1.9.20' apply false @@ -70,15 +70,17 @@ if (pgpKeyContent != null) { ext['signing.secretKeyRingFile'] = keyFile.absolutePath } +// According to https://github.com/gradle-nexus/publish-plugin?tab=readme-ov-file#publishing-to-maven-central-via-sonatype-central nexusPublishing { repositories { + // see https://central.sonatype.org/publish/publish-portal-ossrh-staging-api/#configuration sonatype { - stagingProfileId = 'e1e4b9ea52730' - nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) - snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) + nexusUrl.set(uri("https://ossrh-staging-api.central.sonatype.com/service/local/")) + snapshotRepositoryUrl.set(uri("https://central.sonatype.com/repository/maven-snapshots/")) username = ossrhUsername password = ossrhPassword version = sdkVersion + group = pomGroupID } } } diff --git a/TransifexNativeSDK/clitool/src/main/java/com/transifex/clitool/StringXMLConverter.java b/TransifexNativeSDK/clitool/src/main/java/com/transifex/clitool/StringXMLConverter.java index ee9787e..a00b70f 100644 --- a/TransifexNativeSDK/clitool/src/main/java/com/transifex/clitool/StringXMLConverter.java +++ b/TransifexNativeSDK/clitool/src/main/java/com/transifex/clitool/StringXMLConverter.java @@ -105,11 +105,14 @@ public void process(@NonNull Document document, @NonNull LinkedHashMaphere. - *

- * The final processing of HTML entities and tags is left for the SDK at runtime. So, any tags - * are left as is. HTML entities such as {@code "&", "<" and ">"} - * (with the exception of {@code """ which is converted to """} are left untouched. + * Returns the content of the provided element as text, including any XML content. */ @NonNull private String getXMLText(@NonNull Element element) { @@ -163,28 +160,21 @@ private String getXMLText(@NonNull Element element) { return ""; } - String originalString = stringWriter.toString(); - - // We follow Android XML Parser's special character handling. - - // Replace new lines with spaces. - String unescapedString = originalString.replace('\n', ' '); - // Replace tabs with spaces. - unescapedString = unescapedString.replace('\t', ' '); - // Unescape special chars. For example convert a typed "\n" ("\\n" in Java) to a new line - // ("\n" in Java) and do some extra processing similar to the Android XML parser. - unescapedString= unescapeJavaString(unescapedString); - - return unescapedString; + return stringWriter.toString(); } /** * Unescapes a string that contains standard Java escape sequences. + *

+ * It also applies the special character handling of Android's XML parser as defined + * here. + *

+ * The final processing of HTML entities and tags is left for the SDK at runtime. So, any tags + * are left as is. HTML entities such as {@code "&", "<" and ">"} + * (with the exception of {@code """ which is converted to """} are left untouched. *

* @@ -192,8 +182,12 @@ private String getXMLText(@NonNull Element element) { * original code to follow the Android XML Parser behavior. Octal support has been * removed. The following are now supported: *