Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
39 changes: 38 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 = "<transifex_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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
16 changes: 9 additions & 7 deletions TransifexNativeSDK/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -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
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,14 @@ public void process(@NonNull Document document, @NonNull LinkedHashMap<String, L

if (child.getName().equals("string")) {
String key = child.getAttribute("name").getValue();
String string = getXMLText(child);
String xmlText = getXMLText(child);
// Ignore resource references
if (string.startsWith("@")) {
if (xmlText.startsWith("@")) {
continue;
}
// 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.
String string = unescapeJavaString(xmlText);
stringMap.put(key, new LocaleData.StringInfo(string));
}
else if (child.getName().equals("plurals")) {
Expand Down Expand Up @@ -145,13 +148,7 @@ else if (child.getName().equals("plurals")) {
}

/**
* Returns the content of the provided element as text, including any XML content. It also
* applies the special character handling of Android's XML parser as defined
* <a href="https://developer.android.com/guide/topics/resources/string-resource#escaping_quotes">here</a>.
* <p>
* 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 "&amp;", "&lt;" and "&gt;"}
* (with the exception of {@code "&quot;" 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) {
Expand All @@ -163,37 +160,34 @@ 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.
* <p>
* It also applies the special character handling of Android's XML parser as defined
* <a href="https://developer.android.com/guide/topics/resources/string-resource#escaping_quotes">here</a>.
* <p>
* 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 "&amp;", "&lt;" and "&gt;"}
* (with the exception of {@code "&quot;" which is converted to """} are left untouched.
* <ul>
* <li><strong>&#92;b &#92;f &#92;n &#92;r &#92;t &#92;" &#92;'</strong> :
* BS, FF, NL, CR, TAB, double and single quote.</li>
* <li><strong>&#92;X &#92;XX &#92;XXX</strong> : Octal character
* specification (0 - 377, 0x00 - 0xFF).</li>
* <li><strong>&#92;uXXXX</strong> : Hexadecimal based Unicode character.</li>
* </ul>
*
* Changes have been made to the <a href="https://gist.github.com/uklimaschewski/6741769">
* original code</a> to follow the Android XML Parser behavior. Octal support has been
* removed. The following are now supported:
* <ul>
* <li>Whitespace character sequences are collapsed into a single space, unless they
* are enclosed in double quotes.</li>
* <li>Tabs are converted to spaces.</li>
* <li>New lines are converted and collapsed into a single space, unless they are
* enclosed in double quotes.</li>
* <li>Whitespace character sequences, either typed as such or converted from tabs or
* new lines, are collapsed into a single space, unless they are enclosed in double
* quotes.</li>
* <li>Double quotes are removed, unless escaped.</li>
* <li>Single quotes are removed (you can't actually write them using Android Studio's
* string editor), unless escaped or quoted.</li>
Expand All @@ -209,6 +203,8 @@ private String getXMLText(@NonNull Element element) {
* @return The unescaped string.
*/
public String unescapeJavaString(String st) {
// Replace tabs with spaces.
st = st.replace('\t', ' ');

StringBuilder sb = new StringBuilder(st.length());

Expand Down Expand Up @@ -271,12 +267,15 @@ else if (ch == '\'') {
continue;
}
}
else if (ch == ' ') {
else if (ch == ' ' || ch == '\n') {
if (!isInsideDoubleQuotes) {
// Collapse sequences of whitespace characters into a single space, unless we're
// inside double quotes
//
// Collapse sequences of whitespace characters or new lines into a single space,
// unless we're inside double quotes. This also, implicitly replaces a new line
// with a space.
ch = ' ';
for (int nextIndex = i+1; nextIndex < st.length(); nextIndex++) {
if (st.charAt(nextIndex) == ' ') {
if (st.charAt(nextIndex) == ' ' || st.charAt(nextIndex) == '\n') {
i = nextIndex;
}
else {
Expand Down
Loading
Loading