Plato on Github
Report Home
directive/translate.js
Maintainability
67.03
Lines of code
380
Difficulty
38.17
Estimated Errors
2.43
Function weight
By Complexity
By SLOC
angular.module('pascalprecht.translate') /** * @ngdoc directive * @name pascalprecht.translate.directive:translate * @requires $interpolate, * @requires $compile, * @requires $parse, * @requires $rootScope * @restrict AE * * @description * Translates given translation id either through attribute or DOM content. * Internally it uses $translate service to translate the translation id. It possible to * pass an optional `translate-values` object literal as string into translation id. * * @param {string=} translate Translation id which could be either string or interpolated string. * @param {string=} translate-values Values to pass into translation id. Can be passed as object literal string or interpolated object. * @param {string=} translate-attr-ATTR translate Translation id and put it into ATTR attribute. * @param {string=} translate-default will be used unless translation was successful * @param {string=} translate-sanitize-strategy defines locally sanitize strategy * @param {boolean=} translate-compile (default true if present) defines locally activation of {@link pascalprecht.translate.$translateProvider#methods_usePostCompiling} * @param {boolean=} translate-keep-content (default true if present) defines that in case a KEY could not be translated, that the existing content is left in the innerHTML} * * @example <example module="ngView"> <file name="index.html"> <div ng-controller="TranslateCtrl"> <pre translate="TRANSLATION_ID"></pre> <pre translate>TRANSLATION_ID</pre> <pre translate translate-attr-title="TRANSLATION_ID"></pre> <pre translate="{{translationId}}"></pre> <pre translate>{{translationId}}</pre> <pre translate="WITH_VALUES" translate-values="{value: 5}"></pre> <pre translate translate-values="{value: 5}">WITH_VALUES</pre> <pre translate="WITH_VALUES" translate-values="{{values}}"></pre> <pre translate translate-values="{{values}}">WITH_VALUES</pre> <pre translate translate-attr-title="WITH_VALUES" translate-values="{{values}}"></pre> <pre translate="WITH_CAMEL_CASE_KEY" translate-value-camel-case-key="Hi"></pre> </div> </file> <file name="script.js"> angular.module('ngView', ['pascalprecht.translate']) .config(function ($translateProvider) { $translateProvider.translations('en',{ 'TRANSLATION_ID': 'Hello there!', 'WITH_VALUES': 'The following value is dynamic: {{value}}', 'WITH_CAMEL_CASE_KEY': 'The interpolation key is camel cased: {{camelCaseKey}}' }).preferredLanguage('en'); }); angular.module('ngView').controller('TranslateCtrl', function ($scope) { $scope.translationId = 'TRANSLATION_ID'; $scope.values = { value: 78 }; }); </file> <file name="scenario.js"> it('should translate', function () { inject(function ($rootScope, $compile) { $rootScope.translationId = 'TRANSLATION_ID'; element = $compile('<p translate="TRANSLATION_ID"></p>')($rootScope); $rootScope.$digest(); expect(element.text()).toBe('Hello there!'); element = $compile('<p translate="{{translationId}}"></p>')($rootScope); $rootScope.$digest(); expect(element.text()).toBe('Hello there!'); element = $compile('<p translate>TRANSLATION_ID</p>')($rootScope); $rootScope.$digest(); expect(element.text()).toBe('Hello there!'); element = $compile('<p translate>{{translationId}}</p>')($rootScope); $rootScope.$digest(); expect(element.text()).toBe('Hello there!'); element = $compile('<p translate translate-attr-title="TRANSLATION_ID"></p>')($rootScope); $rootScope.$digest(); expect(element.attr('title')).toBe('Hello there!'); element = $compile('<p translate="WITH_CAMEL_CASE_KEY" translate-value-camel-case-key="Hello"></p>')($rootScope); $rootScope.$digest(); expect(element.text()).toBe('The interpolation key is camel cased: Hello'); }); }); </file> </example> */ .directive('translate', translateDirective); function translateDirective($translate, $interpolate, $compile, $parse, $rootScope) { 'use strict'; /** * @name trim * @private * * @description * trim polyfill * * @returns {string} The string stripped of whitespace from both ends */ var trim = function() { return this.toString().replace(/^\s+|\s+$/g, ''); }; /** * @name lowercase * @private * * @description * Return the lowercase string only if the type is string * * @returns {string} The string all in lowercase */ var lowercase = function (string) { return angular.isString(string) ? string.toLowerCase() : string; }; return { restrict: 'AE', scope: true, priority: $translate.directivePriority(), compile: function (tElement, tAttr) { var translateValuesExist = (tAttr.translateValues) ? tAttr.translateValues : undefined; var translateInterpolation = (tAttr.translateInterpolation) ? tAttr.translateInterpolation : undefined; var translateSanitizeStrategyExist = (tAttr.translateSanitizeStrategy) ? tAttr.translateSanitizeStrategy : undefined; var translateValueExist = tElement[0].outerHTML.match(/translate-value-+/i); var interpolateRegExp = '^(.*)(' + $interpolate.startSymbol() + '.*' + $interpolate.endSymbol() + ')(.*)', watcherRegExp = '^(.*)' + $interpolate.startSymbol() + '(.*)' + $interpolate.endSymbol() + '(.*)'; return function linkFn(scope, iElement, iAttr) { scope.interpolateParams = {}; scope.preText = ''; scope.postText = ''; scope.translateNamespace = getTranslateNamespace(scope); var translationIds = {}; var initInterpolationParams = function (interpolateParams, iAttr, tAttr) { // initial setup if (iAttr.translateValues) { angular.extend(interpolateParams, $parse(iAttr.translateValues)(scope.$parent)); } // initially fetch all attributes if existing and fill the params if (translateValueExist) { for (var attr in tAttr) { if (Object.prototype.hasOwnProperty.call(iAttr, attr) && attr.substr(0, 14) === 'translateValue' && attr !== 'translateValues') { var attributeName = lowercase(attr.substr(14, 1)) + attr.substr(15); interpolateParams[attributeName] = tAttr[attr]; } } } }; // Ensures any change of the attribute "translate" containing the id will // be re-stored to the scope's "translationId". // If the attribute has no content, the element's text value (white spaces trimmed off) will be used. var observeElementTranslation = function (translationId) { // Remove any old watcher if (angular.isFunction(observeElementTranslation._unwatchOld)) { observeElementTranslation._unwatchOld(); observeElementTranslation._unwatchOld = undefined; } if (angular.equals(translationId , '') || !angular.isDefined(translationId)) { var iElementText = trim.apply(iElement.text()).replace(/\n/g, ' '); // Resolve translation id by inner html if required var interpolateMatches = iElementText.match(interpolateRegExp); // Interpolate translation id if required if (angular.isArray(interpolateMatches)) { scope.preText = interpolateMatches[1]; scope.postText = interpolateMatches[3]; translationIds.translate = $interpolate(interpolateMatches[2])(scope.$parent); var watcherMatches = iElementText.match(watcherRegExp); if (angular.isArray(watcherMatches) && watcherMatches[2] && watcherMatches[2].length) { observeElementTranslation._unwatchOld = scope.$watch(watcherMatches[2], function (newValue) { translationIds.translate = newValue; updateTranslations(); }); } } else { // do not assigne the translation id if it is empty. translationIds.translate = !iElementText ? undefined : iElementText; } } else { translationIds.translate = translationId; } updateTranslations(); }; var observeAttributeTranslation = function (translateAttr) { iAttr.$observe(translateAttr, function (translationId) { translationIds[translateAttr] = translationId; updateTranslations(); }); }; // initial setup with values initInterpolationParams(scope.interpolateParams, iAttr, tAttr); var firstAttributeChangedEvent = true; iAttr.$observe('translate', function (translationId) { if (typeof translationId === 'undefined') { // case of element "<translate>xyz</translate>" observeElementTranslation(''); } else { // case of regular attribute if (translationId !== '' || !firstAttributeChangedEvent) { translationIds.translate = translationId; updateTranslations(); } } firstAttributeChangedEvent = false; }); for (var translateAttr in iAttr) { if (iAttr.hasOwnProperty(translateAttr) && translateAttr.substr(0, 13) === 'translateAttr' && translateAttr.length > 13) { observeAttributeTranslation(translateAttr); } } iAttr.$observe('translateDefault', function (value) { scope.defaultText = value; updateTranslations(); }); if (translateSanitizeStrategyExist) { iAttr.$observe('translateSanitizeStrategy', function (value) { scope.sanitizeStrategy = $parse(value)(scope.$parent); updateTranslations(); }); } if (translateValuesExist) { iAttr.$observe('translateValues', function (interpolateParams) { if (interpolateParams) { scope.$parent.$watch(function () { angular.extend(scope.interpolateParams, $parse(interpolateParams)(scope.$parent)); }); } }); } if (translateValueExist) { var observeValueAttribute = function (attrName) { iAttr.$observe(attrName, function (value) { var attributeName = lowercase(attrName.substr(14, 1)) + attrName.substr(15); scope.interpolateParams[attributeName] = value; }); }; for (var attr in iAttr) { if (Object.prototype.hasOwnProperty.call(iAttr, attr) && attr.substr(0, 14) === 'translateValue' && attr !== 'translateValues') { observeValueAttribute(attr); } } } // Master update function var updateTranslations = function () { for (var key in translationIds) { if (translationIds.hasOwnProperty(key) && translationIds[key] !== undefined) { updateTranslation(key, translationIds[key], scope, scope.interpolateParams, scope.defaultText, scope.translateNamespace); } } }; // Put translation processing function outside loop var updateTranslation = function(translateAttr, translationId, scope, interpolateParams, defaultTranslationText, translateNamespace) { if (translationId) { // if translation id starts with '.' and translateNamespace given, prepend namespace if (translateNamespace && translationId.charAt(0) === '.') { translationId = translateNamespace + translationId; } $translate(translationId, interpolateParams, translateInterpolation, defaultTranslationText, scope.translateLanguage, scope.sanitizeStrategy) .then(function (translation) { applyTranslation(translation, scope, true, translateAttr); }, function (translationId) { applyTranslation(translationId, scope, false, translateAttr); }); } else { // as an empty string cannot be translated, we can solve this using successful=false applyTranslation(translationId, scope, false, translateAttr); } }; var applyTranslation = function (value, scope, successful, translateAttr) { if (!successful) { if (typeof scope.defaultText !== 'undefined') { value = scope.defaultText; } } if (translateAttr === 'translate') { // default translate into innerHTML if (successful || (!successful && !$translate.isKeepContent() && typeof iAttr.translateKeepContent === 'undefined')) { iElement.empty().append(scope.preText + value + scope.postText); } var globallyEnabled = $translate.isPostCompilingEnabled(); var locallyDefined = typeof tAttr.translateCompile !== 'undefined'; var locallyEnabled = locallyDefined && tAttr.translateCompile !== 'false'; if ((globallyEnabled && !locallyDefined) || locallyEnabled) { $compile(iElement.contents())(scope); } } else { // translate attribute var attributeName = iAttr.$attr[translateAttr]; if (attributeName.substr(0, 5) === 'data-') { // ensure html5 data prefix is stripped attributeName = attributeName.substr(5); } attributeName = attributeName.substr(15); iElement.attr(attributeName, value); } }; if (translateValuesExist || translateValueExist || iAttr.translateDefault) { scope.$watch('interpolateParams', updateTranslations, true); } // Replaced watcher on translateLanguage with event listener scope.$on('translateLanguageChanged', updateTranslations); // Ensures the text will be refreshed after the current language was changed // w/ $translate.use(...) var unbind = $rootScope.$on('$translateChangeSuccess', updateTranslations); // ensure translation will be looked up at least one if (iElement.text().length) { if (iAttr.translate) { observeElementTranslation(iAttr.translate); } else { observeElementTranslation(''); } } else if (iAttr.translate) { // ensure attribute will be not skipped observeElementTranslation(iAttr.translate); } updateTranslations(); scope.$on('$destroy', unbind); }; } }; } /** * Returns the scope's namespace. * @private * @param scope * @returns {string} */ function getTranslateNamespace(scope) { 'use strict'; if (scope.translateNamespace) { return scope.translateNamespace; } if (scope.$parent) { return getTranslateNamespace(scope.$parent); } } translateDirective.displayName = 'translateDirective';