Skip to content

Commit

Permalink
Add i18n service and start using it (#2)
Browse files Browse the repository at this point in the history
* add i18n service and start using it

* uninstall expo-crypto explicitly
  • Loading branch information
rosalinddeibert authored Sep 17, 2024
1 parent 585bdd1 commit edcc2fe
Show file tree
Hide file tree
Showing 13 changed files with 516 additions and 4 deletions.
77 changes: 77 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,13 @@
"expo-standard-web-crypto": "^1.8.1",
"expo-status-bar": "~1.12.1",
"expo-system-ui": "~3.0.7",
"i18next": "^23.15.1",
"micro-key-producer": "^0.7.0",
"react": "18.2.0",
"react-i18next": "^15.0.2",
"react-native": "0.74.5",
"react-native-gesture-handler": "~2.16.1",
"react-native-localize": "^3.2.1",
"react-native-reanimated": "~3.10.1",
"react-native-safe-area-context": "4.10.5",
"react-native-screens": "3.31.1",
Expand Down
13 changes: 10 additions & 3 deletions src/app/(app)/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as Notifications from "expo-notifications";
import React from "react";
import { useTranslation } from "react-i18next";
import { Pressable, Text, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";

Expand All @@ -12,6 +13,7 @@ import { palette, sharedStyles as ss } from "@/src/styles";
export default function HomeScreen() {
const insets = useSafeAreaInsets();
const { conduitKeyPair } = useAccountContext();
const { t } = useTranslation();

const [message, setMessage] = React.useState("Conduit is OFF");

Expand Down Expand Up @@ -51,17 +53,22 @@ export default function HomeScreen() {
onPress={async () => {
await Notifications.requestPermissionsAsync();
setMessage("Conduit is not implemented yet!");
setTimeout(() => setMessage("Conduit is OFF"), 5000);
setTimeout(
() => setMessage(t("CONDUIT_OFF_I18N.string")),
5000,
);
}}
>
<Text style={[ss.whiteText, ss.boldFont]}>Turn ON</Text>
<Text style={[ss.whiteText, ss.boldFont]}>
{t("TURN_ON_I18N.string")}
</Text>
</Pressable>
<Text style={[ss.whiteText, ss.bodyFont]}>{message}</Text>
<NotificationsStatus />
</View>
<View style={[ss.flex, ss.row, ss.justifyCenter, ss.alignCenter]}>
<Text style={[ss.whiteText, ss.bodyFont]}>
Your Conduit ID:{" "}
{t("YOUR_ID_I18N.string")}{" "}
</Text>
<ProxyID proxyId={getProxyId(conduitKeyPair)} />
</View>
Expand Down
4 changes: 4 additions & 0 deletions src/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import { polyfillWebCrypto } from "expo-standard-web-crypto";
import { useEffect } from "react";
import "react-native-reanimated";

import i18nService from "@/src/i18n/i18n";

i18nService.initI18n();

polyfillWebCrypto();

import { AuthProvider } from "@/src/auth/context";
Expand Down
6 changes: 5 additions & 1 deletion src/app/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { router } from "expo-router";
import React from "react";
import { useTranslation } from "react-i18next";
import { ActivityIndicator, Text, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";

Expand All @@ -9,6 +10,7 @@ import { handleError } from "@/src/common/errors";
export default function Index() {
const insets = useSafeAreaInsets();
const { signIn } = useAuthContext();
const { t } = useTranslation();

React.useEffect(() => {
signIn().then((result) => {
Expand Down Expand Up @@ -41,7 +43,9 @@ export default function Index() {
alignItems: "center",
}}
>
<Text style={{ color: "white" }}>Loading...</Text>
<Text style={{ color: "white" }}>
{t("LOADING_I18N.string")}
</Text>
<ActivityIndicator color="white" size="large" />
</View>
</View>
Expand Down
85 changes: 85 additions & 0 deletions src/i18n/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Internationalization

This project uses the react-i18next package for string localization and the react-native-localize package for retrieving device information about the user's preferred languages.

## String Localization

To localize a string within the project:

### Import

Import and initialize the translation function in the component that contains the string to be translated.

```
import {useTranslation} from 'react-i18next';
```

```
const {t} = useTranslation();
```

### Replace string

Replace the string that is to be translated with a key which will be used to reference the string in the JSON translations file, followed by `.string`. Wrap the key in the translator function

```
<Text>
Sign in
</Text>
```

Should become

```
<Text>
{t("SIGN_IN_I18N.string")}
</Text>
```

The key should be a unique string that identifies the message and follows the MACRO_CASE naming convention, and should end in I18N to make searching the project for all translation strings easy.

### Add to JSON file

Add the key and string to `locales/en/translation.json` and include a developer_comment that explains the meaning of the string to translators.

```
"SIGN_IN_I18N": {
"string": "Sign in",
"developer_comment": "Title for the button where the user can sign in."
},
```

We are using this format so that the file can be uploaded to Transifex for translation, and we can provide additional information or context to the translators. More information about the JSON formatting options for Transifex can be found [here](https://help.transifex.com/en/articles/6220899-structured-json)

### Generate Pseudolocales

`node i18n/fake-translations.js`

## Pseudolocales

Pseudolocales are locales that are designed to simulate characteristics of languages that cause UI and layout related changes while still being readable in english. They are useful for testing app localization because they make obvious untranslated text and UI or layout related issues that might not be evident in other locales.

### Android Pseudolocales

There are two Pseudolocales available on Android OS, en-XA which is the accented package, and ar-XB which is the mirrored package. In order to test this application with these Pseudolocales, we have generated xa and xb translation.json files which contain the accented or mirrored strings. Each Pseudolocale can be enabled in the Android Settings on the phone, and our custom generated translation.json files will be used.

#### en-XA

Our pseudolocale generator creates en-XA strings by modifying the english strings to have additional characters and accents added. This is useful to simulate the use of various accents that might be uncommon in english, and to test the UI with longer strings as the same text translated into different locales will have different lengths. Both the accents and longer strings will help to make potential UI issues evident.

#### ar-XB

Our pseudolocale generator creates ar-XB strings by modifying the english strings to replace characters with mirrored/flipped versions of themselves and give the text a RTL direction. This is useful to simulate RTL text and view the app in an RTL layout to identify potential layout issues.

### Pseudolocale Generator

The pseudolocale strings for en-XA and ar-XB are generated from `fake-translations.js` and `pseudo.js`.

When strings are added or changed, this `node i18n/fake-translations.js` should be run to create the corresponding Pseudolocale strings.

The script copies all translation objects from `en/translations.json` into `en-xa/translations/json` and `ar-xb/translations.json` and runs the pseudo function from `pseudo.js` which modifies the string appropriately.

### iOS Pseudolocales

iOS does not offer language tags that can be used for Pseudolocale testing. The only available language tags from the iOS settings all correspond to real languages. For now, there is no way to test Pseudolocales on iOS.
62 changes: 62 additions & 0 deletions src/i18n/fake-translation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"use strict";

const fs = require("fs").promises;
const path = require("path");
const pseudo = require("./pseudo").PSEUDO;

// englishLines will hold the contents of the English translation file. xaLines and
// xbLines will hold the contents of the en-xa and ar-xb translation files, which are
// pseudolocales used for testing.
let englishLines,
xaLines = {},
xbLines = {};

async function getEnglishLines() {
const data = await fs.readFile(
path.resolve(__dirname, "locales/en/translation.json"),
(err, data) => {
if (err) throw err;
},
);
englishLines = JSON.parse(data);
}

async function processTranslationFiles() {
for (let translationItem in englishLines) {
// Add the strings from englishLines to xaLines and xbLines and translate using the
// corresponding pseudolocale function.
xaLines[translationItem] = {
string: pseudo["xa"].translate(
englishLines[translationItem].string,
),
};
xbLines[translationItem] = {
string: pseudo["xb"].translate(
englishLines[translationItem].string,
),
};
}

// Write the xa and xb lines to their respective files
await fs.writeFile(
path.resolve(__dirname, "locales/en-xa/translation.json"),
JSON.stringify(xaLines, null, 2) + "\n",
(err) => {
if (err) throw err;
},
);
await fs.writeFile(
path.resolve(__dirname, "locales/ar-xb/translation.json"),
JSON.stringify(xbLines, null, 2) + "\n",
(err) => {
if (err) throw err;
},
);
}

async function main() {
await getEnglishLines();
await processTranslationFiles();
}

main();
Loading

0 comments on commit edcc2fe

Please sign in to comment.