Skip to content

Commit

Permalink
use i18next instead of angular translate
Browse files Browse the repository at this point in the history
We rely heavily on Angular for internationalization, both directly in the HTML and in the JS behind the scenes. However, it's specific to Angular and we can't use it in React components.

i18next is compatible with React / React Native Web and we will be able to use it in both frameworks at the same time.
The i18n JSON files themselves can keep the same exact structure. Only a few strings were changed (the 'equals X cookies, equals X ice cream', etc) - i18next has a different mechanism for pluralization and we don't have to use ICU for only 3 strings.
  • Loading branch information
JGreenlee committed May 26, 2023
1 parent 4ca207d commit 14e8a67
Show file tree
Hide file tree
Showing 58 changed files with 442 additions and 410 deletions.
4 changes: 3 additions & 1 deletion package.serve.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,13 @@
"angular-sanitize": "1.6.7",
"angular-simple-logger": "^0.1.7",
"angular-translate": "^2.18.1",
"angular-translate-interpolation-messageformat": "^2.18.1",
"angular-translate-loader-static-files": "^2.18.1",
"angular-ui-router": "0.2.13",
"animate.css": "^3.5.2",
"bottleneck": "^2.19.5",
"core-js": "^2.5.7",
"fs-extra": "^9.0.1",
"i18next": "^22.5.0",
"install": "^0.13.0",
"ionic-datepicker": "1.2.1",
"ionic-toast": "^0.4.1",
Expand All @@ -63,8 +63,10 @@
"leaflet": "^0.7.7",
"leaflet-plugins": "^3.0.0",
"leaflet.awesome-markers": "^2.0.5",
"messageformat": "^2.3.0",
"moment": "^2.29.4",
"moment-timezone": "^0.5.43",
"ng-i18next": "^1.0.7",
"npm": "^9.6.3",
"nvd3": "^1.8.6",
"prop-types": "^15.8.1",
Expand Down
10 changes: 6 additions & 4 deletions www/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -243,17 +243,19 @@
"average": "Average for group:",
"avoided": "CO₂ avoided (vs. all 'taxi'):",
"label-to-squish": "Label trips to collapse the range into a single number",
"equals-phone-charges": "Saved {charges, plural, =0{at least 0 smartphone charges} one {at least 1 smartphone charge} other {at least # smartphone charges}} vs. all 'taxi'",
"lastweek": "My last week value:",
"us-2030-goal": "US 2030 Goal Estimate:",
"us-2050-goal": "US 2050 Goal Estimate:",
"calories": "My Calories",
"calibrate": "Calibrate",
"no-summary-data": "No summary data",
"mean-speed": "My Average Speed",
"equals-cookies": "Equals {cookies, plural, =0{at least 0 homemade chocolate chip cookies} one {at least 1 homemade chocolate chip cookie} other {at least # homemade chocolate chip cookies}}",
"equals-icecream": "Equals {icecream, plural, =0{at least 0 half cups vanilla ice cream} one {at least 1 half cup vanilla ice cream} other {at least # half cups vanilla ice cream}}",
"equals-bananas": "Equals {bananas, plural, =0{at least 0 bananas} one {at least 1 banana} other {at least # bananas}}"
"equals-cookies_one": "Equals at least {{count}} homemade chocolate chip cookie",
"equals-cookies_other": "Equals at least {{count}} homemade chocolate chip cookies",
"equals-icecream_one": "Equals at least {{count}} half cup vanilla ice cream",
"equals-icecream_other": "Equals at least {{count}} half cups vanilla ice cream",
"equals-bananas_one": "Equals at least {{count}} banana",
"equals-bananas_other": "Equals at least {{count}} bananas"
},

"main-diary" : "Diary",
Expand Down
72 changes: 49 additions & 23 deletions www/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,61 @@ import 'angular-translate-loader-static-files';
import 'moment';
import 'moment-timezone';

import i18next from 'i18next';

import 'ionic-toast';
import 'ionic-datepicker';
import 'angular-simple-logger';

import '../manual_lib/ionic/js/ionic.js';
import '../manual_lib/ionic/js/ionic-angular.js';

