Implement manual initialzation for Angular app

Manual initialization allows for some asynchronous work to
finish ahead of Angular's startup. The initial motivation is
to be able to guarantee translation files have been fetched
before rendering content that needs translation. If a locale
isn't supported or if the request to get a json file fails,
the i18n service falls back to en.

Signed-off-by: gconsidine <gconsidi@redhat.com>
This commit is contained in:
gconsidine 2017-09-27 15:04:09 -04:00
parent bd5e33c2f4
commit 297904462d
17 changed files with 253 additions and 159 deletions

View File

@ -17,3 +17,4 @@ test
!client/lib/models/**/*.js
!client/lib/services/**/*.js
!client/features/**/*.js
!client/src/app.start.js

View File

@ -20,7 +20,7 @@ module.exports = {
node: true
},
globals: {
angular: true,
angular: true,
d3: true,
$: true,
_: true,
@ -43,6 +43,7 @@ module.exports = {
'no-param-reassign': 'off',
'no-plusplus': 'off',
'no-underscore-dangle': 'off',
'no-use-before-define': 'off',
'object-curly-newline': 'off',
'space-before-function-paren': ['error', 'always']
}

View File

@ -109,7 +109,7 @@ const base = {
jsonlint: 'codemirror.jsonlint'
}),
new ExtractTextPlugin('css/[name].[chunkhash].css'),
new CleanWebpackPlugin([STATIC_PATH, COVERAGE_PATH, LANGUAGES_PATH], {
new CleanWebpackPlugin([STATIC_PATH, COVERAGE_PATH], {
root: UI_PATH,
verbose: false
}),

View File

@ -1,5 +1,3 @@
const path = require('path');
const _ = require('lodash');
const base = require('./webpack.base');

View File

@ -1,13 +1,12 @@
const path = require('path');
const _ = require('lodash');
const webpack = require('webpack');
const STATIC_URL = '/static/';
const development = require('./webpack.development');
const development = require('./webpack.base');
const test = {
devtool: 'cheap-source-map',
plugins: [
new webpack.DefinePlugin({
$basePath: STATIC_URL

View File

@ -2,8 +2,8 @@ const path = require('path');
const _ = require('lodash');
const webpack = require('webpack');
const HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin');
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
const HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin');
const TARGET_PORT = _.get(process.env, 'npm_package_config_django_port', 8043);
const TARGET_HOST = _.get(process.env, 'npm_package_config_django_host', 'https://localhost');
@ -29,6 +29,7 @@ const watch = {
]
},
plugins: [
new HtmlWebpackHarddiskPlugin(),
new HardSourceWebpackPlugin({
cacheDirectory: 'node_modules/.cache/hard-source/[confighash]',
recordsPath: 'node_modules/.cache/hard-source/[confighash]/records.json',
@ -41,7 +42,6 @@ const watch = {
files: ['package.json']
}
}),
new HtmlWebpackHarddiskPlugin(),
new webpack.HotModuleReplacementPlugin()
],
devServer: {

View File

@ -3,6 +3,8 @@ import AddController from './add-credentials.controller';
import EditController from './edit-credentials.controller';
import CredentialsStrings from './credentials.strings';
const MODULE_NAME = 'at.features.credentials';
const addEditTemplate = require('~features/credentials/add-edit-credentials.view.html');
function CredentialsResolve ($q, $stateParams, Me, Credential, CredentialType, Organization) {
@ -51,12 +53,8 @@ CredentialsResolve.$inject = [
'OrganizationModel'
];
function CredentialsConfig ($stateExtenderProvider, legacyProvider, stringProvider) {
const stateExtender = $stateExtenderProvider.$get();
const legacy = legacyProvider.$get();
const strings = stringProvider.$get();
stateExtender.addState({
function CredentialsRun ($stateExtender, legacy, strings) {
$stateExtender.addState({
name: 'credentials.add',
route: '/add',
ncyBreadcrumb: {
@ -78,7 +76,7 @@ function CredentialsConfig ($stateExtenderProvider, legacyProvider, stringProvid
}
});
stateExtender.addState({
$stateExtender.addState({
name: 'credentials.edit',
route: '/:credential_id',
ncyBreadcrumb: {
@ -101,25 +99,27 @@ function CredentialsConfig ($stateExtenderProvider, legacyProvider, stringProvid
}
});
stateExtender.addState(legacy.getStateConfiguration('list'));
stateExtender.addState(legacy.getStateConfiguration('edit-permissions'));
stateExtender.addState(legacy.getStateConfiguration('add-permissions'));
stateExtender.addState(legacy.getStateConfiguration('add-organization'));
stateExtender.addState(legacy.getStateConfiguration('edit-organization'));
stateExtender.addState(legacy.getStateConfiguration('add-credential-type'));
stateExtender.addState(legacy.getStateConfiguration('edit-credential-type'));
$stateExtender.addState(legacy.getStateConfiguration('list'));
$stateExtender.addState(legacy.getStateConfiguration('edit-permissions'));
$stateExtender.addState(legacy.getStateConfiguration('add-permissions'));
$stateExtender.addState(legacy.getStateConfiguration('add-organization'));
$stateExtender.addState(legacy.getStateConfiguration('edit-organization'));
$stateExtender.addState(legacy.getStateConfiguration('add-credential-type'));
$stateExtender.addState(legacy.getStateConfiguration('edit-credential-type'));
}
CredentialsConfig.$inject = [
'$stateExtenderProvider',
'LegacyCredentialsServiceProvider',
'CredentialsStringsProvider'
CredentialsRun.$inject = [
'$stateExtender',
'LegacyCredentialsService',
'CredentialsStrings'
];
angular
.module('at.features.credentials', [])
.config(CredentialsConfig)
.module(MODULE_NAME, [])
.controller('AddController', AddController)
.controller('EditController', EditController)
.service('LegacyCredentialsService', LegacyCredentials)
.service('CredentialsStrings', CredentialsStrings);
.service('CredentialsStrings', CredentialsStrings)
.run(CredentialsRun);
export default MODULE_NAME;

View File

@ -1,5 +1,16 @@
import '~features/credentials';
import atLibServices from '~services';
import atLibComponents from '~components';
import atLibModels from '~models';
angular.module('at.features', [
'at.features.credentials'
import atFeaturesCredentials from '~features/credentials';
const MODULE_NAME = 'at.features';
angular.module(MODULE_NAME, [
atLibServices,
atLibComponents,
atLibModels,
atFeaturesCredentials
]);
export default MODULE_NAME;

View File

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en" ng-app="awApp">
<html>
<head>
<meta charset="utf-8">

View File

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en" ng-app="awApp">
<html>
<head>
{% load staticfiles %}
<meta charset="utf-8">

View File

@ -1,3 +1,5 @@
import atLibServices from '~services';
import actionGroup from '~components/action/action-group.directive';
import divider from '~components/utility/divider.directive';
import form from '~components/form/form.directive';
@ -28,8 +30,12 @@ import truncate from '~components/truncate/truncate.directive';
import BaseInputController from '~components/input/base.controller';
import ComponentsStrings from '~components/components.strings';
const MODULE_NAME = 'at.lib.components';
angular
.module('at.lib.components', [])
.module(MODULE_NAME, [
atLibServices
])
.directive('atActionGroup', actionGroup)
.directive('atDivider', divider)
.directive('atForm', form)
@ -58,3 +64,5 @@ angular
.directive('atTruncate', truncate)
.service('BaseInputController', BaseInputController)
.service('ComponentsStrings', ComponentsStrings);
export default MODULE_NAME;

View File

@ -1,3 +1,5 @@
import atLibServices from '~services';
import Base from '~models/Base';
import Config from '~models/Config';
import Credential from '~models/Credential';
@ -5,11 +7,17 @@ import CredentialType from '~models/CredentialType';
import Me from '~models/Me';
import Organization from '~models/Organization';
const MODULE_NAME = 'at.lib.models';
angular
.module('at.lib.models', [])
.module(MODULE_NAME, [
atLibServices
])
.service('BaseModel', Base)
.service('ConfigModel', Config)
.service('CredentialModel', Credential)
.service('CredentialTypeModel', CredentialType)
.service('MeModel', Me)
.service('OrganizationModel', Organization);
export default MODULE_NAME;

View File

@ -1,11 +1,17 @@
import AppStrings from '~services/app.strings';
import BaseStringService from '~services/base-string.service';
import CacheService from '~services/cache.service';
import EventService from '~services/event.service';
import BaseStringService from '~services/base-string.service';
import AppStrings from '~services/app.strings';
const MODULE_NAME = 'at.lib.services';
angular
.module('at.lib.services', [])
.module(MODULE_NAME, [
'I18N'
])
.service('AppStrings', AppStrings)
.service('BaseStringService', BaseStringService)
.service('CacheService', CacheService)
.service('EventService', EventService);
export default MODULE_NAME;

View File

@ -1,7 +1,7 @@
// Configuration dependencies
global.$AnsibleConfig = null;
// Provided via Webpack DefinePlugin in webpack.config.js
global.$ENV = {} ;
global.$ENV = {};
// ui-router debugging
if ($ENV['route-debug']){
let trace = angular.module('ui.router').trace;
@ -14,7 +14,8 @@ if ($basePath) {
urlPrefix = `${$basePath}`;
}
// Modules
import start from './app.start';
import portalMode from './portal-mode/main';
import systemTracking from './system-tracking/main';
import inventoriesHosts from './inventories-hosts/main';
@ -46,71 +47,76 @@ import access from './access/main';
import scheduler from './scheduler/main';
import instanceGroups from './instance-groups/main';
import '../lib/components';
import '../lib/models';
import '../lib/services';
import '../features';
import atFeatures from '~features';
import atLibComponents from '~components';
import atLibModels from '~models';
import atLibServices from '~services';
angular.module('awApp', [
'I18N',
'AngularCodeMirrorModule',
'angular-duration-format',
'angularMoment',
'AngularScheduler',
'angular-md5',
'dndLists',
'ncy-angular-breadcrumb',
'ngSanitize',
'ngCookies',
'ngToast',
'gettext',
'Timezones',
'ui.router',
'ui.router.state.events',
'lrInfiniteScroll',
start.bootstrap(() => {
angular.bootstrap(document.body, ['awApp']);
});
about.name,
access.name,
license.name,
RestServices.name,
browserData.name,
configuration.name,
systemTracking.name,
inventoriesHosts.name,
inventoryScripts.name,
credentials.name,
credentialTypes.name,
organizations.name,
managementJobs.name,
breadCrumb.name,
home.name,
login.name,
activityStream.name,
workflowResults.name,
jobResults.name,
jobSubmission.name,
notifications.name,
standardOut.name,
Templates.name,
portalMode.name,
jobs.name,
teams.name,
users.name,
projects.name,
scheduler.name,
instanceGroups.name,
angular
.module('awApp', [
'I18N',
'AngularCodeMirrorModule',
'angular-duration-format',
'angularMoment',
'AngularScheduler',
'angular-md5',
'dndLists',
'ncy-angular-breadcrumb',
'ngSanitize',
'ngCookies',
'ngToast',
'gettext',
'Timezones',
'ui.router',
'ui.router.state.events',
'lrInfiniteScroll',
'Utilities',
'templates',
'PromptDialog',
'AWDirectives',
'features',
about.name,
access.name,
license.name,
RestServices.name,
browserData.name,
configuration.name,
systemTracking.name,
inventoriesHosts.name,
inventoryScripts.name,
credentials.name,
credentialTypes.name,
organizations.name,
managementJobs.name,
breadCrumb.name,
home.name,
login.name,
activityStream.name,
workflowResults.name,
jobResults.name,
jobSubmission.name,
notifications.name,
standardOut.name,
Templates.name,
portalMode.name,
jobs.name,
teams.name,
users.name,
projects.name,
scheduler.name,
instanceGroups.name,
'at.lib.components',
'at.lib.models',
'at.lib.services',
'at.features',
])
'Utilities',
'templates',
'PromptDialog',
'AWDirectives',
'features',
atFeatures,
atLibComponents,
atLibModels,
atLibServices
])
.constant('AngularScheduler.partials', urlPrefix + 'lib/angular-scheduler/lib/')
.constant('AngularScheduler.useTimezone', true)
.constant('AngularScheduler.showUTCField', true)
@ -171,13 +177,13 @@ angular.module('awApp', [
'CheckLicense', '$location', 'Authorization', 'LoadBasePaths', 'Timer',
'LoadConfig', 'Store', 'pendoService', 'Prompt', 'Rest',
'Wait', 'ProcessErrors', '$state', 'GetBasePath', 'ConfigService',
'FeaturesService', '$filter', 'SocketService', 'AppStrings', 'I18NInit',
'FeaturesService', '$filter', 'SocketService', 'AppStrings',
function($stateExtender, $q, $compile, $cookies, $rootScope, $log, $stateParams,
CheckLicense, $location, Authorization, LoadBasePaths, Timer,
LoadConfig, Store, pendoService, Prompt, Rest, Wait,
ProcessErrors, $state, GetBasePath, ConfigService, FeaturesService,
$filter, SocketService, AppStrings, I18NInit) {
I18NInit();
$filter, SocketService, AppStrings) {
$rootScope.$state = $state;
$rootScope.$state.matches = function(stateName) {
return $state.current.name.search(stateName) > 0;

View File

@ -0,0 +1,72 @@
const SUPPORTED_LOCALES = ['en', 'es', 'fr', 'ja', 'nl'];
const DEFAULT_LOCALE = 'en';
const BASE_PATH = global.$basePath ? `${global.$basePath}languages/` : '/static/languages/';
/**
* The Angular app is manually initialized in order to complete some
* asynchronous work up front. This function returns a callback so app.js can
* call `angular.bootstrap` when the work is complete.
*
* @argument {function} - Callback.
*/
function bootstrap (callback) {
fetchLocaleStrings((locale) => {
if (locale) {
angular.module('I18N').constant('LOCALE', locale);
}
angular.element(document).ready(() => callback());
});
}
/**
* GET the localized JSON strings file or fall back to the default language
* if the locale isn't supported or if the request fails.
*
* @argument {function} - Callback.
*
* @returns {object=} - Locale data if it exists.
*/
function fetchLocaleStrings (callback) {
const code = normalizeLocaleCode(navigator.language || navigator.userLanguage);
if (isDefaultLocale(code) || !isSupportedLocale(code)) {
callback({ code });
return;
}
const request = $.ajax(`${BASE_PATH}${code}.json`);
request.done(res => {
if (res[code]) {
callback({ code, strings: res[code] });
} else {
callback({ code: DEFAULT_LOCALE });
}
});
request.fail(() => callback({ code: DEFAULT_LOCALE }));
}
function normalizeLocaleCode (code) {
try {
code = code.split('-')[0].toLowerCase();
} catch (error) {
code = DEFAULT_LOCALE;
}
return code;
}
function isSupportedLocale (code) {
return SUPPORTED_LOCALES.includes(code);
}
function isDefaultLocale (code) {
return code === DEFAULT_LOCALE;
}
export default {
bootstrap
};

View File

@ -1,52 +1,36 @@
/* jshint ignore:start */
import { sprintf } from 'sprintf-js';
var sprintf = require('sprintf-js').sprintf;
let defaultLanguage = 'en_US';
function I18n (gettextCatalog) {
return {
N_,
sprintf,
_: s => gettextCatalog.getString(s),
translate: (singular, context) => gettextCatalog.getString(singular, context),
translatePlural: (count, singular, plural, context) => {
return gettextCatalog.getPlural(count, singular, plural, context);
},
hasTranslation: () => gettextCatalog.strings[gettextCatalog.currentLanguage] !== undefined
};
}
I18n.$inject = ['gettextCatalog'];
function run (LOCALE, gettextCatalog) {
if (LOCALE.code && LOCALE.strings) {
gettextCatalog.setCurrentLanguage(LOCALE.code);
gettextCatalog.setStrings(LOCALE.code, LOCALE.strings);
}
}
run.$inject = ['LOCALE', 'gettextCatalog'];
/**
* @ngdoc method
* @name function:i18n#N_
* @methodOf function:N_
* @description this function marks the translatable string with N_
* for 'grunt nggettext_extract'
*
*/
export function N_(s) {
return s;
}
export default
angular.module('I18N', [])
.factory('I18NInit', ['$window', 'gettextCatalog',
function ($window, gettextCatalog) {
return function() {
var langInfo = ($window.navigator.languages || [])[0] ||
$window.navigator.language ||
$window.navigator.userLanguage ||
'';
var langUrl = langInfo.replace('-', '_');
if (langUrl === defaultLanguage) {
return;
}
// gettextCatalog.debug = true;
gettextCatalog.setCurrentLanguage(langInfo);
gettextCatalog.loadRemote('/static/languages/' + langUrl + '.json');
};
}])
.factory('i18n', ['gettextCatalog',
function (gettextCatalog) {
return {
_: function (s) { return gettextCatalog.getString (s); },
N_: N_,
translate: (singular, context) => gettextCatalog.getString(singular, context),
translatePlural: (count, singular, plural, context) => {
return gettextCatalog.getPlural(count, singular, plural, context);
},
sprintf: sprintf,
hasTranslation: function () {
return gettextCatalog.strings[gettextCatalog.currentLanguage] !== undefined;
}
};
}]);
export default angular
.module('I18N', [
'gettext'
])
.factory('i18n', I18n)
.run(run);

View File

@ -16,16 +16,16 @@ module.exports = function(config) {
reporters: ['progress', 'coverage', 'junit'],
files:[
'./client/src/vendor.js',
'./client/src/app.js',
'./node_modules/angular-mocks/angular-mocks.js',
{ pattern: './tests/**/*-test.js' },
'client/src/**/*.html'
'./client/src/app.js',
'./tests/**/*-test.js',
'./client/src/**/*.html'
],
preprocessors: {
'./client/src/vendor.js': ['webpack', 'sourcemap'],
'./client/src/app.js': ['webpack', 'sourcemap'],
'./tests/**/*-test.js': ['webpack', 'sourcemap'],
'client/src/**/*.html': ['html2js']
'./client/src/vendor.js': ['webpack'],
'./client/src/app.js': ['webpack'],
'./tests/**/*-test.js': ['webpack'],
'./client/src/**/*.html': ['html2js']
},
webpack: webpackTestConfig,
webpackMiddleware: {