diff --git a/awx/ui/client/features/credentials/credentials.strings.js b/awx/ui/client/features/credentials/credentials.strings.js index 16cd98a840..d8c031f743 100644 --- a/awx/ui/client/features/credentials/credentials.strings.js +++ b/awx/ui/client/features/credentials/credentials.strings.js @@ -5,27 +5,27 @@ function CredentialsStrings (BaseString) { let ns = this.credentials; ns.state = { - ADD_BREADCRUMB_LABEL: t('CREATE CREDENTIAL'), - EDIT_BREADCRUMB_LABEL: t('EDIT CREDENTIAL') + ADD_BREADCRUMB_LABEL: t.s('CREATE CREDENTIAL'), + EDIT_BREADCRUMB_LABEL: t.s('EDIT CREDENTIAL') }; ns.tab = { - DETAILS: t('Details'), - PERMISSIONS: t('Permissions') + DETAILS: t.s('Details'), + PERMISSIONS: t.s('Permissions') }; ns.inputs = { - GROUP_TITLE: t('Type Details'), - ORGANIZATION_PLACEHOLDER: t('SELECT AN ORGANIZATION'), - CREDENTIAL_TYPE_PLACEHOLDER: t('SELECT A CREDENTIAL TYPE') + GROUP_TITLE: t.s('Type Details'), + ORGANIZATION_PLACEHOLDER: t.s('SELECT AN ORGANIZATION'), + CREDENTIAL_TYPE_PLACEHOLDER: t.s('SELECT A CREDENTIAL TYPE') }; ns.add = { - PANEL_TITLE: t('NEW CREDENTIAL') + PANEL_TITLE: t.s('NEW CREDENTIAL') }; ns.permissions = { - TITLE: t('CREDENTIALS PERMISSIONS') + TITLE: t.s('CREDENTIALS PERMISSIONS') }; } diff --git a/awx/ui/client/lib/components/components.strings.js b/awx/ui/client/lib/components/components.strings.js index 2fba205de5..07346e966b 100644 --- a/awx/ui/client/lib/components/components.strings.js +++ b/awx/ui/client/lib/components/components.strings.js @@ -4,48 +4,48 @@ function ComponentsStrings (BaseString) { let t = this.t; let ns = this.components; - ns.REPLACE = t('REPLACE'); - ns.REVERT = t('REVERT'); - ns.ENCRYPTED = t('ENCRYPTED'); - ns.OPTIONS = t('OPTIONS'); - ns.SHOW = t('SHOW'); - ns.HIDE = t('HIDE'); + ns.REPLACE = t.s('REPLACE'); + ns.REVERT = t.s('REVERT'); + ns.ENCRYPTED = t.s('ENCRYPTED'); + ns.OPTIONS = t.s('OPTIONS'); + ns.SHOW = t.s('SHOW'); + ns.HIDE = t.s('HIDE'); ns.message = { - REQUIRED_INPUT_MISSING: t('Please enter a value.'), - INVALID_INPUT: t('Invalid input for this type.') + REQUIRED_INPUT_MISSING: t.s('Please enter a value.'), + INVALID_INPUT: t.s('Invalid input for this type.') }; ns.form = { - SUBMISSION_ERROR_TITLE: t('Unable to Submit'), - SUBMISSION_ERROR_MESSAGE:t('Unexpected server error. View the console for more information'), - SUBMISSION_ERROR_PREFACE: t('Unexpected Error') + SUBMISSION_ERROR_TITLE: t.s('Unable to Submit'), + SUBMISSION_ERROR_MESSAGE:t.s('Unexpected server error. View the console for more information'), + SUBMISSION_ERROR_PREFACE: t.s('Unexpected Error') }; ns.group = { - UNSUPPORTED_ERROR_PREFACE: t('Unsupported input type') + UNSUPPORTED_ERROR_PREFACE: t.s('Unsupported input type') }; ns.label = { - PROMPT_ON_LAUNCH: t('Prompt on launch') + PROMPT_ON_LAUNCH: t.s('Prompt on launch') }; ns.select = { - UNSUPPORTED_TYPE_ERROR: t('Unsupported display model type'), - EMPTY_PLACEHOLDER: t('NO OPTIONS AVAILABLE') + UNSUPPORTED_TYPE_ERROR: t.s('Unsupported display model type'), + EMPTY_PLACEHOLDER: t.s('NO OPTIONS AVAILABLE') }; ns.textarea = { - SSH_KEY_HINT: t('HINT: Drag and drop an SSH private key file on the field below.') + SSH_KEY_HINT: t.s('HINT: Drag and drop an SSH private key file on the field below.') }; ns.lookup = { - NOT_FOUND: t('That value was not found. Please enter or select a valid value.') + NOT_FOUND: t.s('That value was not found. Please enter or select a valid value.') }; ns.truncate = { - DEFAULT: t('Copy full revision to clipboard.'), - COPIED: t('Copied to clipboard.') + DEFAULT: t.s('Copy full revision to clipboard.'), + COPIED: t.s('Copied to clipboard.') } } diff --git a/awx/ui/client/lib/services/base-string.service.js b/awx/ui/client/lib/services/base-string.service.js index b514e653fd..437cd10b2a 100644 --- a/awx/ui/client/lib/services/base-string.service.js +++ b/awx/ui/client/lib/services/base-string.service.js @@ -3,17 +3,49 @@ import defaults from '../../assets/default.strings.json'; let i18n; function BaseStringService (namespace) { - let t = i18n._; - - const ERROR_NO_NAMESPACE = t('BaseString cannot be extended without providing a namespace'); - const ERROR_NO_STRING = t('No string exists with this name'); + const ERROR_NO_NAMESPACE = 'BaseString cannot be extended without providing a namespace'; + const ERROR_NO_STRING = 'No string exists with this name'; if (!namespace) { throw new Error(ERROR_NO_NAMESPACE); } - this.t = t; this[namespace] = {}; + this.t = {}; + + /** + * To translate a singular string by itself or a string with context data, use `translate`. + * For brevity, this is renamed as `t.s` (as in "translate singular"). `t.s` serves a dual + * purpose -- it's to mark strings for translation so they appear in the `.pot` file after + * the grunt-angular-gettext task is run AND it's used to fetch the translated string at + * runtime. + * + * NOTE: View ui/src/i18n.js for where these i18n methods are defined. i18n is a wrapper around + * the library angular-gettext. + * + * @arg {string} string - The string to be translated + * @arg {object=} context - A data object used to populate dynamic context data in a string. + * + * @returns {string} The translated string or the original string in the even the translation + * does not exist. + */ + this.t.s = i18n.translate; + + /** + * To translate a plural string use `t.p`. The `count` supplied will determine whether the + * singular or plural string is returned. + * + * @arg {number} count - The count of the plural object + * @arg {string} singular - The singular version of the string to be translated + * @arg {string} plural - The plural version of the string to be translated + * @arg {object=} context - A data object used to populate dynamic context data in a string. + * + * @returns {string} The translated string or the original string in the even the translation + * does not exist. + */ + this.t.p = i18n.translatePlural; + + let t = this.t; /* * These strings are globally relevant and configured to give priority to values in @@ -26,9 +58,9 @@ function BaseStringService (namespace) { * Globally relevant strings should be defined here to avoid duplication of content across the * the project. */ - this.CANCEL = t('CANCEL'); - this.SAVE = t('SAVE'); - this.OK = t('OK'); + this.CANCEL = t.s('CANCEL'); + this.SAVE = t.s('SAVE'); + this.OK = t.s('OK'); /** * This getter searches the extending class' namespace first for a match then falls back to @@ -37,8 +69,16 @@ function BaseStringService (namespace) { * * If no match is found, an error is thrown to alert the developer immediately instead of * failing silently. + * + * The `t.s` and `t.p` calls should only be used where strings are defined in + * .strings.js` files. To use translated strings elsewhere, access them through this + * common interface. + * + * @arg {string} name - The property name of the string (e.g. 'CANCEL') + * @arg {number=} count - A count of objects referenced in your plural string + * @arg {object=} context - An object containing data to use in the interpolation of the string */ - this.get = name => { + this.get = (name, ...args) => { let keys = name.split('.'); let value; @@ -50,11 +90,11 @@ function BaseStringService (namespace) { } if (!value) { - throw new Error(ERROR_NO_STRING); + throw new Error(ERROR_NO_STRING + `: ${name}`); } }); - return value; + return typeof value === 'string' ? value : value(...args); }; } diff --git a/awx/ui/client/src/i18n.js b/awx/ui/client/src/i18n.js index 062c2455ea..685c7e77df 100644 --- a/awx/ui/client/src/i18n.js +++ b/awx/ui/client/src/i18n.js @@ -40,6 +40,10 @@ export default 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; diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/groups/list/groups-list.controller.js b/awx/ui/client/src/inventories-hosts/inventories/related/groups/list/groups-list.controller.js index d0a16f11ea..99b1090e2d 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/groups/list/groups-list.controller.js +++ b/awx/ui/client/src/inventories-hosts/inventories/related/groups/list/groups-list.controller.js @@ -7,9 +7,11 @@ ['$scope', '$rootScope', '$state', '$stateParams', 'GroupList', 'InventoryUpdate', 'GroupsService', 'CancelSourceUpdate', 'rbacUiControlService', 'GetBasePath', 'GetHostsStatusMsg', 'Dataset', 'Find', 'QuerySet', 'inventoryData', 'canAdd', + 'InventoryHostsStrings', function($scope, $rootScope, $state, $stateParams, GroupList, InventoryUpdate, GroupsService, CancelSourceUpdate, rbacUiControlService, GetBasePath, - GetHostsStatusMsg, Dataset, Find, qs, inventoryData, canAdd){ + GetHostsStatusMsg, Dataset, Find, qs, inventoryData, canAdd, + InventoryHostsStrings){ let list = GroupList; @@ -20,6 +22,10 @@ $scope.canAdhoc = inventoryData.summary_fields.user_capabilities.adhoc; $scope.canAdd = canAdd; + $scope.strings = { + deleteModal: {} + }; + // Search init $scope.list = list; $scope[`${list.iterator}_dataset`] = Dataset.data; @@ -89,26 +95,36 @@ }; $scope.deleteGroup = function(group){ $scope.toDelete = {}; + $scope.strings.deleteModal = {}; angular.extend($scope.toDelete, group); if($scope.toDelete.total_groups === 0 && $scope.toDelete.total_hosts === 0) { // This group doesn't have any child groups or hosts - the user is just trying to delete // the group $scope.deleteOption = "delete"; } + else { + $scope.strings.deleteModal.group = InventoryHostsStrings.get('deletegroup.GROUP', $scope.toDelete.total_groups); + $scope.strings.deleteModal.host = InventoryHostsStrings.get('deletegroup.HOST', $scope.toDelete.total_hosts); + + if($scope.toDelete.total_groups === 0 || $scope.toDelete.total_hosts === 0) { + if($scope.toDelete.total_groups === 0) { + $scope.strings.deleteModal.deleteGroupsHosts = InventoryHostsStrings.get('deletegroup.DELETE_HOST', $scope.toDelete.total_hosts); + $scope.strings.deleteModal.promoteGroupsHosts = InventoryHostsStrings.get('deletegroup.PROMOTE_HOST', $scope.toDelete.total_hosts); + } + else if($scope.toDelete.total_hosts === 0) { + $scope.strings.deleteModal.deleteGroupsHosts = InventoryHostsStrings.get('deletegroup.DELETE_GROUP', $scope.toDelete.total_groups); + $scope.strings.deleteModal.promoteGroupsHosts = InventoryHostsStrings.get('deletegroup.PROMOTE_GROUP', $scope.toDelete.total_groups); + } + } + else { + $scope.strings.deleteModal.deleteGroupsHosts = InventoryHostsStrings.get('deletegroup.DELETE_GROUPS_AND_HOSTS', {groups: $scope.toDelete.total_groups, hosts: $scope.toDelete.total_hosts}); + $scope.strings.deleteModal.promoteGroupsHosts = InventoryHostsStrings.get('deletegroup.PROMOTE_GROUPS_AND_HOSTS', {groups: $scope.toDelete.total_groups, hosts: $scope.toDelete.total_hosts}); + } + } + $('#group-delete-modal').modal('show'); }; $scope.confirmDelete = function(){ - - // Bind an even listener for the modal closing. Trying to $state.go() before the modal closes - // will mean that these two things are running async and the modal may not finish closing before - // the state finishes transitioning. - $('#group-delete-modal').off('hidden.bs.modal').on('hidden.bs.modal', function () { - // Remove the event handler so that we don't end up with multiple bindings - $('#group-delete-modal').off('hidden.bs.modal'); - // Reload the inventory manage page and show that the group has been removed - $state.go('.', null, {reload: true}); - }); - let reloadListStateParams = null; if($scope.groups.length === 1 && $state.params.group_search && !_.isEmpty($state.params.group_search.page) && $state.params.group_search.page !== '1') { diff --git a/awx/ui/client/src/inventories-hosts/inventories/related/groups/list/groups-list.partial.html b/awx/ui/client/src/inventories-hosts/inventories/related/groups/list/groups-list.partial.html index 6b2a01a971..ec594610a6 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/related/groups/list/groups-list.partial.html +++ b/awx/ui/client/src/inventories-hosts/inventories/related/groups/list/groups-list.partial.html @@ -21,42 +21,20 @@

Deleting group {{ toDelete.name }}. - This group contains {{ toDelete.total_groups }} groups and {{ toDelete.total_hosts }} hosts. - This group contains {{ toDelete.total_hosts }} hosts. - This group contains {{ toDelete.total_groups }} groups. + This group contains {{ toDelete.total_groups }} {{:: strings.deleteModal.group }} and {{ toDelete.total_hosts }} {{:: strings.deleteModal.host }}. + This group contains {{ toDelete.total_hosts }} {{:: strings.deleteModal.host }}. + This group contains {{ toDelete.total_groups }} {{:: strings.deleteModal.group }}. Delete or promote the group's children?

-
+
-
+
-
- -
- -
-
- -
- -
- -
-
-
@@ -71,7 +49,7 @@
diff --git a/awx/ui/client/src/inventories-hosts/inventories/smart-inventory/main.js b/awx/ui/client/src/inventories-hosts/inventories/smart-inventory/main.js index 9265ca2501..0f8a57560f 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/smart-inventory/main.js +++ b/awx/ui/client/src/inventories-hosts/inventories/smart-inventory/main.js @@ -9,7 +9,6 @@ import smartInventoryEdit from './edit/main'; import smartInventoryForm from './smart-inventory.form'; import smartInventoryHostFilter from './smart-inventory-host-filter/smart-inventory-host-filter.directive'; import hostFilterModal from './smart-inventory-host-filter/host-filter-modal/host-filter-modal.directive'; -import SmartInventoryStrings from './smart-inventory.strings'; export default angular.module('smartInventory', [ @@ -18,5 +17,4 @@ angular.module('smartInventory', [ ]) .factory('smartInventoryForm', smartInventoryForm) .directive('smartInventoryHostFilter', smartInventoryHostFilter) - .directive('hostFilterModal', hostFilterModal) - .service('SmartInventoryStrings', SmartInventoryStrings); + .directive('hostFilterModal', hostFilterModal); diff --git a/awx/ui/client/src/inventories-hosts/inventories/smart-inventory/smart-inventory-host-filter/smart-inventory-host-filter.controller.js b/awx/ui/client/src/inventories-hosts/inventories/smart-inventory/smart-inventory-host-filter/smart-inventory-host-filter.controller.js index 198e398b66..dd9008632b 100644 --- a/awx/ui/client/src/inventories-hosts/inventories/smart-inventory/smart-inventory-host-filter/smart-inventory-host-filter.controller.js +++ b/awx/ui/client/src/inventories-hosts/inventories/smart-inventory/smart-inventory-host-filter/smart-inventory-host-filter.controller.js @@ -4,11 +4,11 @@ * All Rights Reserved *************************************************/ -export default ['$scope', 'QuerySet', 'SmartInventoryStrings', - function($scope, qs, SmartInventoryStrings) { +export default ['$scope', 'QuerySet', 'InventoryHostsStrings', + function($scope, qs, InventoryHostsStrings) { $scope.hostFilterTags = []; - $scope.filterTooltip = SmartInventoryStrings.get('filter.TOOLTIP'); + $scope.filterTooltip = InventoryHostsStrings.get('smartinventories.TOOLTIP'); $scope.$watch('hostFilter', function(){ $scope.hostFilterTags = []; diff --git a/awx/ui/client/src/inventories-hosts/inventories/smart-inventory/smart-inventory.strings.js b/awx/ui/client/src/inventories-hosts/inventories/smart-inventory/smart-inventory.strings.js deleted file mode 100644 index e84d4d38f0..0000000000 --- a/awx/ui/client/src/inventories-hosts/inventories/smart-inventory/smart-inventory.strings.js +++ /dev/null @@ -1,14 +0,0 @@ -function SmartInventoryStrings (BaseString) { - BaseString.call(this, 'smartinventories'); - - let t = this.t; - let ns = this.smartinventories; - - ns.filter = { - TOOLTIP: t('Please click the icon to edit the host filter.') - }; -} - -SmartInventoryStrings.$inject = ['BaseStringService']; - -export default SmartInventoryStrings; diff --git a/awx/ui/client/src/inventories-hosts/inventory-hosts.strings.js b/awx/ui/client/src/inventories-hosts/inventory-hosts.strings.js new file mode 100644 index 0000000000..56cd3d9ba4 --- /dev/null +++ b/awx/ui/client/src/inventories-hosts/inventory-hosts.strings.js @@ -0,0 +1,31 @@ +function InventoryHostsStrings (BaseString) { + BaseString.call(this, 'inventory-hosts'); + + let t = this.t; + let ns = this['inventory-hosts']; + + ns.deletegroup = { + GROUP: count => t.p(count, 'group', 'groups'), + HOST: count => t.p(count, 'host', 'hosts'), + PROMOTE_GROUPS_AND_HOSTS: data => t.s('Promote {{ group }} and {{ host }}', { + group: this.get('deletegroup.GROUP', data.groups), + host: this.get('deletegroup.HOST', data.hosts) + }), + DELETE_GROUPS_AND_HOSTS: data => t.s('Delete {{ group }} and {{ host }}', { + group: this.get('deletegroup.GROUP', data.groups), + host: this.get('deletegroup.HOST', data.hosts) + }), + PROMOTE_GROUP: count => t.p(count, 'Promote group', 'Promote groups'), + DELETE_GROUP: count => t.p(count, 'Delete group', 'Delete groups'), + PROMOTE_HOST: count => t.p(count, 'Promote host', 'Promote hosts'), + DELETE_HOST: count => t.p(count, 'Delete host', 'Delete hosts'), + }; + + ns.smartinventories = { + TOOLTIP: t.s('Please click the icon to edit the host filter.') + }; +} + +InventoryHostsStrings.$inject = ['BaseStringService']; + +export default InventoryHostsStrings; diff --git a/awx/ui/client/src/inventories-hosts/main.js b/awx/ui/client/src/inventories-hosts/main.js index be79ffa2e6..de837d2049 100644 --- a/awx/ui/client/src/inventories-hosts/main.js +++ b/awx/ui/client/src/inventories-hosts/main.js @@ -7,10 +7,12 @@ import hosts from './hosts/main'; import inventories from './inventories/main'; import shared from './shared/main'; + import InventoryHostsStrings from './inventory-hosts.strings'; export default angular.module('inventories-hosts', [ hosts.name, inventories.name, shared.name - ]); + ]) + .service('InventoryHostsStrings', InventoryHostsStrings); diff --git a/awx/ui/grunt-tasks/nggettext_extract.js b/awx/ui/grunt-tasks/nggettext_extract.js index b264b6ada8..437e82dd47 100644 --- a/awx/ui/grunt-tasks/nggettext_extract.js +++ b/awx/ui/grunt-tasks/nggettext_extract.js @@ -10,7 +10,10 @@ let source = [ module.exports = { all: { options: { - markerNames: ['_', 'N_', 't'] + markerNames: ['_', 'N_'], + moduleName: 't', + moduleMethodString: 's', + moduleMethodPlural: 'p' }, files: { 'po/ansible-tower-ui.pot': source