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
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
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
6 changes: 3 additions & 3 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 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
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,11 @@ static Document getXMLSpace() {
return getXMlFromFile("strings-test-space.xml");
}

static Document getXMLNewLineSpaceTab() {

return getXMlFromFile("strings-test-new-line-space-tab.xml");
}

static Document getXMLInsideDoubleQuotes() {

return getXMlFromFile("strings-test-insidedoublequotes.xml");
Expand All @@ -139,6 +144,11 @@ static Document getXMLHTMLEntities() {
return getXMlFromFile("strings-test-htmlentities.xml");
}

static Document getXMLAt() {

return getXMlFromFile("strings-test-at.xml");
}

static Document getPluralsXML() {
return getXML(stringsXMLPlurals);
}
Expand Down Expand Up @@ -210,10 +220,11 @@ public void testProcess_newLine() {
e.printStackTrace();
}

assertThat(stringMap.keySet()).containsExactly("key1", "key2").inOrder();
assertThat(stringMap.keySet()).containsExactly("key1", "key2", "key3").inOrder();
// This should be rendered as: "anew line \n \newline \\nb"
assertThat(stringMap.get("key1").string).isEqualTo("a\n \\n \\\n \\\\nb");
assertThat(stringMap.get("key2").string).isEqualTo("actual new line should be replaced by space");
assertThat(stringMap.get("key3").string).isEqualTo("multiple actual new lines should be replaced by a single space");
}

@Test
Expand Down Expand Up @@ -267,6 +278,19 @@ public void testProcess_space() {
assertThat(stringMap.get("key2").string).isEqualTo(" spaces at the beginning and some trailing ones ");
}

@Test
public void testProcess_newLineSpaceTab() {
Document document = getXMLNewLineSpaceTab();
try {
converter.process(document, stringMap);
} catch (JDOMException | StringXMLConverter.XMLConverterException e) {
e.printStackTrace();
}

assertThat(stringMap.keySet()).containsExactly("key1").inOrder();
assertThat(stringMap.get("key1").string).isEqualTo("multiple spaces, a tab and new lines should be collapsed into a single space");
}

@Test
public void testProcess_insideDoubleQuotes() {
Document document = getXMLInsideDoubleQuotes();
Expand All @@ -276,10 +300,15 @@ public void testProcess_insideDoubleQuotes() {
e.printStackTrace();
}

assertThat(stringMap.keySet()).containsExactly("key1", "key2", "key3").inOrder();
assertThat(stringMap.keySet()).containsExactly("key1", "key2", "key3", "key4").inOrder();
assertThat(stringMap.get("key1").string).isEqualTo("multiple spaces are allowed here but not here");
assertThat(stringMap.get("key2").string).isEqualTo("single quotes ' '' are allowed here but not here");
assertThat(stringMap.get("key3").string).isEqualTo("HTML entity quotes behave like normal quotes");
assertThat(stringMap.get("key4").string).isEqualTo("multiple new lines\n" +
"\n" +
" and new lines \n" +
" \n" +
" with spaces are allowed when in double quotes");
}

@Test
Expand Down Expand Up @@ -328,6 +357,22 @@ public void testProcess_htmlEntities() {
assertThat(stringMap.get("key1").string).isEqualTo("these html entities &amp; &lt; &gt; should be left as is");
}

@Test
public void testProcess_at() {
Document document = getXMLAt();
try {
converter.process(document, stringMap);
} catch (JDOMException | StringXMLConverter.XMLConverterException e) {
e.printStackTrace();
}

// key 2 should be ignored by our parser
assertThat(stringMap.keySet()).containsExactly("key1", "key3", "key4").inOrder();
assertThat(stringMap.get("key1").string).isEqualTo("using it here @ or escaped @ is ok");
assertThat(stringMap.get("key3").string).isEqualTo("@ this is ok");
assertThat(stringMap.get("key4").string).isEqualTo("<b>@</b> this is ok");
}

// endregion parse strings

// region parse plurals
Expand Down
7 changes: 7 additions & 0 deletions TransifexNativeSDK/clitool/testFiles/strings-test-at.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="key1">using it here @ or escaped \@ is ok</string>
<string name="key2">@resource this should be ignored</string>
<string name="key3">\@ this is ok</string>
<string name="key4"><b>@</b> this is ok</string>
</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,9 @@
<string name="key1">"multiple spaces are allowed here" but not here</string>
<string name="key2">"single quotes ' ''" are allowed here but not here''</string>
<string name="key3">HTML entity quotes behave like normal &quot; &quot;quotes</string>
<string name="key4">multiple new lines"

" and "new lines

with spaces" are allowed when in double quotes</string>
</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="key1">multiple spaces,

a tab and new lines should be collapsed into a single space</string>
</resources>
Loading
Loading