Skip to content
This repository has been archived by the owner on Jun 26, 2023. It is now read-only.

Commit

Permalink
[0.63] Migrate localization library to fbt (#159)
Browse files Browse the repository at this point in the history
Migrate localization library. We'd like to use `fbt` in react-native as well as react followed by the article https://medium.com/dooboolab/localizing-react-app-with-fbt-instead-of-i18n-90822e0cb373.
  • Loading branch information
hyochan authored Jan 9, 2021
1 parent 9c9ec79 commit 083499d
Show file tree
Hide file tree
Showing 57 changed files with 2,501 additions and 1,068 deletions.
21 changes: 13 additions & 8 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,17 @@ jobs:
paths:
- node_modules

- run:
name: jest tests
command: |
mkdir -p test-results/jest
yarn run test
environment:
JEST_JUNIT_OUTPUT: test-results/jest/junit.xml

- persist_to_workspace:
root: ~/dooboo
paths:
- node_modules

- run:
name: Generate fbt
working_directory: .
command: |
yarn fbt:all
- run:
name: ESLint Test
working_directory: .
Expand Down Expand Up @@ -110,6 +108,13 @@ jobs:
# paths:
# - android/vendor/bundle

- run:
name: Generate fbt
working_directory: .
command: |
yarn fbt:all && yarn fbt:android
- run:
name: Check MD5 on files
command: |
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,7 @@ package-lock.json

ios/Pods/

.vscode
# fbt
i18n/fbt/.enum_manifest.json
i18n/fbt/.source_strings.json
i18n/fbt/.src_manifest.json
92 changes: 92 additions & 0 deletions @types/fbt/globals.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/// <reference types="react" />

declare namespace FBT {
type $Values<T> = T[keyof T];

// https://github.com/facebookincubator/fbt/blob/e9c591f451dbfc91852e316869ae39ad41848c55/runtime/nonfb/GenderConst.js#L9-L23
interface GenderConst {
NOT_A_PERSON: 0;
FEMALE_SINGULAR: 1;
MALE_SINGULAR: 2;
FEMALE_SINGULAR_GUESS: 3;
MALE_SINGULAR_GUESS: 4;
MIXED_SINGULAR: 5;
MIXED_PLURAL: 5;
NEUTER_SINGULAR: 6;
UNKNOWN_SINGULAR: 7;
FEMALE_PLURAL: 8;
MALE_PLURAL: 9;
NEUTER_PLURAL: 10;
UNKNOWN_PLURAL: 11;
}

interface IntlVariationsGender {
GENDER_MALE: 1;
GENDER_FEMALE: 2;
GENDER_UNKNOWN: 3;
}

// https://github.com/facebookincubator/fbt/blob/e9c591f451dbfc91852e316869ae39ad41848c55/runtime/nonfb/IntlVariations.js#L9-L21
interface IntlVariations extends IntlVariationsGender {
BITMASK_NUMBER: 28;
BITMASK_GENDER: 3;
NUMBER_ZERO: 16;
NUMBER_ONE: 4;
NUMBER_TWO: 8;
NUMBER_FEW: 20;
NUMBER_MANY: 12;
NUMBER_OTHER: 24;
}

// https://github.com/facebookincubator/fbt/blob/c2d363a40b622d5aaf80ff1d249b38604fd869f6/transform/babel-plugin-fbt/FbtConstants.js#L22-L27
type PronounType = 'object' | 'possessive' | 'reflexive' | 'subject';

type IntlVariationsGenderValues = $Values<IntlVariationsGender>;

interface IntlViewerContext {
GENDER: IntlVariationsGenderValues;
locale: string;
}

type IntlVariationsValues = $Values<IntlVariations>;
type GenderConstValues = $Values<GenderConst>;

type Translations = { [locale: string]: { [key: string]: string } };

interface Options {
author?: string;
common?: boolean;
doNotExtract?: boolean;
preserveWhitespace?: boolean;
project?: string;
subject?: IntlVariationsGenderValues;
}

interface PluralOptions {
many?: string;
showCount?: 'yes' | 'no' | 'ifMany';
name?: string;
value?: any;
}

interface ParamOptions {
gender?: IntlVariationsGenderValues;
number?: number | true;
}

interface PronounOptions {
human?: boolean;
capitalize?: boolean;
}

type FbtResult = string;
}

declare namespace JSX {
interface IntrinsicElements {
fbt: {
desc: string;
children: React.ReactNode | React.ReactNode[];
} & FBT.Options;
}
}
76 changes: 76 additions & 0 deletions @types/fbt/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/// <reference path="globals.d.ts" />

declare module 'fbt' {
export const GenderConst: FBT.GenderConst;
export const IntlVariations: FBT.IntlVariations;
export const IntlViewerContext: FBT.IntlViewerContext;

// These exports (Fbt*) isn't real! It is only syntax abstraction
// https://github.com/facebookincubator/fbt/blob/8607c1f2798ef18c6142a2cf1c5a9351c6d7df69/transform/babel-plugin-fbt/FbtUtil.js#L28-L40
export const FbtEnum: React.FC<{
'enum-range': Array<string> | { [enumKey: string]: string };
value: string;
}>;
export const FbtParam: React.FC<
FBT.ParamOptions & { name: string; children: React.ReactNode }
>;
export const FbtPlural: React.FC<
FBT.PluralOptions & { count: number; children: string }
>;
export const FbtPronoun: React.FC<
FBT.PronounOptions & {
type: FBT.PronounType;
gender: FBT.GenderConstValues;
}
>;
export const FbtName: React.FC<
Omit<FBT.ParamOptions, 'gender'> & {
name: string;
gender: FBT.IntlVariationsGenderValues;
children: React.ReactNode;
}
>;
export const FbtSameParam: React.FC<{
name: string;
}>;

export interface Fbt {
(source: string, desc: string, options?: FBT.Options): FBT.FbtResult;

param(
paramName: string,
value: any,
options?: FBT.ParamOptions,
): FBT.FbtResult;
sameParam(paramName: string): FBT.FbtResult;
name(name: string, gender: FBT.IntlVariationsGenderValues): FBT.FbtResult;
plural(
singularPhrase: string,
count: number,
options?: FBT.PluralOptions,
): FBT.FbtResult;
enum<
Range extends { [enumKey: string]: string },
RangeKeys extends keyof Range
>(
enumKey: RangeKeys,
enumRange: Range,
): FBT.FbtResult;
enum(index: string, enumRange: Array<string>): FBT.FbtResult;
pronoun(
type: FBT.PronounType,
gender: FBT.GenderConstValues,
options: FBT.PronounOptions,
): FBT.FbtResult;
}

export const init: (options: { translations: FBT.Translations, hooks: { getViewerContext: () => any} }) => void;

export const fbt: Fbt;
}

declare namespace JSX {
interface IntrinsicElements {
fbt: any;
}
}
121 changes: 7 additions & 114 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,117 +198,10 @@ const component = (props): React.ReactElement => {
### Localization

We've defined Localization strings in `STRINGS.js` which is in root dir.
We used [react-native-localize](https://github.com/react-native-community/react-native-localize) pacakage for this one which use [i18n-js](https://github.com/fnando/i18n-js) together.

- How it is installed

```tsx
import * as Localization from 'react-native-localize';

import en from './assets/langs/en.json';
import i18n from 'i18n-js';
import ja from './assets/langs/ja.json';
import ko from './assets/langs/ko.json';

const locales = Localization.getLocales();

if (Array.isArray(locales)) {
i18n.locale = locales[0].languageTag;
}

i18n.fallbacks = true;
i18n.translations = { en, ko, ja };

export const getString = (param: string, mapObj?: Record<string, unknown>): string => {
if (mapObj) {
return i18n.t(param, mapObj);
}
return i18n.t(param);
};
```

- How it is used

> Import locales in `assets/langs`. Currently, `ko.json` and `en.json` is installed. If you want to add more languages you can add it in `STRINGS.ts` and `i18n.translations = { en, ko };` add more languages inside `i18n.translations`.
```ts
import { getString } from '../../../STRINGS';

getString('LOGIN');
```

- How it is mocked

> Fixed jest setup by adding following in `__mocks__/react-native-localize.ts`.
```ts
interface Locale {
countryCode: string;
languageTag: string;
languageCode: string;
isRTL: boolean;
}
const getLocales = (): Locale[] => [
{
countryCode: 'US',
languageTag: 'en-US',
languageCode: 'en',
isRTL: false,
},
{
countryCode: 'EC',
languageTag: 'es-EC',
languageCode: 'es',
isRTL: false,
},
];

const findBestAvailableLanguage = (): Partial<Locale> => ({
languageTag: 'es',
isRTL: false,
});

const getNumberFormatSettings = (): Record<string, unknown> => ({
decimalSeparator: '.',
groupingSeparator: ',',
});

const getCalendar = (): string => 'gregorian'; // or "japanese", "buddhist"
const getCountry = (): string => 'ES'; // the country code you want
const getCurrencies = (): [string] => ['USD']; // can be empty array
const getTemperatureUnit = (): string => 'celsius'; // or "fahrenheit"
const getTimeZone = (): string => 'Ecuador/Guayaquil'; // the timezone you want
const uses24HourClock = (): boolean => true;
const usesMetricSystem = (): boolean => true;

const addEventListener = jest.fn();
const removeEventListener = jest.fn();

export {
findBestAvailableLanguage,
getLocales,
getNumberFormatSettings,
getCalendar,
getCountry,
getCurrencies,
getTemperatureUnit,
getTimeZone,
uses24HourClock,
usesMetricSystem,
addEventListener,
removeEventListener,
};
```

### React version

16.9

### React Native version

0.61

### React navigation

4
Previously, we used `i18n-j` to localize our app and we decided to switch to [fbt](https://github.com/facebook/fbt). If you want to understand why, you may see our blog for [Localizing react app with FBT instead of i18n](https://medium.com/dooboolab/localizing-react-app-with-fbt-instead-of-i18n-90822e0cb373).

We've defined localized strings in `assets/translations/en.json` for English and `assets/translations/ko.json` for Korean. Since the `en` is default locale setup in current project, you do not need to localize this file. However, you still should not delete this if you don't want to see missing localization warning messages when you are running jest.

We are using [fbt](https://github.com/facebook/fbt) to localize our app which is maintained by Facebook team. Simply running `yarn fbt-all` will generate `i18n/fbt/translatedFbts.json` which has all the localized strings.

If you find trouble using it, you may follow [Integrate FBT into your React Native Application](https://medium.com/translate-your-react-native-application-with/integrate-fbt-into-your-react-native-application-2bac420e8e0c).
21 changes: 0 additions & 21 deletions STRINGS.ts

This file was deleted.

2 changes: 1 addition & 1 deletion android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|layoutDirection|uiMode|locale"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize">
<intent-filter>
Expand Down
Loading

0 comments on commit 083499d

Please sign in to comment.