angular.module('emission', ['ionic',

import en from '../i18n/en.json';
import es from '../../locales/es/i18n/es.json';
import fr from '../../locales/fr/i18n/fr.json';
import it from '../../locales/it/i18n/it.json';
const langs = { en, es, fr, it };

let resources = {};
for (const [lang, json] of Object.entries(langs)) {
resources[lang] = { translation: json }
}

const locales = !navigator?.length ? [navigator.language] : navigator.languages;

let detectedLang;
locales.forEach(locale => {
const lang = locale.trim().split(/-|_/)[0];
if (Object.keys(langs).includes(lang)) {
detectedLang = lang;
}
});
console.debug(`Detected language: ${detectedLang}`);
console.debug('Resources:', resources);
i18next.init({
debug: true,
resources,
lng: detectedLang,
fallbackLng: 'en'
});

console.debug('currlang:', i18next.resolvedLanguage);

i18next.changeLanguage(detectedLang, (err, t) => {
if (err) {
console.error(err);
} else {
console.debug('i18next language changed to:', i18next.resolvedLanguage);
console.debug('t diary.draft:', t('diary.draft'));
console.debug('i18next.t diary.draft:', i18next.t('diary.draft'));
}
});

window.i18next = i18next;
import 'ng-i18next';

angular.module('emission', ['ionic', 'jm.i18next',
'emission.controllers','emission.services', 'emission.plugin.logger',
'emission.splash.customURLScheme', 'emission.splash.referral',
'emission.services.email',
Expand Down Expand Up @@ -75,7 +122,7 @@ angular.module('emission', ['ionic',
console.log("Ending run");
})

.config(function($stateProvider, $urlRouterProvider, $translateProvider, $compileProvider) {
.config(function($stateProvider, $urlRouterProvider, $compileProvider) {
console.log("Starting config");
// alert("config");

Expand Down Expand Up @@ -119,27 +166,6 @@ angular.module('emission', ['ionic',
// alert("about to fall back to otherwise");
// if none of the above states are matched, use this as the fallback
$urlRouterProvider.otherwise('/splash');

// Allow the use of MessageForm interpolation for Gender and Plural.
// $translateProvider.addInterpolation('$translateMessageFormatInterpolation')
// .useSanitizeValueStrategy('escape');


// Define where we can find the .json and the fallback language
$translateProvider
.fallbackLanguage('en')
.registerAvailableLanguageKeys(['en', 'fr', 'it', 'es'], {
'en_*': 'en',
'fr_*': 'fr',
'it_*': 'it',
'es_*': 'es',
'*': 'en'
})
.determinePreferredLanguage()
.useStaticFilesLoader({
prefix: 'i18n/',
suffix: '.json'
});

console.log("Ending config");
});
48 changes: 24 additions & 24 deletions www/js/appstatus/permissioncheck.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ angular.module('emission.appstatus.permissioncheck',
};
}).
controller("PermissionCheckControl", function($scope, $element, $attrs,
$ionicPlatform, $ionicPopup, $window, $translate) {
$ionicPlatform, $ionicPopup, $window) {
console.log("PermissionCheckControl initialized with status "+$scope.overallstatus);

$scope.setupLocChecks = function(platform, version) {
Expand Down Expand Up @@ -197,15 +197,15 @@ controller("PermissionCheckControl", function($scope, $element, $attrs,
console.log("description tags are "+androidSettingsDescTag+" "+androidPermDescTag);
// location settings
let locSettingsCheck = {
name: $translate.instant("intro.appstatus.locsettings.name"),
desc: $translate.instant(androidSettingsDescTag),
name: i18next.t("intro.appstatus.locsettings.name"),
desc: i18next.t(androidSettingsDescTag),
statusState: false,
fix: fixSettings,
refresh: checkSettings
}
let locPermissionsCheck = {
name: $translate.instant("intro.appstatus.locperms.name"),
desc: $translate.instant(androidPermDescTag),
name: i18next.t("intro.appstatus.locperms.name"),
desc: i18next.t(androidPermDescTag),
statusState: false,
fix: fixPerms,
refresh: checkPerms
Expand Down Expand Up @@ -243,15 +243,15 @@ controller("PermissionCheckControl", function($scope, $element, $attrs,
console.log("description tags are "+iOSSettingsDescTag+" "+iOSPermDescTag);
// location settings
let locSettingsCheck = {
name: $translate.instant("intro.appstatus.locsettings.name"),
desc: $translate.instant(iOSSettingsDescTag),
name: i18next.t("intro.appstatus.locsettings.name"),
desc: i18next.t(iOSSettingsDescTag),
statusState: false,
fix: fixSettings,
refresh: checkSettings
}
let locPermissionsCheck = {
name: $translate.instant("intro.appstatus.locperms.name"),
desc: $translate.instant(iOSPermDescTag),
name: i18next.t("intro.appstatus.locperms.name"),
desc: i18next.t(iOSPermDescTag),
statusState: false,
fix: fixPerms,
refresh: checkPerms
Expand All @@ -275,12 +275,12 @@ controller("PermissionCheckControl", function($scope, $element, $attrs,
};

let fitnessPermissionsCheck = {
name: $translate.instant("intro.appstatus.fitnessperms.name"),
desc: $translate.instant("intro.appstatus.fitnessperms.description.android"),
name: i18next.t("intro.appstatus.fitnessperms.name"),
desc: i18next.t("intro.appstatus.fitnessperms.description.android"),
fix: fixPerms,
refresh: checkPerms
}
$scope.overallFitnessName = $translate.instant("intro.appstatus.overall-fitness-name-android");
$scope.overallFitnessName = i18next.t("intro.appstatus.overall-fitness-name-android");
$scope.fitnessChecks = [fitnessPermissionsCheck];
refreshChecks($scope.fitnessChecks, $scope.recomputeFitnessStatus);
}
Expand All @@ -300,12 +300,12 @@ controller("PermissionCheckControl", function($scope, $element, $attrs,
};

let fitnessPermissionsCheck = {
name: $translate.instant("intro.appstatus.fitnessperms.name"),
desc: $translate.instant("intro.appstatus.fitnessperms.description.ios"),
name: i18next.t("intro.appstatus.fitnessperms.name"),
desc: i18next.t("intro.appstatus.fitnessperms.description.ios"),
fix: fixPerms,
refresh: checkPerms
}
$scope.overallFitnessName = $translate.instant("intro.appstatus.overall-fitness-name-ios");
$scope.overallFitnessName = i18next.t("intro.appstatus.overall-fitness-name-ios");
$scope.fitnessChecks = [fitnessPermissionsCheck];
refreshChecks($scope.fitnessChecks, $scope.recomputeFitnessStatus);
}
Expand All @@ -322,8 +322,8 @@ controller("PermissionCheckControl", function($scope, $element, $attrs,
$scope.recomputeNotificationStatus, false);
};
let appAndChannelNotificationsCheck = {
name: $translate.instant("intro.appstatus.notificationperms.app-enabled-name"),
desc: $translate.instant("intro.appstatus.notificationperms.description.android-enable"),
name: i18next.t("intro.appstatus.notificationperms.app-enabled-name"),
desc: i18next.t("intro.appstatus.notificationperms.description.android-enable"),
fix: fixPerms,
refresh: checkPerms
}
Expand Down Expand Up @@ -357,14 +357,14 @@ controller("PermissionCheckControl", function($scope, $element, $attrs,
androidUnusedDescTag= "intro.appstatus.unusedapprestrict.description.android-disable-lt-12";
}
let unusedAppsUnrestrictedCheck = {
name: $translate.instant("intro.appstatus.unusedapprestrict.name"),
desc: $translate.instant(androidUnusedDescTag),
name: i18next.t("intro.appstatus.unusedapprestrict.name"),
desc: i18next.t(androidUnusedDescTag),
fix: fixPerms,
refresh: checkPerms
}
let ignoreBatteryOptCheck = {
name: $translate.instant("intro.appstatus.ignorebatteryopt.name"),
desc: $translate.instant("intro.appstatus.ignorebatteryopt.description.android-disable"),
name: i18next.t("intro.appstatus.ignorebatteryopt.name"),
desc: i18next.t("intro.appstatus.ignorebatteryopt.description.android-disable"),
fix: fixBatteryOpt,
refresh: checkBatteryOpt
}
Expand All @@ -375,16 +375,16 @@ controller("PermissionCheckControl", function($scope, $element, $attrs,
$scope.setupPermissionText = function() {
if($scope.platform.toLowerCase() == "ios") {
if($scope.osver < 13) {
$scope.locationPermExplanation = $translate.instant("intro.permissions.locationPermExplanation-ios-lt-13");
$scope.locationPermExplanation = i18next.t("intro.permissions.locationPermExplanation-ios-lt-13");
} else {
$scope.locationPermExplanation = $translate.instant("intro.permissions.locationPermExplanation-ios-gte-13");
$scope.locationPermExplanation = i18next.t("intro.permissions.locationPermExplanation-ios-gte-13");
}
}

$scope.backgroundRestricted = false;
if($window.device.manufacturer.toLowerCase() == "samsung") {
$scope.backgroundRestricted = true;
$scope.allowBackgroundInstructions = $translate.instant("intro.allow_background.samsung");
$scope.allowBackgroundInstructions = i18next.t("intro.allow_background.samsung");
}

console.log("Explanation = "+$scope.locationPermExplanation);
Expand Down
22 changes: 11 additions & 11 deletions www/js/config/dynamic_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import angular from 'angular';

angular.module('emission.config.dynamic', ['emission.plugin.logger'])
.factory('DynamicConfig', function($http, $ionicPlatform,
$window, $state, $rootScope, $timeout, Logger, $translate) {
$window, $state, $rootScope, $timeout, Logger) {
// also used in the startprefs class
// but without importing this
const CONFIG_PHONE_UI="config/app_ui_config";
Expand Down Expand Up @@ -73,7 +73,7 @@ angular.module('emission.config.dynamic', ['emission.plugin.logger'])
return savedConfig;
}
})
.catch((err) => Logger.displayError($translate.instant('config.unable-read-saved-config'), err));
.catch((err) => Logger.displayError(i18next.t('config.unable-read-saved-config'), err));
}

/**
Expand Down Expand Up @@ -107,7 +107,7 @@ angular.module('emission.config.dynamic', ['emission.plugin.logger'])
if (thenGoToIntro) $state.go("root.intro")
})
.then(() => true)
.catch((storeError) => Logger.displayError($translate.instant('config.unable-to-store-config'), storeError));
.catch((storeError) => Logger.displayError(i18next.t('config.unable-to-store-config'), storeError));
});
}

Expand Down Expand Up @@ -181,10 +181,10 @@ angular.module('emission.config.dynamic', ['emission.plugin.logger'])
const tokenParts = token.split("_");
if (tokenParts.length < 3) {
// all tokens must have at least nrelop_[study name]_...
throw new Error($translate.instant('config.not-enough-parts-old-style', {"token": token}));
throw new Error(i18next.t('config.not-enough-parts-old-style', {"token": token}));
}
if (tokenParts[0] != "nrelop") {
throw new Error($translate.instant('config.no-nrelop-start', {token: token}));
throw new Error(i18next.t('config.no-nrelop-start', {token: token}));
}
return tokenParts[1];
}
Expand All @@ -194,20 +194,20 @@ angular.module('emission.config.dynamic', ['emission.plugin.logger'])
// new style study, expects token with sub-group
const tokenParts = token.split("_");
if (tokenParts.length <= 3) { // no subpart defined
throw new Error($translate.instant('config.not-enough-parts', {token: token}));
throw new Error(i18next.t('config.not-enough-parts', {token: token}));
}
if (config.opcode.subgroups) {
if (config.opcode.subgroups.indexOf(tokenParts[2]) == -1) {
// subpart not in config list
throw new Error($translate.instant('config.invalid-subgroup', {token: token, subgroup: tokenParts[2], config_subgroups: config.opcode.subgroups}));
throw new Error(i18next.t('config.invalid-subgroup', {token: token, subgroup: tokenParts[2], config_subgroups: config.opcode.subgroups}));
} else {
console.log("subgroup "+tokenParts[2]+" found in list "+config.opcode.subgroups);
return tokenParts[2];
}
} else {
if (tokenParts[2] != "default") {
// subpart not in config list
throw new Error($translate.instant('config.invalid-subgroup', {token: token}));
throw new Error(i18next.t('config.invalid-subgroup', {token: token}));
} else {
console.log("no subgroups in config, 'default' subgroup found in token ");
return tokenParts[2];
Expand Down Expand Up @@ -246,10 +246,10 @@ angular.module('emission.config.dynamic', ['emission.plugin.logger'])
// on successful download, cache the token in the rootScope
.then((wasUpdated) => {$rootScope.scannedToken = dc.scannedToken})
.catch((fetchErr) => {
Logger.displayError($translate.instant('config.unable-download-config'), fetchErr);
Logger.displayError(i18next.t('config.unable-download-config'), fetchErr);
});
} catch (error) {
Logger.displayError($translate.instant('config.invalid-opcode-format'), error);
Logger.displayError(i18next.t('config.invalid-opcode-format'), error);
return Promise.reject(error);
}
});
Expand Down Expand Up @@ -277,7 +277,7 @@ angular.module('emission.config.dynamic', ['emission.plugin.logger'])
$rootScope.$apply(() => dc.saveAndNotifyConfigReady(existingConfig));
}
}).catch((err) => {
Logger.displayError($translate('config.error-loading-config-app-start'), err)
Logger.displayError('Error loading config on app start', err)
});
};
$ionicPlatform.ready().then(function() {
Expand Down
Loading

0 comments on commit 14e8a67

Please sign in to comment.