├── index.js ├── test ├── configs │ ├── e2e.conf.js │ ├── unit-nojquery.conf.js │ └── unit.conf.js ├── unit │ ├── gettext.js │ ├── fallback_language.js │ ├── filter.js │ ├── plurals.js │ ├── loading.js │ ├── catalog.js │ └── directive.js ├── e2e │ ├── xss.js │ └── click.js └── fixtures │ ├── xss-nojquery.html │ ├── xss.html │ ├── click.html │ ├── click-nojquery.html │ └── playground.html ├── .gitignore ├── .npmignore ├── .travis.yml ├── bower.json ├── .jshintrc ├── src ├── fallback_language.js ├── filter.js ├── index.js ├── util.js ├── plural.js ├── directive.js └── catalog.js ├── LICENSE ├── package.json ├── README.md ├── .jscs.json ├── docs └── api │ └── index.ngdoc ├── genplurals.py ├── dist ├── angular-gettext.min.js └── angular-gettext.js └── Gruntfile.js /index.js: -------------------------------------------------------------------------------- 1 | require('./dist/angular-gettext.js'); 2 | module.exports = 'gettext'; 3 | -------------------------------------------------------------------------------- /test/configs/e2e.conf.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | specs: ["../e2e/**/*.js"] 3 | }; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .*.swp 2 | /node_modules 3 | /bower_components 4 | /libpeerconnection.log 5 | /unit-results.xml 6 | /e2e-results.xml 7 | /.idea 8 | *.iml 9 | .DS_Store -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /Gruntfile.js 2 | /.travis.yml 3 | /genplurals.py 4 | /test 5 | /src 6 | /node_modules 7 | /bower_components 8 | /libpeerconnection.log 9 | /*-results.xml 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | dist: trusty 5 | sudo: true 6 | addons: 7 | chrome: stable 8 | before_install: 9 | - npm install -g grunt-cli bower 10 | - bower install 11 | - export DISPLAY=:99.0 12 | - sh -e /etc/init.d/xvfb start 13 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-gettext", 3 | "version": "2.4.2", 4 | "main": "dist/angular-gettext.js", 5 | "ignore": [ 6 | "**/.*", 7 | "src", 8 | "node_modules", 9 | "bower_components", 10 | "test", 11 | "genplurals.py", 12 | "Gruntfile.js" 13 | ], 14 | "dependencies": { 15 | "angular": ">=1.2.0" 16 | }, 17 | "devDependencies": { 18 | "jquery": ">=1.8.0", 19 | "angular-mocks": ">=1.2.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/unit/gettext.js: -------------------------------------------------------------------------------- 1 | describe("gettext", function () { 2 | var gettext = null; 3 | 4 | beforeEach(module("gettext")); 5 | 6 | beforeEach(inject(function ($injector) { 7 | gettext = $injector.get("gettext"); 8 | })); 9 | 10 | it("gettext function is a noop", function () { 11 | assert.equal(3, gettext(3)); 12 | }); 13 | 14 | it("gettext function is a noop (string)", function () { 15 | assert.equal("test", gettext("test")); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /test/configs/unit-nojquery.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | basePath: "../..", 4 | 5 | frameworks: ["mocha", "chai"], 6 | 7 | files: [ 8 | "bower_components/angular/angular.js", 9 | "bower_components/angular-mocks/angular-mocks.js", 10 | "dist/angular-gettext.js", 11 | "test/unit/**/*.js" 12 | ], 13 | 14 | port: 9876, 15 | 16 | client: { 17 | mocha: { 18 | timeout: 5000 19 | } 20 | } 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /test/e2e/xss.js: -------------------------------------------------------------------------------- 1 | var tests = { 2 | "Should not allow XSS": function (file) { 3 | return function () { 4 | browser.get("http://localhost:9000/test/fixtures/xss" + file + ".html"); 5 | expect(element(by.css("body")).getText()).not.toBe("fail"); 6 | }; 7 | } 8 | }; 9 | 10 | describe("XSS", function () { 11 | for (var key in tests) { 12 | it(key, tests[key]("")); 13 | } 14 | }); 15 | 16 | describe("XSS (no jQuery)", function () { 17 | for (var key in tests) { 18 | it(key, tests[key]("-nojquery")); 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /test/configs/unit.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | basePath: "../..", 4 | 5 | frameworks: ["mocha", "chai"], 6 | 7 | files: [ 8 | "bower_components/jquery/dist/jquery.js", 9 | "bower_components/angular/angular.js", 10 | "bower_components/angular-mocks/angular-mocks.js", 11 | "dist/angular-gettext.js", 12 | "test/unit/**/*.js" 13 | ], 14 | 15 | port: 9877, 16 | 17 | client: { 18 | mocha: { 19 | timeout: 5000 20 | } 21 | } 22 | }); 23 | }; 24 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "curly": true, 4 | "eqeqeq": true, 5 | "es3": true, 6 | "freeze": true, 7 | "immed": true, 8 | "indent": 4, 9 | "latedef": true, 10 | "newcap": true, 11 | "noarg": true, 12 | "node": true, 13 | "trailing": true, 14 | "undef": true, 15 | "unused": true, 16 | "predef": [ 17 | "$", 18 | "after", 19 | "afterEach", 20 | "angular", 21 | "assert", 22 | "before", 23 | "beforeEach", 24 | "browser", 25 | "by", 26 | "describe", 27 | "element", 28 | "expect", 29 | "inject", 30 | "it" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /test/unit/fallback_language.js: -------------------------------------------------------------------------------- 1 | describe("Fallback languages", function () { 2 | var fallback = null; 3 | 4 | beforeEach(module("gettext")); 5 | 6 | beforeEach(inject(function (gettextFallbackLanguage) { 7 | fallback = gettextFallbackLanguage; 8 | })); 9 | 10 | it("returns base language as fallback", function () { 11 | assert.equal(fallback("en_GB"), "en"); 12 | }); 13 | it("returns null for simple language codes", function () { 14 | assert.isNull(fallback("nl"), null); 15 | }); 16 | it("returns null for null", function () { 17 | assert.isNull(fallback(null)); 18 | }); 19 | it("returns consistent values", function () { 20 | assert.equal(fallback("de_CH"), fallback("de_CH")); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /test/fixtures/xss-nojquery.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | XSS test 5 | 6 | 7 |

XSS test

8 |

expression: {{ value }}

9 |

translate directive: {{ value }}

10 |

translate directive with translate-params: {{ value }}

11 | 12 | 13 | 14 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /test/fixtures/xss.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | XSS test 5 | 6 | 7 |

XSS test

8 |

expression: {{ value }}

9 |

translate directive: {{ value }}

10 |

translate directive with translate-params: {{ value }}

11 | 12 | 13 | 14 | 15 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/fallback_language.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc factory 3 | * @module gettext 4 | * @name gettextFallbackLanguage 5 | * @param {String} langCode language code 6 | * @returns {String|Null} fallback language 7 | * @description Strips regional code and returns language code only 8 | * 9 | * Example 10 | * ```js 11 | * gettextFallbackLanguage('ru'); // "null" 12 | * gettextFallbackLanguage('en_GB'); // "en" 13 | * gettextFallbackLanguage(); // null 14 | * ``` 15 | */ 16 | angular.module("gettext").factory("gettextFallbackLanguage", function () { 17 | var cache = {}; 18 | var pattern = /([^_]+)_[^_]+$/; 19 | 20 | return function (langCode) { 21 | if (cache[langCode]) { 22 | return cache[langCode]; 23 | } 24 | 25 | var matches = pattern.exec(langCode); 26 | if (matches) { 27 | cache[langCode] = matches[1]; 28 | return matches[1]; 29 | } 30 | 31 | return null; 32 | }; 33 | }); -------------------------------------------------------------------------------- /src/filter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc filter 3 | * @module gettext 4 | * @name translate 5 | * @requires gettextCatalog 6 | * @param {String} input translation key 7 | * @param {String} context context to evaluate key against 8 | * @returns {String} translated string or annotated key 9 | * @see {@link doc:context Verb, Noun} 10 | * @description Takes key and returns string 11 | * 12 | * Sometimes it's not an option to use an attribute (e.g. when you want to annotate an attribute value). 13 | * There's a `translate` filter available for this purpose. 14 | * 15 | * ```html 16 | * 17 | * ``` 18 | * This filter does not support plural strings. 19 | * 20 | * You may want to use {@link guide:custom-annotations custom annotations} to avoid using the `translate` filter all the time. * Is 21 | */ 22 | angular.module('gettext').filter('translate', function (gettextCatalog) { 23 | function filter(input, context) { 24 | return gettextCatalog.getString(input, null, context); 25 | } 26 | filter.$stateful = true; 27 | return filter; 28 | }); 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2013-2018 by Ruben Vermeersch 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /test/e2e/click.js: -------------------------------------------------------------------------------- 1 | var tests = { 2 | "Should not break ng-click": function (file) { 3 | return function () { 4 | browser.get("http://localhost:9000/test/fixtures/click" + file + ".html"); 5 | element(by.css("button#test1")).click(); 6 | expect(element(by.css("#field")).getText()).toBe("Success"); 7 | }; 8 | }, 9 | 10 | "Should not break ng-click for translated strings": function (file) { 11 | return function () { 12 | browser.get("http://localhost:9000/test/fixtures/click" + file + ".html"); 13 | element(by.css("button#test2")).click(); 14 | expect(element(by.css("#field")).getText()).toBe("Success"); 15 | }; 16 | }, 17 | 18 | "Should compile ng-click": function (file) { 19 | return function () { 20 | browser.get("http://localhost:9000/test/fixtures/click" + file + ".html"); 21 | element(by.css("#test3 button")).click(); 22 | expect(element(by.css("#field")).getText()).toBe("Success"); 23 | }; 24 | } 25 | }; 26 | 27 | describe("Click", function () { 28 | for (var key in tests) { 29 | it(key, tests[key]("")); 30 | } 31 | }); 32 | 33 | describe("Click (no jQuery)", function () { 34 | for (var key in tests) { 35 | it(key, tests[key]("-nojquery")); 36 | } 37 | }); 38 | -------------------------------------------------------------------------------- /test/fixtures/click.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Click test 5 | 6 | 7 |

Click test

8 | 9 | 10 | Compile 11 |
12 | 13 | 14 | 15 | 16 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /test/fixtures/click-nojquery.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Click test 5 | 6 | 7 |

Click test

8 | 9 | 10 | Compile 11 | Compile 12 |
13 | 14 | 15 | 16 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-gettext", 3 | "version": "2.4.2", 4 | "title": "Angular Gettext", 5 | "description": "Gettext support for Angular.js", 6 | "main": "index.js", 7 | "directories": { 8 | "test": "test" 9 | }, 10 | "scripts": { 11 | "test": "grunt ci", 12 | "prepublish": "grunt build" 13 | }, 14 | "keywords": [ 15 | "angular", 16 | "gettext" 17 | ], 18 | "author": { 19 | "name": "Ruben Vermeersch", 20 | "email": "ruben@rocketeer.be", 21 | "url": "http://rocketeer.be/" 22 | }, 23 | "homepage": "http://angular-gettext.rocketeer.be/", 24 | "license": "MIT", 25 | "devDependencies": { 26 | "chai": "^4.1.2", 27 | "dgeni-alive": "~0.4.1", 28 | "grunt": "~1.0.1", 29 | "grunt-bump": "0.8.0", 30 | "grunt-contrib-clean": "~1.1.0", 31 | "grunt-contrib-concat": "~1.0.0", 32 | "grunt-contrib-connect": "~1.0.0", 33 | "grunt-contrib-jshint": "~1.1.0", 34 | "grunt-contrib-uglify": "~3.3.0", 35 | "grunt-contrib-watch": "~1.1.0", 36 | "grunt-jscs": "^3.0.0", 37 | "grunt-karma": "~2.0.0", 38 | "grunt-ng-annotate": "^3.0.0", 39 | "grunt-protractor-runner": "^5.0.0", 40 | "grunt-shell": "^2.1.0", 41 | "karma": "~3.0.0", 42 | "karma-chai": "~0.1.0", 43 | "karma-chrome-launcher": "^2.2.0", 44 | "karma-mocha": "~1.3.0", 45 | "karma-phantomjs-launcher": "^1.0.0", 46 | "mocha": "^5.1.1", 47 | "phantomjs-prebuilt": "^2.1.7", 48 | "serve-static": "^1.11.1" 49 | }, 50 | "repository": { 51 | "type": "git", 52 | "url": "git://github.com/rubenv/angular-gettext.git" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # angular-gettext - gettext utilities for angular.js 2 | 3 | > Translate your Angular.JS applications with gettext. 4 | 5 | [![Build Status](https://travis-ci.org/rubenv/angular-gettext.png?branch=master)](https://travis-ci.org/rubenv/angular-gettext) 6 | 7 | Check the website for usage instructions: [http://angular-gettext.rocketeer.be/](http://angular-gettext.rocketeer.be/). 8 | 9 | ## License 10 | 11 | (The MIT License) 12 | 13 | Copyright (C) 2013-2018 by Ruben Vermeersch 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in 23 | all copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 31 | THE SOFTWARE. 32 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc module 3 | * @name gettext 4 | * @packageName angular-gettext 5 | * @description Super simple Gettext for Angular.JS 6 | * 7 | * A sample application can be found at https://github.com/rubenv/angular-gettext-example. 8 | * This is an adaptation of the [TodoMVC](http://todomvc.com/) example. You can use this as a guideline while adding {@link angular-gettext angular-gettext} to your own application. 9 | */ 10 | /** 11 | * @ngdoc factory 12 | * @module gettext 13 | * @name gettextPlurals 14 | * @param {String} [langCode=en] language code 15 | * @param {Number} [n=0] number to calculate form for 16 | * @returns {Number} plural form number 17 | * @description Provides correct plural form id for the given language 18 | * 19 | * Example 20 | * ```js 21 | * gettextPlurals('ru', 10); // 1 22 | * gettextPlurals('en', 1); // 0 23 | * gettextPlurals(); // 1 24 | * ``` 25 | */ 26 | angular.module('gettext', []); 27 | /** 28 | * @ngdoc object 29 | * @module gettext 30 | * @name gettext 31 | * @kind function 32 | * @param {String} str annotation key 33 | * @description Gettext constant function for annotating strings 34 | * 35 | * ```js 36 | * angular.module('myApp', ['gettext']).config(function(gettext) { 37 | * /// MyApp document title 38 | * gettext('my-app.title'); 39 | * ... 40 | * }) 41 | * ``` 42 | */ 43 | angular.module('gettext').constant('gettext', function (str) { 44 | /* 45 | * Does nothing, simply returns the input string. 46 | * 47 | * This function serves as a marker for `grunt-angular-gettext` to know that 48 | * this string should be extracted for translations. 49 | */ 50 | return str; 51 | }); 52 | -------------------------------------------------------------------------------- /.jscs.json: -------------------------------------------------------------------------------- 1 | { 2 | "requireCurlyBraces": ["if", "else", "for", "while", "do", "try", "catch"], 3 | "requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "return", "try", "catch"], 4 | "requireParenthesesAroundIIFE": true, 5 | "requireSpacesInFunctionExpression": { 6 | "beforeOpeningCurlyBrace": true 7 | }, 8 | "requireSpacesInAnonymousFunctionExpression": { 9 | "beforeOpeningRoundBrace": true 10 | }, 11 | "disallowSpacesInNamedFunctionExpression": { 12 | "beforeOpeningRoundBrace": true 13 | }, 14 | "disallowSpacesInFunctionDeclaration": { 15 | "beforeOpeningRoundBrace": true 16 | }, 17 | "disallowMultipleVarDecl": true, 18 | "requireSpacesInsideObjectBrackets": "all", 19 | "disallowQuotedKeysInObjects": "allButReserved", 20 | "disallowSpaceAfterObjectKeys": true, 21 | "requireCommaBeforeLineBreak": true, 22 | "disallowSpaceBeforeBinaryOperators": [","], 23 | "disallowSpaceAfterPrefixUnaryOperators": ["++", "--", "+", "-", "~", "!"], 24 | "disallowSpaceBeforePostfixUnaryOperators": ["++", "--"], 25 | "requireSpaceBeforeBinaryOperators": ["+", "-", "/", "*", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], 26 | "requireSpaceAfterBinaryOperators": [",", "+", "-", "/", "*", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="], 27 | "validateQuoteMarks": { "mark": true, "escape": true }, 28 | "validateIndentation": 4, 29 | "disallowTrailingWhitespace": true, 30 | "disallowKeywordsOnNewLine": ["else"], 31 | "requireCapitalizedConstructors": true, 32 | "safeContextKeyword": "self", 33 | "requireDotNotation": true, 34 | "disallowYodaConditions": true 35 | } 36 | -------------------------------------------------------------------------------- /test/unit/filter.js: -------------------------------------------------------------------------------- 1 | describe("Filter", function () { 2 | var catalog = null; 3 | var $rootScope = null; 4 | var $compile = null; 5 | 6 | beforeEach(module("gettext")); 7 | 8 | beforeEach(inject(function ($injector, gettextCatalog) { 9 | $rootScope = $injector.get("$rootScope"); 10 | $compile = $injector.get("$compile"); 11 | catalog = gettextCatalog; 12 | catalog.setStrings("nl", { 13 | Hello: "Hallo", 14 | "Hello {{name}}!": "Hallo {{name}}!", 15 | "One boat": ["Een boot", "{{count}} boten"], 16 | Archive: { verb: "Archiveren", noun: "Archief", $$noContext: "Archief (no context)" } 17 | }); 18 | })); 19 | 20 | it("Should have a translate filter", function () { 21 | var el = $compile("

{{\"Hello!\"|translate}}

")($rootScope); 22 | $rootScope.$digest(); 23 | assert.equal(el.text(), "Hello!"); 24 | }); 25 | 26 | it("Should translate known strings", function () { 27 | catalog.setCurrentLanguage("nl"); 28 | var el = $compile("{{\"Hello\"|translate}}")($rootScope); 29 | $rootScope.$digest(); 30 | assert.equal(el.text(), "Hallo"); 31 | }); 32 | 33 | it("Should translate known strings according to translate context", function () { 34 | catalog.setCurrentLanguage("nl"); 35 | var el = $compile("{{\"Archive\"|translate:'verb'}}")($rootScope); 36 | $rootScope.$digest(); 37 | assert.equal(el.text(), "Archiveren"); 38 | el = $compile("{{\"Archive\"|translate:'noun'}}")($rootScope); 39 | $rootScope.$digest(); 40 | assert.equal(el.text(), "Archief"); 41 | el = $compile("{{\"Archive\"|translate}}")($rootScope); 42 | $rootScope.$digest(); 43 | assert.equal(el.text(), "Archief (no context)"); 44 | }); 45 | 46 | it("Can use filter in attribute values", function () { 47 | catalog.setCurrentLanguage("nl"); 48 | var el = $compile("")($rootScope); 49 | $rootScope.$digest(); 50 | assert.equal(el.attr("placeholder"), "Hallo"); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/unit/plurals.js: -------------------------------------------------------------------------------- 1 | describe("Plurals", function () { 2 | var plurals = null; 3 | 4 | beforeEach(module("gettext")); 5 | 6 | beforeEach(inject(function (gettextPlurals) { 7 | plurals = gettextPlurals; 8 | })); 9 | 10 | it("Plural form of singular english is 0", function () { 11 | assert.equal(plurals("en", 1), 0); 12 | }); 13 | it("Plural form of plural english is 1", function () { 14 | assert.equal(plurals("en", 2), 1); 15 | }); 16 | it("Plural form of zero in english is 1", function () { 17 | assert.equal(plurals("en", 0), 1); 18 | }); 19 | it("Plural form of singular english (locale en_US) is 0", function () { 20 | assert.equal(plurals("en_US", 1), 0); 21 | }); 22 | it("Plural form of plural english (locale en_US) is 1", function () { 23 | assert.equal(plurals("en_US", 2), 1); 24 | }); 25 | it("Plural form of zero in english (locale en_US) is 1", function () { 26 | assert.equal(plurals("en_US", 0), 1); 27 | }); 28 | it("Plural form of singular english (locale en-US) is 0", function () { 29 | assert.equal(plurals("en-US", 1), 0); 30 | }); 31 | it("Plural form of plural english (locale en-US) is 1", function () { 32 | assert.equal(plurals("en-US", 2), 1); 33 | }); 34 | it("Plural form of zero in english (locale en-US) is 1", function () { 35 | assert.equal(plurals("en-US", 0), 1); 36 | }); 37 | it("Plural form of singular dutch is 0", function () { 38 | assert.equal(plurals("nl", 1), 0); 39 | }); 40 | it("Plural form of plural dutch is 1", function () { 41 | assert.equal(plurals("nl", 2), 1); 42 | }); 43 | it("Plural form of zero in dutch is 1", function () { 44 | assert.equal(plurals("nl", 0), 1); 45 | }); 46 | it("Plural form of singular french is 0", function () { 47 | assert.equal(plurals("fr", 1), 0); 48 | }); 49 | it("Plural form of plural french is 1", function () { 50 | assert.equal(plurals("fr", 2), 1); 51 | }); 52 | it("Plural form of zero in french is 0", function () { 53 | assert.equal(plurals("fr", 0), 0); 54 | }); 55 | it("Plural form of 27 in arabic is 4", function () { 56 | assert.equal(plurals("ar", 27), 4); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /docs/api/index.ngdoc: -------------------------------------------------------------------------------- 1 | @ngdoc overview 2 | @name angular-gettext 3 | @area api 4 | @id api-index 5 | @description Super-simple translation support for Angular.JS 6 | 7 | ## Effortless translations 8 | Angular-gettext let's you focus on developing your application. 9 | Just write everything in English and annotate which parts should be translated. The tools do the rest. 10 | 11 | Marking a string as translatable is as simple as adding an attribute: 12 | ```html 13 | Home 14 | ``` 15 | 16 | No need to maintain translation codes or magic values, that just causes headaches! 17 | 18 | ## Seamless Angular.JS integration 19 | Translating your application doesn't mean you have to give up any of the good stuff that Angular.JS provides. 20 | Interpolation and everything else we love and are used to just keep on working: 21 | 22 | ```html 23 | Hello {{name}} 24 | ``` 25 | 26 | And with a minified footprint of less than 4kb, you don't have to worry about the size of your application. 27 | Add gzip compression and it amounts to less than 1.5kb. 28 | 29 | ## Correct plurals in all languages 30 | Not every language works like English. Did you know that Polish uses three plural forms? Or that Irish uses five? No worries, `angular-gettext` handles all of this for you. Just provide a plural string where needed. 31 | 32 | ```html 33 | 1 new message 34 | ``` 35 | 36 | The span above will always show a correctly pluralized message, even if the language uses wildly different pluralization rules. 37 | 38 | A full list of supported languages (over 130) can be found [here](http://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/pluralforms.html?id=l10n/pluralforms). 39 | 40 | ## Rich tool support 41 | The widely used [gettext](http://en.wikipedia.org/wiki/Gettext) format is used in `angular-gettext` (hence the name). 42 | This means you can use widely established translation tools like [Poedit](http://www.poedit.net/) or online [Free PO editor](https://pofile.net/free-po-editor). 43 | Or you can use an online translation platform like [Pootle](http://pootle.translatehouse.org/), [Transifex](https://www.transifex.com/), or [Zanata](http://www.zanata.org/). 44 | 45 | The upside of this? Non-technical users can help out by translating. Or you can use professional translation services. Better results, faster. 46 | -------------------------------------------------------------------------------- /test/fixtures/playground.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Click test 5 | 6 | 7 | 8 | 9 | 10 | 49 | 50 | 51 |

Plain string: Hello

52 |

With var: Hello {{name}}

53 |

Action:

54 |

Action with var:

55 |

Plural: There is {{count}} bird

56 |

With ng-if: Hello

57 |

Hello {{name}}

58 |
59 |

60 |

61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /test/unit/loading.js: -------------------------------------------------------------------------------- 1 | describe("String loading", function () { 2 | var catalog = null; 3 | var $httpBackend = null; 4 | 5 | beforeEach(module("gettext")); 6 | beforeEach(inject(function ($injector) { 7 | catalog = $injector.get("gettextCatalog"); 8 | $httpBackend = $injector.get("$httpBackend"); 9 | })); 10 | 11 | afterEach(function () { 12 | $httpBackend.verifyNoOutstandingExpectation(); 13 | $httpBackend.verifyNoOutstandingRequest(); 14 | }); 15 | 16 | it("Will load remote strings", function () { 17 | catalog.loadRemote("/strings/nl.json"); 18 | $httpBackend.expectGET("/strings/nl.json").respond(200); 19 | $httpBackend.flush(); 20 | }); 21 | 22 | it("Will set the loaded strings", function () { 23 | catalog.loadRemote("/strings/nl.json"); 24 | $httpBackend.expectGET("/strings/nl.json").respond(200, { 25 | nl: { 26 | Hello: "Hallo" 27 | } 28 | }); 29 | $httpBackend.flush(); 30 | assert.notEqual(void 0, catalog.strings.nl); 31 | }); 32 | 33 | it("Returns a promise", function () { 34 | var called; 35 | called = false; 36 | catalog.loadRemote("/strings/nl.json").then(function () { 37 | called = true; 38 | }); 39 | $httpBackend.expectGET("/strings/nl.json").respond(200); 40 | $httpBackend.flush(); 41 | assert(called); 42 | }); 43 | 44 | it("Returns a promise (failure)", function () { 45 | var successCalled = false; 46 | var failedCalled = false; 47 | var promise = catalog.loadRemote("/strings/nl.json"); 48 | function success() { 49 | successCalled = true; 50 | } 51 | function failed() { 52 | failedCalled = true; 53 | } 54 | promise.then(success, failed); 55 | $httpBackend.expectGET("/strings/nl.json").respond(404); 56 | $httpBackend.flush(); 57 | assert(!successCalled); 58 | assert(failedCalled); 59 | }); 60 | 61 | it("Exposes the loaded data in the promise", function () { 62 | var responseIsPresent = null; 63 | catalog.loadRemote("/strings/nl.json").then(function (response) { 64 | responseIsPresent = response; 65 | }); 66 | $httpBackend.expectGET("/strings/nl.json").respond(200); 67 | $httpBackend.flush(); 68 | assert(responseIsPresent); 69 | }); 70 | 71 | it("Caches strings", function () { 72 | catalog.loadRemote("/strings/nl.json"); 73 | $httpBackend.expectGET("/strings/nl.json").respond(200, { 74 | nl: { 75 | Hello: "Hallo" 76 | } 77 | }); 78 | $httpBackend.flush(); 79 | catalog.loadRemote("/strings/nl.json"); 80 | $httpBackend.verifyNoOutstandingRequest(); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc factory 3 | * @module gettext 4 | * @name gettextUtil 5 | * @description Utility service for common operations and polyfills. 6 | */ 7 | angular.module('gettext').factory('gettextUtil', function gettextUtil() { 8 | /** 9 | * @ngdoc method 10 | * @name gettextUtil#trim 11 | * @public 12 | * @param {string} value String to be trimmed. 13 | * @description Trim polyfill for old browsers (instead of jQuery). Based on AngularJS-v1.2.2 (angular.js#620). 14 | * 15 | * Example 16 | * ```js 17 | * gettextUtil.assert(' no blanks '); // "no blanks" 18 | * ``` 19 | */ 20 | var trim = (function () { 21 | if (!String.prototype.trim) { 22 | return function (value) { 23 | return (typeof value === 'string') ? value.replace(/^\s*/, '').replace(/\s*$/, '') : value; 24 | }; 25 | } 26 | return function (value) { 27 | return (typeof value === 'string') ? value.trim() : value; 28 | }; 29 | })(); 30 | 31 | /** 32 | * @ngdoc method 33 | * @name gettextUtil#assert 34 | * @public 35 | * @param {bool} condition condition to check 36 | * @param {String} missing name of the directive missing attribute 37 | * @param {String} found name of attribute that has been used with directive 38 | * @description Throws error if condition is not met, which means that directive was used with certain parameter 39 | * that requires another one (which is missing). 40 | * 41 | * Example 42 | * ```js 43 | * gettextUtil.assert(!attrs.translatePlural || attrs.translateN, 'translate-n', 'translate-plural'); 44 | * //You should add a translate-n attribute whenever you add a translate-plural attribute. 45 | * ``` 46 | */ 47 | function assert(condition, missing, found) { 48 | if (!condition) { 49 | throw new Error('You should add a ' + missing + ' attribute whenever you add a ' + found + ' attribute.'); 50 | } 51 | } 52 | 53 | /** 54 | * @ngdoc method 55 | * @name gettextUtil#startsWith 56 | * @public 57 | * @param {string} target String on which checking will occur. 58 | * @param {string} query String expected to be at the beginning of target. 59 | * @returns {boolean} Returns true if object has no ownProperties. For arrays returns true if length == 0. 60 | * @description Checks if string starts with another string. 61 | * 62 | * Example 63 | * ```js 64 | * gettextUtil.startsWith('Home sweet home.', 'Home'); //true 65 | * gettextUtil.startsWith('Home sweet home.', 'sweet'); //false 66 | * ``` 67 | */ 68 | function startsWith(target, query) { 69 | return target.indexOf(query) === 0; 70 | } 71 | 72 | /** 73 | * @ngdoc method 74 | * @name gettextUtil#lcFirst 75 | * @public 76 | * @param {string} target String to transform. 77 | * @returns {string} Strings beginning with lowercase letter. 78 | * @description Makes first letter of the string lower case 79 | * 80 | * Example 81 | * ```js 82 | * gettextUtil.lcFirst('Home Sweet Home.'); //'home Sweet Home' 83 | * gettextUtil.lcFirst('ShouldBeCamelCase.'); //'shouldBeCamelCase' 84 | * ``` 85 | */ 86 | function lcFirst(target) { 87 | var first = target.charAt(0).toLowerCase(); 88 | return first + target.substr(1); 89 | } 90 | 91 | return { 92 | trim: trim, 93 | assert: assert, 94 | startsWith: startsWith, 95 | lcFirst: lcFirst 96 | }; 97 | }); 98 | -------------------------------------------------------------------------------- /genplurals.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Adapted from Aaron Bockover's Vernacular: https://github.com/rdio/vernacular/ 4 | 5 | """ 6 | Copyright 2012 Rdio, Inc. 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in 16 | all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | THE SOFTWARE. 25 | """ 26 | 27 | import urllib.request 28 | from html.parser import HTMLParser 29 | from html.entities import name2codepoint 30 | import re 31 | 32 | url = 'http://translate.sourceforge.net/wiki/l10n/pluralforms' 33 | html = urllib.request.urlopen(url).read().decode('utf-8') 34 | 35 | class Parser(HTMLParser): 36 | def __init__(self): 37 | HTMLParser.__init__(self) 38 | 39 | self.data = '' 40 | self.current_node = [] 41 | self.in_td = False 42 | self.below_td = 0 43 | 44 | self.rules = {} 45 | 46 | def handle_current_node(self): 47 | code, name, rule = self.current_node 48 | m = re.match(r'^ *nplurals *=*(\d+); *plural *=(.*);', rule) 49 | if not m: 50 | return 51 | 52 | nplurals = int(m.group(1)) 53 | rule = m.group(2).replace(';', '').strip() 54 | 55 | rule = re.sub(r'^\(?n *([\<\>\!\=]{1,2}) *(\d+)\)?$', r'n\1\2 ? 1 : 0', rule) 56 | rule = rule.replace('and', '&&') 57 | rule = rule.replace('or', '||') 58 | 59 | if '?' not in rule and rule != '0': 60 | rule += ' ? 1 : 0' 61 | 62 | if rule == 'n!=1 ? 1 : 0': 63 | return 64 | 65 | if rule in self.rules: 66 | self.rules[rule].append((code, name, nplurals)) 67 | else: 68 | self.rules[rule] = [(code, name, nplurals)] 69 | 70 | def handle_starttag(self, tag, attrs): 71 | if self.in_td: 72 | self.below_td += 1 73 | return 74 | self.in_td = tag == 'td' 75 | 76 | def handle_endtag(self, tag): 77 | if self.below_td: 78 | self.below_td -= 1 79 | return 80 | if not self.in_td or tag != 'td': 81 | return 82 | 83 | self.in_td = False 84 | self.data = self.data.strip() 85 | 86 | field = len(self.current_node) 87 | 88 | if (field == 0 and re.match(r'^[a-zA-Z_]{2,5}$', self.data)) or field in [1, 2]: 89 | self.current_node.append(self.data) 90 | if field == 2: 91 | self.handle_current_node() 92 | self.current_node = [] 93 | else: 94 | self.current_node = [] 95 | 96 | self.data = '' 97 | 98 | def handle_data(self, data): 99 | if self.in_td and self.below_td == 0: 100 | self.data += data 101 | 102 | def handle_entityref(self, name): 103 | if self.in_td: 104 | self.data += chr(name2codepoint[name]) 105 | 106 | parser = Parser() 107 | parser.feed(html) 108 | 109 | rules = [rule for rule in parser.rules.items()] 110 | rules.sort(key = lambda rule: (str(rule[1][0][2]) + rule[0])) 111 | 112 | print('// Do not edit this file, it is autogenerated using genplurals.py!'); 113 | print('angular.module("gettext").factory("gettextPlurals", function () {'); 114 | print(' return function (langCode, n) {') 115 | print(' switch (langCode) {') 116 | for rule, langs in rules: 117 | last_forms = 0 118 | langs.sort(key = lambda lang: lang[0]) 119 | for code, name, forms in langs: 120 | last_forms = forms 121 | space = ' ' 122 | if len(code) == 3: 123 | space = ' ' 124 | print(' case "%s":%s// %s' % (code, space, name)) 125 | if last_forms == 1: 126 | print(' // %d form' % last_forms) 127 | else: 128 | print(' // %d forms' % last_forms) 129 | print(' return %s;' % rule) 130 | print(' default: // Everything else') 131 | print(' return n != 1 ? 1 : 0;') 132 | print(' }') 133 | print(' }') 134 | print('});') 135 | -------------------------------------------------------------------------------- /dist/angular-gettext.min.js: -------------------------------------------------------------------------------- 1 | angular.module("gettext",[]),angular.module("gettext").constant("gettext",function(t){return t}),angular.module("gettext").factory("gettextCatalog",["gettextPlurals","gettextFallbackLanguage","$http","$cacheFactory","$interpolate","$rootScope",function(i,u,e,t,c,r){var a,l="$$noContext",n='test',g=angular.element(""+n+"").html()!==n,o=function(t){return a.debug&&a.currentLanguage!==a.baseLanguage?a.debugPrefix+t:t},f=function(t){return a.showTranslatedMarkers?a.translatedMarkerPrefix+t+a.translatedMarkerSuffix:t};function h(){r.$broadcast("gettextLanguageChanged")}return a={debug:!1,debugPrefix:"[MISSING]: ",showTranslatedMarkers:!1,translatedMarkerPrefix:"[",translatedMarkerSuffix:"]",strings:{},baseLanguage:"en",currentLanguage:"en",cache:t("strings"),setCurrentLanguage:function(t){this.currentLanguage=t,h()},getCurrentLanguage:function(){return this.currentLanguage},setStrings:function(t,e){this.strings[t]||(this.strings[t]={});var r=i(t,1);for(var a in e){var n=e[a];if(g&&(a=angular.element(""+a+"").html()),angular.isString(n)||angular.isArray(n)){var s={};s[l]=n,n=s}for(var u in this.strings[t][a]||(this.strings[t][a]={}),n){var c=n[u];angular.isArray(c)?this.strings[t][a][u]=c:(this.strings[t][a][u]=[],this.strings[t][a][u][r]=c)}}h()},getStringFormFor:function(t,e,r,a){return t?(((this.strings[t]||{})[e]||{})[a||l]||[])[i(t,r)]:null},getString:function(t,e,r){var a=u(this.currentLanguage);return t=this.getStringFormFor(this.currentLanguage,t,1,r)||this.getStringFormFor(a,t,1,r)||o(t),t=e?c(t)(e):t,f(t)},getPlural:function(t,e,r,a,n){var s=u(this.currentLanguage);return e=this.getStringFormFor(this.currentLanguage,e,t,n)||this.getStringFormFor(s,e,t,n)||o(1===t?e:r),a&&(a.$count=t,e=c(e)(a)),f(e)},loadRemote:function(t){return e({method:"GET",url:t,cache:a.cache}).then(function(t){var e=t.data;for(var r in e)a.setStrings(r,e[r]);return t})}}}]),angular.module("gettext").directive("translate",["gettextCatalog","$parse","$animate","$compile","$window","gettextUtil",function(h,a,m,d,t,v){var n=parseInt((/msie (\d+)/i.exec(t.navigator.userAgent)||[])[1],10),c="translateParams";function x(e,r,n){var t=Object.keys(r).filter(function(t){return v.startsWith(t,c)&&t!==c});if(!t.length)return null;var s=e.$new(),u=[];return t.forEach(function(a){var t=e.$watch(r[a],function(t){var e,r=(e=a,v.lcFirst(e.replace(c,"")));s[r]=t,n(s)});u.push(t)}),e.$on("$destroy",function(){u.forEach(function(t){t()}),s.$destroy()}),s}return{restrict:"AE",terminal:!0,compile:function(t,e){var r=e.translate;if(!r||!r.match(/^yes|no$/i)){v.assert(!e.translatePlural||e.translateN,"translate-n","translate-plural"),v.assert(!e.translateN||e.translatePlural,"translate-plural","translate-n");var g=v.trim(t.html()),o=e.translatePlural,f=e.translateContext;return n<=8&&"\x3c!--IE fix--\x3e"===g.slice(-13)&&(g=g.slice(0,-13)),{post:function(s,u,t){var c=a(t.translateN),i=null,l=!0;function e(t){var e;t=t||null,o?((s=i||(i=s.$new())).$count=c(s),e=h.getPlural(s.$count,g,o,null,f)):e=h.getString(g,null,f);var r=u.contents();if(r||e)if(e!==v.trim(r.html())){var a=angular.element(""+e+"");d(a.contents())(t||s);var n=a.contents();m.enter(n,u),m.leave(r)}else l&&d(r)(s)}var r=x(s,t,e);e(r),l=!1,t.translateN&&s.$watch(t.translateN,function(){e(r)}),s.$on("gettextLanguageChanged",function(){e(r)})}}}}}}]),angular.module("gettext").factory("gettextFallbackLanguage",function(){var r={},a=/([^_]+)_[^_]+$/;return function(t){if(r[t])return r[t];var e=a.exec(t);return e?(r[t]=e[1],e[1]):null}}),angular.module("gettext").filter("translate",["gettextCatalog",function(r){function t(t,e){return r.getString(t,null,e)}return t.$stateful=!0,t}]),angular.module("gettext").factory("gettextPlurals",function(){var r={pt_BR:"pt_BR","pt-BR":"pt_BR"};return function(t,e){switch(function(t){r[t]||(r[t]=t.split(/\-|_/).shift());return r[t]}(t)){case"ay":case"bo":case"cgg":case"dz":case"fa":case"id":case"ja":case"jbo":case"ka":case"kk":case"km":case"ko":case"ky":case"lo":case"ms":case"my":case"sah":case"su":case"th":case"tt":case"ug":case"vi":case"wo":case"zh":return 0;case"is":return e%10!=1||e%100==11?1:0;case"jv":return 0!=e?1:0;case"mk":return 1==e||e%10==1?0:1;case"ach":case"ak":case"am":case"arn":case"br":case"fil":case"fr":case"gun":case"ln":case"mfe":case"mg":case"mi":case"oc":case"pt_BR":case"tg":case"ti":case"tr":case"uz":case"wa":case"zh":return 11 ? 1 : 0; 66 | case "lv": // Latvian 67 | // 3 forms 68 | return (n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2); 69 | case "lt": // Lithuanian 70 | // 3 forms 71 | return (n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2); 72 | case "be": // Belarusian 73 | case "bs": // Bosnian 74 | case "hr": // Croatian 75 | case "ru": // Russian 76 | case "sr": // Serbian 77 | case "uk": // Ukrainian 78 | // 3 forms 79 | return (n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2); 80 | case "mnk": // Mandinka 81 | // 3 forms 82 | return (n==0 ? 0 : n==1 ? 1 : 2); 83 | case "ro": // Romanian 84 | // 3 forms 85 | return (n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2); 86 | case "pl": // Polish 87 | // 3 forms 88 | return (n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2); 89 | case "cs": // Czech 90 | case "sk": // Slovak 91 | // 3 forms 92 | return (n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2; 93 | case "sl": // Slovenian 94 | // 4 forms 95 | return (n%100==1 ? 1 : n%100==2 ? 2 : n%100==3 || n%100==4 ? 3 : 0); 96 | case "mt": // Maltese 97 | // 4 forms 98 | return (n==1 ? 0 : n==0 || ( n%100>1 && n%100<11) ? 1 : (n%100>10 && n%100<20 ) ? 2 : 3); 99 | case "gd": // Scottish Gaelic 100 | // 4 forms 101 | return (n==1 || n==11) ? 0 : (n==2 || n==12) ? 1 : (n > 2 && n < 20) ? 2 : 3; 102 | case "cy": // Welsh 103 | // 4 forms 104 | return (n==1) ? 0 : (n==2) ? 1 : (n != 8 && n != 11) ? 2 : 3; 105 | case "kw": // Cornish 106 | // 4 forms 107 | return (n==1) ? 0 : (n==2) ? 1 : (n == 3) ? 2 : 3; 108 | case "ga": // Irish 109 | // 5 forms 110 | return n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n<11 ? 3 : 4; 111 | case "ar": // Arabic 112 | // 6 forms 113 | return (n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5); 114 | default: // Everything else 115 | return n != 1 ? 1 : 0; 116 | } 117 | }; 118 | 119 | /** 120 | * Method extracts iso639-2 language code from code with locale e.g. pl_PL, en_US, etc. 121 | * If it's provided with standalone iso639-2 language code it simply returns it. 122 | * @param {String} langCode 123 | * @returns {String} iso639-2 language Code 124 | */ 125 | function getLanguageCode(langCode) { 126 | if (!languageCodes[langCode]) { 127 | languageCodes[langCode] = langCode.split(/\-|_/).shift(); 128 | } 129 | return languageCodes[langCode]; 130 | } 131 | }); 132 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | var serveStatic = require("serve-static"); 2 | 3 | module.exports = function (grunt) { 4 | grunt.loadNpmTasks("dgeni-alive"); 5 | grunt.loadNpmTasks("grunt-bump"); 6 | grunt.loadNpmTasks("grunt-contrib-clean"); 7 | grunt.loadNpmTasks("grunt-contrib-concat"); 8 | grunt.loadNpmTasks("grunt-contrib-connect"); 9 | grunt.loadNpmTasks("grunt-contrib-jshint"); 10 | grunt.loadNpmTasks("grunt-contrib-uglify"); 11 | grunt.loadNpmTasks("grunt-contrib-watch"); 12 | grunt.loadNpmTasks("grunt-jscs"); 13 | grunt.loadNpmTasks("grunt-karma"); 14 | grunt.loadNpmTasks("grunt-ng-annotate"); 15 | grunt.loadNpmTasks("grunt-protractor-runner"); 16 | grunt.loadNpmTasks("grunt-shell"); 17 | 18 | grunt.initConfig({ 19 | pkg: grunt.file.readJSON("package.json"), 20 | 21 | jshint: { 22 | all: ["Gruntfile.js", "{src,test}/**/*.js", "!src/plural.js"], 23 | options: { 24 | jshintrc: ".jshintrc" 25 | } 26 | }, 27 | 28 | jscs: { 29 | src: { 30 | options: { 31 | config: ".jscs.json" 32 | }, 33 | files: { 34 | src: ["Gruntfile.js", "{src,test}/**/*.js", "!src/plural.js"] 35 | } 36 | } 37 | }, 38 | 39 | concat: { 40 | dist: { 41 | files: { 42 | "dist/angular-gettext.js": ["src/index.js", "src/*.js"] 43 | } 44 | } 45 | }, 46 | 47 | uglify: { 48 | dist: { 49 | files: { 50 | "dist/angular-gettext.min.js": "dist/angular-gettext.js" 51 | } 52 | } 53 | }, 54 | 55 | clean: { 56 | all: ["dist"] 57 | }, 58 | 59 | watch: { 60 | options: { 61 | livereload: true 62 | }, 63 | all: { 64 | files: ["src/**.js", "test/*/*"], 65 | tasks: ["build", "karma:unit:run", "karma:unit_nojquery:run", "protractor:dev"] 66 | }, 67 | unit: { 68 | files: ["src/**.js", "test/unit/*"], 69 | tasks: ["build", "karma:unit:run", "karma:unit_nojquery:run"] 70 | }, 71 | e2e: { 72 | files: ["src/**.js", "test/{e2e,fixtures}/*"], 73 | tasks: ["build", "protractor:dev"] 74 | } 75 | }, 76 | 77 | ngAnnotate: { 78 | dist: { 79 | files: { 80 | "dist/angular-gettext.js": "dist/angular-gettext.js" 81 | } 82 | } 83 | }, 84 | 85 | connect: { 86 | e2e: { 87 | options: { 88 | port: 9000, 89 | hostname: "0.0.0.0", 90 | middleware: function () { 91 | return [serveStatic(__dirname)]; 92 | } 93 | } 94 | } 95 | }, 96 | 97 | karma: { 98 | unit: { 99 | configFile: "test/configs/unit.conf.js", 100 | browsers: ["PhantomJS"], 101 | background: true 102 | }, 103 | unit_nojquery: { 104 | configFile: "test/configs/unit-nojquery.conf.js", 105 | browsers: ["PhantomJS"], 106 | background: true 107 | }, 108 | unitci: { 109 | configFile: "test/configs/unit.conf.js", 110 | browsers: ["PhantomJS"], 111 | singleRun: true, 112 | reporters: ["dots"] 113 | }, 114 | unitci_nojquery: { 115 | configFile: "test/configs/unit-nojquery.conf.js", 116 | browsers: ["PhantomJS"], 117 | singleRun: true, 118 | reporters: ["dots"] 119 | } 120 | }, 121 | 122 | protractor: { 123 | options: { 124 | noColor: false, 125 | configFile: "test/configs/e2e.conf.js" 126 | }, 127 | dev: { 128 | options: { 129 | keepAlive: true, 130 | args: { 131 | directConnect: true 132 | } 133 | } 134 | }, 135 | ci: { 136 | options: { 137 | args: { 138 | capabilities: { 139 | browserName: "chrome", 140 | chromeOptions: { 141 | args: ["headless", "window-size=1920,1080"] 142 | } 143 | } 144 | } 145 | } 146 | } 147 | }, 148 | 149 | bump: { 150 | options: { 151 | files: ["package.json", "bower.json"], 152 | commitFiles: ["-a"], 153 | pushTo: "origin" 154 | } 155 | }, 156 | 157 | shell: { 158 | protractor_update: { 159 | command: "./node_modules/.bin/webdriver-manager update", 160 | options: { 161 | stdout: true 162 | } 163 | } 164 | }, 165 | 166 | "dgeni-alive": { 167 | options: { 168 | serve: { 169 | port: "10000", 170 | openBrowser: true 171 | } 172 | }, 173 | api: { 174 | title: "<%= pkg.title %>", 175 | version: "<%= pkg.version %>", 176 | expand: false, 177 | src: [ 178 | "src/**/*.js", 179 | "docs/**/*.ngdoc" 180 | ], 181 | dest: "dist/docs" 182 | } 183 | } 184 | }); 185 | 186 | grunt.registerTask("default", ["test"]); 187 | grunt.registerTask("build", ["clean", "jshint", "jscs", "concat", "ngAnnotate", "uglify"]); 188 | grunt.registerTask("test", ["build", "shell:protractor_update", "connect:e2e", "karma:unit", "karma:unit_nojquery", "protractor:dev", "watch:all"]); 189 | grunt.registerTask("test_unit", ["build", "shell:protractor_update", "karma:unit", "karma:unit_nojquery", "watch:unit"]); 190 | grunt.registerTask("test_e2e", ["build", "shell:protractor_update", "connect:e2e", "protractor:dev", "watch:e2e"]); 191 | grunt.registerTask("ci", ["build", "shell:protractor_update", "karma:unitci", "karma:unitci_nojquery", "connect:e2e", "protractor:ci"]); 192 | }; 193 | -------------------------------------------------------------------------------- /test/unit/catalog.js: -------------------------------------------------------------------------------- 1 | describe("Catalog", function () { 2 | var catalog = null; 3 | 4 | beforeEach(module("gettext")); 5 | 6 | beforeEach(inject(function (gettextCatalog) { 7 | catalog = gettextCatalog; 8 | })); 9 | 10 | it("Can set strings", function () { 11 | var strings = { Hello: "Hallo" }; 12 | assert.deepEqual(catalog.strings, {}); 13 | catalog.setStrings("nl", strings); 14 | assert.deepEqual(catalog.strings.nl.Hello.$$noContext[0], "Hallo"); 15 | }); 16 | 17 | it("Can retrieve strings", function () { 18 | var strings = { Hello: "Hallo" }; 19 | catalog.setStrings("nl", strings); 20 | catalog.setCurrentLanguage("nl"); 21 | assert.equal(catalog.getString("Hello"), "Hallo"); 22 | }); 23 | 24 | it("Can set and retrieve strings when default plural is not zero", function () { 25 | var strings = { Hello: "Hallo" }; 26 | catalog.setStrings("ar", strings); 27 | catalog.setCurrentLanguage("ar"); 28 | assert.equal(catalog.getString("Hello"), "Hallo"); 29 | }); 30 | 31 | it("Should return original for unknown strings", function () { 32 | var strings = { Hello: "Hallo" }; 33 | catalog.setStrings("nl", strings); 34 | catalog.setCurrentLanguage("nl"); 35 | assert.equal(catalog.getString("Bye"), "Bye"); 36 | }); 37 | 38 | it("Should return original for unknown languages", function () { 39 | catalog.setCurrentLanguage("fr"); 40 | assert.equal(catalog.getString("Hello"), "Hello"); 41 | }); 42 | 43 | it("Should add prefix for untranslated strings when in debug", function () { 44 | catalog.debug = true; 45 | catalog.setCurrentLanguage("fr"); 46 | assert.equal(catalog.getString("Hello"), "[MISSING]: Hello"); 47 | }); 48 | 49 | it("Should add custom prefix for untranslated strings when in debug", function () { 50 | catalog.debug = true; 51 | catalog.debugPrefix = "#X# "; 52 | catalog.setCurrentLanguage("fr"); 53 | assert.equal(catalog.getString("Hello"), "#X# Hello"); 54 | }); 55 | 56 | it("Should not add prefix for untranslated strings in English", function () { 57 | catalog.debug = true; 58 | catalog.setCurrentLanguage("en"); 59 | assert.equal(catalog.getString("Hello"), "Hello"); 60 | }); 61 | 62 | it("Should not add prefix for untranslated strings in preferred language", function () { 63 | catalog.debug = true; 64 | catalog.setCurrentLanguage(catalog.baseLanguage); 65 | assert.equal(catalog.getString("Hello"), "Hello"); 66 | }); 67 | 68 | it("Should return singular for unknown singular strings", function () { 69 | assert.equal(catalog.getPlural(1, "Bird", "Birds"), "Bird"); 70 | }); 71 | 72 | it("Should return plural for unknown plural strings", function () { 73 | assert.equal(catalog.getPlural(2, "Bird", "Birds"), "Birds"); 74 | }); 75 | 76 | it("Should return singular for singular strings", function () { 77 | catalog.setCurrentLanguage("nl"); 78 | catalog.setStrings("nl", { 79 | Bird: ["Vogel", "Vogels"] 80 | }); 81 | assert.equal(catalog.getPlural(1, "Bird", "Birds"), "Vogel"); 82 | }); 83 | 84 | it("Should return plural for plural strings", function () { 85 | catalog.setCurrentLanguage("nl"); 86 | catalog.setStrings("nl", { 87 | Bird: ["Vogel", "Vogels"] 88 | }); 89 | assert.equal(catalog.getPlural(2, "Bird", "Birds"), "Vogels"); 90 | }); 91 | 92 | it("Should add prefix for untranslated plural strings when in debug (single)", function () { 93 | catalog.debug = true; 94 | catalog.setCurrentLanguage("nl"); 95 | assert.equal(catalog.getPlural(1, "Bird", "Birds"), "[MISSING]: Bird"); 96 | }); 97 | 98 | it("Should add prefix for untranslated plural strings when in debug", function () { 99 | catalog.debug = true; 100 | catalog.setCurrentLanguage("nl"); 101 | assert.equal(catalog.getPlural(2, "Bird", "Birds"), "[MISSING]: Birds"); 102 | }); 103 | 104 | it("Can return an interpolated string", function () { 105 | var strings = { "Hello {{name}}!": "Hallo {{name}}!" }; 106 | assert.deepEqual(catalog.strings, {}); 107 | catalog.setCurrentLanguage("nl"); 108 | catalog.setStrings("nl", strings); 109 | assert.equal(catalog.getString("Hello {{name}}!", { name: "Andrew" }), "Hallo Andrew!"); 110 | }); 111 | 112 | it("Can return a pure interpolated string", function () { 113 | var strings = { "{{name}}": "{{name}}" }; 114 | assert.deepEqual(catalog.strings, {}); 115 | catalog.setCurrentLanguage("nl"); 116 | catalog.setStrings("nl", strings); 117 | assert.equal(catalog.getString("{{name}}", { name: "Andrew" }), "Andrew"); 118 | }); 119 | 120 | it("Can return an interpolated plural string", function () { 121 | assert.deepEqual(catalog.strings, {}); 122 | catalog.setCurrentLanguage("en"); 123 | catalog.setStrings("en", { 124 | "There is {{count}} bird": ["There is {{count}} bird", "There are {{count}} birds"] 125 | }); 126 | assert.equal(catalog.getPlural(2, "There is {{count}} bird", "There are {{count}} birds", { count: 2 }), "There are 2 birds"); 127 | assert.equal(catalog.getPlural(1, "There is {{count}} bird", "There are {{count}} birds", { count: 1 }), "There is 1 bird"); 128 | }); 129 | 130 | it("Should add translation markers when enabled", function () { 131 | catalog.showTranslatedMarkers = true; 132 | assert.equal(catalog.getString("Bye"), "[Bye]"); 133 | }); 134 | 135 | it("Should add custom translation markers when enabled", function () { 136 | catalog.showTranslatedMarkers = true; 137 | catalog.translatedMarkerPrefix = "(TRANS: "; 138 | catalog.translatedMarkerSuffix = ")"; 139 | assert.equal(catalog.getString("Bye"), "(TRANS: Bye)"); 140 | }); 141 | 142 | it("Should add prefix for untranslated strings and add translation markers when enabled", function () { 143 | catalog.debug = true; 144 | catalog.showTranslatedMarkers = true; 145 | catalog.setCurrentLanguage("fr"); 146 | assert.equal(catalog.getString("Bye"), "[[MISSING]: Bye]"); 147 | }); 148 | 149 | it("Understands contexts in the string catalog format", function () { 150 | catalog.setCurrentLanguage("nl"); 151 | catalog.setStrings("nl", { 152 | Cat: "Kat", // Single string 153 | "1 boat": ["1 boot", "{{$count}} boten"], // Plural 154 | Archive: { verb: "Archiveren", noun: "Archief" } // Contexts 155 | }); 156 | 157 | assert.equal(catalog.getString("Cat"), "Kat"); 158 | assert.equal(catalog.getPlural(1, "1 boat", "{{$count}} boats", {}), "1 boot"); 159 | assert.equal(catalog.getPlural(2, "1 boat", "{{$count}} boats", {}), "2 boten"); 160 | assert.equal(catalog.getString("Archive", {}, "verb"), "Archiveren"); 161 | assert.equal(catalog.getString("Archive", {}, "noun"), "Archief"); 162 | }); 163 | 164 | it("Should return string from fallback language if current language has no translation", function () { 165 | var strings = { Hello: "Hallo" }; 166 | catalog.setStrings("nl", strings); 167 | catalog.setCurrentLanguage("nl_NL"); 168 | assert.equal(catalog.getString("Bye"), "Bye"); 169 | assert.equal(catalog.getString("Hello"), "Hallo"); 170 | }); 171 | 172 | it("Should not return string from fallback language if current language has translation", function () { 173 | var stringsEn = { Baggage: "Baggage" }; 174 | var stringsEnGB = { Baggage: "Luggage" }; 175 | catalog.setStrings("en", stringsEn); 176 | catalog.setStrings("en_GB", stringsEnGB); 177 | catalog.setCurrentLanguage("en_GB"); 178 | assert.equal(catalog.getString("Bye"), "Bye"); 179 | assert.equal(catalog.getString("Baggage"), "Luggage"); 180 | }); 181 | 182 | it("Should handle multiple context when loaded from separate files", function () { 183 | catalog.setStrings("en", { "Multibillion-Dollar": { rich: "nothing" } }); 184 | catalog.setStrings("en", { "Multibillion-Dollar": { poor: "dream" } }); 185 | catalog.setCurrentLanguage("en"); 186 | assert.equal(catalog.getString("Multibillion-Dollar", null, "rich"), "nothing"); 187 | assert.equal(catalog.getString("Multibillion-Dollar", null, "poor"), "dream"); 188 | }); 189 | }); 190 | -------------------------------------------------------------------------------- /src/directive.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc directive 3 | * @module gettext 4 | * @name translate 5 | * @requires gettextCatalog 6 | * @requires gettextUtil 7 | * @requires https://docs.angularjs.org/api/ng/service/$parse $parse 8 | * @requires https://docs.angularjs.org/api/ng/service/$animate $animate 9 | * @requires https://docs.angularjs.org/api/ng/service/$compile $compile 10 | * @requires https://docs.angularjs.org/api/ng/service/$window $window 11 | * @restrict AE 12 | * @param {String} [translatePlural] plural form 13 | * @param {Number} translateN value to watch to substitute correct plural form 14 | * @param {String} translateContext context value, e.g. {@link doc:context Verb, Noun} 15 | * @description Annotates and translates text inside directive 16 | * 17 | * Full interpolation support is available in translated strings, so the following will work as expected: 18 | * ```js 19 | *
Hello {{name}}!
20 | * ``` 21 | * 22 | * You can also use custom context parameters while interpolating. This approach allows usage 23 | * of angular filters as well as custom logic inside your translated messages without unnecessary impact on translations. 24 | * 25 | * So for example when you have message like this: 26 | * ```js 27 | *
Last modified {{modificationDate | date:'yyyy-MM-dd HH:mm:ss Z'}} by {{author}}.
28 | * ``` 29 | * you will have it extracted in exact same version so it would look like this: 30 | * `Last modified {{modificationDate | date:'yyyy-MM-dd HH:mm:ss Z'}} by {{author}}`. 31 | * To start with it might be too complicated to read and handle by non technical translator. It's easy to make mistake 32 | * when copying format for example. Secondly if you decide to change format by some point of the project translation will broke 33 | * as it won't be the same string anymore. 34 | * 35 | * Instead your translator should only be concerned to place {{modificationDate}} correctly and you should have a free hand 36 | * to modify implementation details on how to present the results. This is how you can achieve the goal: 37 | * ```js 38 | *
Last modified {{modificationDate}} by {{author}}.
39 | * ``` 40 | * 41 | * There's a few more things worth to point out: 42 | * 1. You can use as many parameters as you want. Each parameter begins with `translate-params-` followed by snake-case parameter name. 43 | * Each parameter will be available for interpolation in camelCase manner (just like angular directive works by default). 44 | * ```js 45 | *
Param {{myCustomParam}} has been changed by {{name}}.
46 | * ``` 47 | * 2. You can rename your variables from current scope to simple ones if you like. 48 | * ```js 49 | *
Today's date is: {{date}}.
50 | * ``` 51 | * 3. You can use translate-params only for some interpolations. Rest would be treated as usual. 52 | * ```js 53 | *
This product: {{product}} costs {{cost}}.
54 | * ``` 55 | */ 56 | angular.module('gettext').directive('translate', function (gettextCatalog, $parse, $animate, $compile, $window, gettextUtil) { 57 | var msie = parseInt((/msie (\d+)/i.exec($window.navigator.userAgent) || [])[1], 10); 58 | var PARAMS_PREFIX = 'translateParams'; 59 | 60 | function getCtxAttr(key) { 61 | return gettextUtil.lcFirst(key.replace(PARAMS_PREFIX, '')); 62 | } 63 | 64 | function handleInterpolationContext(scope, attrs, update) { 65 | var attributes = Object.keys(attrs).filter(function (key) { 66 | return gettextUtil.startsWith(key, PARAMS_PREFIX) && key !== PARAMS_PREFIX; 67 | }); 68 | 69 | if (!attributes.length) { 70 | return null; 71 | } 72 | 73 | var interpolationContext = scope.$new(); 74 | var unwatchers = []; 75 | attributes.forEach(function (attribute) { 76 | var unwatch = scope.$watch(attrs[attribute], function (newVal) { 77 | var key = getCtxAttr(attribute); 78 | interpolationContext[key] = newVal; 79 | update(interpolationContext); 80 | }); 81 | unwatchers.push(unwatch); 82 | }); 83 | scope.$on('$destroy', function () { 84 | unwatchers.forEach(function (unwatch) { 85 | unwatch(); 86 | }); 87 | 88 | interpolationContext.$destroy(); 89 | }); 90 | return interpolationContext; 91 | } 92 | 93 | return { 94 | restrict: 'AE', 95 | terminal: true, 96 | compile: function compile(element, attrs) { 97 | var translate = attrs.translate; 98 | if (translate && translate.match(/^yes|no$/i)) { 99 | // Ignore the translate attribute if it has a "yes" or "no" value, assuming that it is the HTML 100 | // native translate attribute, see 101 | // https://html.spec.whatwg.org/multipage/dom.html#the-translate-attribute 102 | // 103 | // In that case we skip processing as this attribute is intended for the user agent itself. 104 | return; 105 | } 106 | 107 | // Validate attributes 108 | gettextUtil.assert(!attrs.translatePlural || attrs.translateN, 'translate-n', 'translate-plural'); 109 | gettextUtil.assert(!attrs.translateN || attrs.translatePlural, 'translate-plural', 'translate-n'); 110 | 111 | var msgid = gettextUtil.trim(element.html()); 112 | var translatePlural = attrs.translatePlural; 113 | var translateContext = attrs.translateContext; 114 | 115 | if (msie <= 8) { 116 | // Workaround fix relating to angular adding a comment node to 117 | // anchors. angular/angular.js/#1949 / angular/angular.js/#2013 118 | if (msgid.slice(-13) === '') { 119 | msgid = msgid.slice(0, -13); 120 | } 121 | } 122 | 123 | return { 124 | post: function (scope, element, attrs) { 125 | var countFn = $parse(attrs.translateN); 126 | var pluralScope = null; 127 | var linking = true; 128 | 129 | function update(interpolationContext) { 130 | interpolationContext = interpolationContext || null; 131 | 132 | // Fetch correct translated string. 133 | var translated; 134 | if (translatePlural) { 135 | scope = pluralScope || (pluralScope = scope.$new()); 136 | scope.$count = countFn(scope); 137 | translated = gettextCatalog.getPlural(scope.$count, msgid, translatePlural, null, translateContext); 138 | } else { 139 | translated = gettextCatalog.getString(msgid, null, translateContext); 140 | } 141 | var oldContents = element.contents(); 142 | 143 | if (!oldContents && !translated){ 144 | return; 145 | } 146 | 147 | // Avoid redundant swaps 148 | if (translated === gettextUtil.trim(oldContents.html())){ 149 | // Take care of unlinked content 150 | if (linking){ 151 | $compile(oldContents)(scope); 152 | } 153 | return; 154 | } 155 | 156 | // Swap in the translation 157 | var newWrapper = angular.element('' + translated + ''); 158 | $compile(newWrapper.contents())(interpolationContext || scope); 159 | var newContents = newWrapper.contents(); 160 | 161 | $animate.enter(newContents, element); 162 | $animate.leave(oldContents); 163 | } 164 | 165 | var interpolationContext = handleInterpolationContext(scope, attrs, update); 166 | update(interpolationContext); 167 | linking = false; 168 | 169 | if (attrs.translateN) { 170 | scope.$watch(attrs.translateN, function () { 171 | update(interpolationContext); 172 | }); 173 | } 174 | 175 | /** 176 | * @ngdoc event 177 | * @name translate#gettextLanguageChanged 178 | * @eventType listen on scope 179 | * @description Listens for language updates and changes translation accordingly 180 | */ 181 | scope.$on('gettextLanguageChanged', function () { 182 | update(interpolationContext); 183 | }); 184 | 185 | } 186 | }; 187 | } 188 | }; 189 | }); 190 | -------------------------------------------------------------------------------- /src/catalog.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc service 3 | * @module gettext 4 | * @name gettextCatalog 5 | * @requires gettextPlurals 6 | * @requires gettextFallbackLanguage 7 | * @requires https://docs.angularjs.org/api/ng/service/$http $http 8 | * @requires https://docs.angularjs.org/api/ng/service/$cacheFactory $cacheFactory 9 | * @requires https://docs.angularjs.org/api/ng/service/$interpolate $interpolate 10 | * @requires https://docs.angularjs.org/api/ng/service/$rootScope $rootScope 11 | * @description Provides set of method to translate strings 12 | */ 13 | angular.module('gettext').factory('gettextCatalog', function (gettextPlurals, gettextFallbackLanguage, $http, $cacheFactory, $interpolate, $rootScope) { 14 | var catalog; 15 | var noContext = '$$noContext'; 16 | 17 | // IE8 returns UPPER CASE tags, even though the source is lower case. 18 | // This can causes the (key) string in the DOM to have a different case to 19 | // the string in the `po` files. 20 | // IE9, IE10 and IE11 reorders the attributes of tags. 21 | var test = 'test'; 22 | var isHTMLModified = (angular.element('' + test + '').html() !== test); 23 | 24 | var prefixDebug = function (string) { 25 | if (catalog.debug && catalog.currentLanguage !== catalog.baseLanguage) { 26 | return catalog.debugPrefix + string; 27 | } else { 28 | return string; 29 | } 30 | }; 31 | 32 | var addTranslatedMarkers = function (string) { 33 | if (catalog.showTranslatedMarkers) { 34 | return catalog.translatedMarkerPrefix + string + catalog.translatedMarkerSuffix; 35 | } else { 36 | return string; 37 | } 38 | }; 39 | 40 | function broadcastUpdated() { 41 | /** 42 | * @ngdoc event 43 | * @name gettextCatalog#gettextLanguageChanged 44 | * @eventType broadcast on $rootScope 45 | * @description Fires language change notification without any additional parameters. 46 | */ 47 | $rootScope.$broadcast('gettextLanguageChanged'); 48 | } 49 | 50 | catalog = { 51 | /** 52 | * @ngdoc property 53 | * @name gettextCatalog#debug 54 | * @public 55 | * @type {Boolean} false 56 | * @see gettextCatalog#debug 57 | * @description Whether or not to prefix untranslated strings with `[MISSING]:` or a custom prefix. 58 | */ 59 | debug: false, 60 | /** 61 | * @ngdoc property 62 | * @name gettextCatalog#debugPrefix 63 | * @public 64 | * @type {String} [MISSING]: 65 | * @description Custom prefix for untranslated strings when {@link gettextCatalog#debug gettextCatalog#debug} set to `true`. 66 | */ 67 | debugPrefix: '[MISSING]: ', 68 | /** 69 | * @ngdoc property 70 | * @name gettextCatalog#showTranslatedMarkers 71 | * @public 72 | * @type {Boolean} false 73 | * @description Whether or not to wrap all processed text with markers. 74 | * 75 | * Example output: `[Welcome]` 76 | */ 77 | showTranslatedMarkers: false, 78 | /** 79 | * @ngdoc property 80 | * @name gettextCatalog#translatedMarkerPrefix 81 | * @public 82 | * @type {String} [ 83 | * @description Custom prefix to mark strings that have been run through {@link angular-gettext angular-gettext}. 84 | */ 85 | translatedMarkerPrefix: '[', 86 | /** 87 | * @ngdoc property 88 | * @name gettextCatalog#translatedMarkerSuffix 89 | * @public 90 | * @type {String} ] 91 | * @description Custom suffix to mark strings that have been run through {@link angular-gettext angular-gettext}. 92 | */ 93 | translatedMarkerSuffix: ']', 94 | /** 95 | * @ngdoc property 96 | * @name gettextCatalog#strings 97 | * @private 98 | * @type {Object} 99 | * @description An object of loaded translation strings. Shouldn't be used directly. 100 | */ 101 | strings: {}, 102 | /** 103 | * @ngdoc property 104 | * @name gettextCatalog#baseLanguage 105 | * @protected 106 | * @deprecated 107 | * @since 2.0 108 | * @type {String} en 109 | * @description The default language, in which you're application is written. 110 | * 111 | * This defaults to English and it's generally a bad idea to use anything else: 112 | * if your language has different pluralization rules you'll end up with incorrect translations. 113 | */ 114 | baseLanguage: 'en', 115 | /** 116 | * @ngdoc property 117 | * @name gettextCatalog#currentLanguage 118 | * @public 119 | * @type {String} 120 | * @description Active language. 121 | */ 122 | currentLanguage: 'en', 123 | /** 124 | * @ngdoc property 125 | * @name gettextCatalog#cache 126 | * @public 127 | * @type {String} en 128 | * @description Language cache for lazy load 129 | */ 130 | cache: $cacheFactory('strings'), 131 | 132 | /** 133 | * @ngdoc method 134 | * @name gettextCatalog#setCurrentLanguage 135 | * @public 136 | * @param {String} lang language name 137 | * @description Sets the current language and makes sure that all translations get updated correctly. 138 | */ 139 | setCurrentLanguage: function (lang) { 140 | this.currentLanguage = lang; 141 | broadcastUpdated(); 142 | }, 143 | 144 | /** 145 | * @ngdoc method 146 | * @name gettextCatalog#getCurrentLanguage 147 | * @public 148 | * @returns {String} current language 149 | * @description Returns the current language. 150 | */ 151 | getCurrentLanguage: function () { 152 | return this.currentLanguage; 153 | }, 154 | 155 | /** 156 | * @ngdoc method 157 | * @name gettextCatalog#setStrings 158 | * @public 159 | * @param {String} language language name 160 | * @param {Object.} strings set of strings where the key is the translation `key` and `value` is the translated text 161 | * @description Processes an object of string definitions. {@link guide:manual-setstrings More details here}. 162 | */ 163 | setStrings: function (language, strings) { 164 | if (!this.strings[language]) { 165 | this.strings[language] = {}; 166 | } 167 | 168 | var defaultPlural = gettextPlurals(language, 1); 169 | for (var key in strings) { 170 | var val = strings[key]; 171 | 172 | if (isHTMLModified) { 173 | // Use the DOM engine to render any HTML in the key (#131). 174 | key = angular.element('' + key + '').html(); 175 | } 176 | 177 | if (angular.isString(val) || angular.isArray(val)) { 178 | // No context, wrap it in $$noContext. 179 | var obj = {}; 180 | obj[noContext] = val; 181 | val = obj; 182 | } 183 | 184 | if (!this.strings[language][key]) { 185 | this.strings[language][key] = {}; 186 | } 187 | 188 | for (var context in val) { 189 | var str = val[context]; 190 | if (!angular.isArray(str)) { 191 | // Expand single strings 192 | this.strings[language][key][context] = []; 193 | this.strings[language][key][context][defaultPlural] = str; 194 | } else { 195 | this.strings[language][key][context] = str; 196 | } 197 | } 198 | } 199 | 200 | broadcastUpdated(); 201 | }, 202 | 203 | /** 204 | * @ngdoc method 205 | * @name gettextCatalog#getStringFormFor 206 | * @protected 207 | * @param {String} language language name 208 | * @param {String} string translation key 209 | * @param {Number=} n number to build string form for 210 | * @param {String=} context translation key context, e.g. {@link doc:context Verb, Noun} 211 | * @returns {String|Null} translated or annotated string or null if language is not set 212 | * @description Translate a string with the given language, count and context. 213 | */ 214 | getStringFormFor: function (language, string, n, context) { 215 | if (!language) { 216 | return null; 217 | } 218 | var stringTable = this.strings[language] || {}; 219 | var contexts = stringTable[string] || {}; 220 | var plurals = contexts[context || noContext] || []; 221 | return plurals[gettextPlurals(language, n)]; 222 | }, 223 | 224 | /** 225 | * @ngdoc method 226 | * @name gettextCatalog#getString 227 | * @public 228 | * @param {String} string translation key 229 | * @param {$rootScope.Scope=} scope scope to do interpolation against 230 | * @param {String=} context translation key context, e.g. {@link doc:context Verb, Noun} 231 | * @returns {String} translated or annotated string 232 | * @description Translate a string with the given scope and context. 233 | * 234 | * First it tries {@link gettextCatalog#currentLanguage gettextCatalog#currentLanguage} (e.g. `en-US`) then {@link gettextFallbackLanguage fallback} (e.g. `en`). 235 | * 236 | * When `scope` is supplied it uses Angular.JS interpolation, so something like this will do what you expect: 237 | * ```js 238 | * var hello = gettextCatalog.getString("Hello {{name}}!", { name: "Ruben" }); 239 | * // var hello will be "Hallo Ruben!" in Dutch. 240 | * ``` 241 | * Avoid using scopes - this skips interpolation and is a lot faster. 242 | */ 243 | getString: function (string, scope, context) { 244 | var fallbackLanguage = gettextFallbackLanguage(this.currentLanguage); 245 | string = this.getStringFormFor(this.currentLanguage, string, 1, context) || 246 | this.getStringFormFor(fallbackLanguage, string, 1, context) || 247 | prefixDebug(string); 248 | string = scope ? $interpolate(string)(scope) : string; 249 | return addTranslatedMarkers(string); 250 | }, 251 | 252 | /** 253 | * @ngdoc method 254 | * @name gettextCatalog#getPlural 255 | * @public 256 | * @param {Number} n number to build string form for 257 | * @param {String} string translation key 258 | * @param {String} stringPlural plural translation key 259 | * @param {$rootScope.Scope=} scope scope to do interpolation against 260 | * @param {String=} context translation key context, e.g. {@link doc:context Verb, Noun} 261 | * @returns {String} translated or annotated string 262 | * @see {@link gettextCatalog#getString gettextCatalog#getString} for details 263 | * @description Translate a plural string with the given context. 264 | */ 265 | getPlural: function (n, string, stringPlural, scope, context) { 266 | var fallbackLanguage = gettextFallbackLanguage(this.currentLanguage); 267 | string = this.getStringFormFor(this.currentLanguage, string, n, context) || 268 | this.getStringFormFor(fallbackLanguage, string, n, context) || 269 | prefixDebug(n === 1 ? string : stringPlural); 270 | if (scope) { 271 | scope.$count = n; 272 | string = $interpolate(string)(scope); 273 | } 274 | return addTranslatedMarkers(string); 275 | }, 276 | 277 | /** 278 | * @ngdoc method 279 | * @name gettextCatalog#loadRemote 280 | * @public 281 | * @param {String} url location of the translations 282 | * @description Load a set of translation strings from a given URL. 283 | * 284 | * This should be a JSON catalog generated with [angular-gettext-tools](https://github.com/rubenv/angular-gettext-tools). 285 | * {@link guide:lazy-loading More details here}. 286 | */ 287 | loadRemote: function (url) { 288 | return $http({ 289 | method: 'GET', 290 | url: url, 291 | cache: catalog.cache 292 | }).then(function (response) { 293 | var data = response.data; 294 | for (var lang in data) { 295 | catalog.setStrings(lang, data[lang]); 296 | } 297 | return response; 298 | }); 299 | } 300 | }; 301 | 302 | return catalog; 303 | }); 304 | -------------------------------------------------------------------------------- /test/unit/directive.js: -------------------------------------------------------------------------------- 1 | describe("Directive", function () { 2 | var catalog = null; 3 | var $rootScope = null; 4 | var $compile = null; 5 | 6 | beforeEach(module("gettext")); 7 | 8 | beforeEach(inject(function ($injector, gettextCatalog) { 9 | $rootScope = $injector.get("$rootScope"); 10 | $compile = $injector.get("$compile"); 11 | catalog = gettextCatalog; 12 | catalog.setStrings("nl", { 13 | Hello: "Hallo", 14 | "Hello {{name}}!": "Hallo {{name}}!", 15 | "Hello {{author}}!": "Hallo {{author}}!", 16 | "{{author}}": "{{author}}", 17 | "One boat": ["Een boot", "{{count}} boten"], 18 | Archive: { verb: "Archiveren", noun: "Archief" } 19 | }); 20 | catalog.setStrings("af", { 21 | "This link: {{url}} will have the 'ng-binding' class attached before the translate directive can capture it.": "Die skakel: {{url}} sal die 'ng-binding' klass aangevoeg hê voor die translate directive dit kan vasvat." 22 | }); 23 | catalog.setStrings("pl", { 24 | "This product: {{product}} costs {{cost}}.": "Ten produkt: {{product}} kosztuje {{cost}}.", 25 | "This product: {{product}} costs {{cost}}{{currency}}.": "Ten produkt: {{product}} kosztuje {{cost}}{{currency}}.", 26 | "Product of the week: {{product}}.": "Produkt tygodnia: {{product}}." 27 | }); 28 | })); 29 | 30 | it("Should work on empty strings", function () { 31 | var el = $compile("

")($rootScope); 32 | $rootScope.$digest(); 33 | assert.equal(el.text(), ""); 34 | }); 35 | 36 | it("Should return string unchanged when no translation is available", function () { 37 | var el = $compile("

Hello!

")($rootScope); 38 | $rootScope.$digest(); 39 | assert.equal(el.text(), "Hello!"); 40 | }); 41 | 42 | it("Should translate known strings", function () { 43 | catalog.setCurrentLanguage("nl"); 44 | var el = $compile("

Hello

")($rootScope); 45 | $rootScope.$digest(); 46 | assert.equal(el.text(), "Hallo"); 47 | }); 48 | 49 | it("Should translate known strings according to defined translation context", function () { 50 | catalog.setCurrentLanguage("nl"); 51 | var el = $compile("

Archive

")($rootScope); 52 | $rootScope.$digest(); 53 | assert.equal(el.text(), "Archiveren"); 54 | el = $compile("

Archive

")($rootScope); 55 | $rootScope.$digest(); 56 | assert.equal(el.text(), "Archief"); 57 | }); 58 | 59 | it("Should still allow for interpolation", function () { 60 | $rootScope.name = "Ruben"; 61 | catalog.setCurrentLanguage("nl"); 62 | var el = $compile("
Hello {{name}}!
")($rootScope); 63 | $rootScope.$digest(); 64 | assert.equal(el.text(), "Hallo Ruben!"); 65 | }); 66 | 67 | it("Can provide plural value and string, should translate", function () { 68 | $rootScope.count = 3; 69 | catalog.setCurrentLanguage("nl"); 70 | var el = $compile("
One boat
")($rootScope); 71 | $rootScope.$digest(); 72 | assert.equal(el.text(), "3 boten"); 73 | }); 74 | 75 | it("Can provide plural value and string, should translate even for unknown languages", function () { 76 | $rootScope.count = 2; 77 | catalog.setCurrentLanguage("fr"); 78 | var el = $compile("
One boat
")($rootScope); 79 | $rootScope.$digest(); 80 | assert.equal(el.text(), "2 boats"); 81 | }); 82 | 83 | it("Can provide plural value and string, should translate even for default strings", function () { 84 | $rootScope.count = 0; 85 | var el = $compile("
One boat
")($rootScope); 86 | $rootScope.$digest(); 87 | assert.equal(el.text(), "0 boats"); 88 | }); 89 | 90 | it("Can provide plural value and string, should translate even for default strings, singular", function () { 91 | $rootScope.count = 1; 92 | var el = $compile("
One boat
")($rootScope); 93 | $rootScope.$digest(); 94 | assert.equal(el.text(), "One boat"); 95 | }); 96 | 97 | it("Can provide plural value and string, should translate even for default strings, plural", function () { 98 | $rootScope.count = 2; 99 | var el = $compile("
One boat
")($rootScope); 100 | $rootScope.$digest(); 101 | assert.equal(el.text(), "2 boats"); 102 | }); 103 | 104 | it("Can provide plural value and string, should translate with hardcoded count", function () { 105 | $rootScope.count = 3; 106 | var el = $compile("
One boat
")($rootScope); 107 | $rootScope.$digest(); 108 | assert.equal(el.text(), "3 boats"); 109 | }); 110 | 111 | it("Can provide plural value and string, should translate with injected $count", function () { 112 | $rootScope.some = { 113 | custom: { 114 | elements: { 115 | length: 6 116 | } 117 | } 118 | }; 119 | var el = $compile("
One boat
")($rootScope); 120 | $rootScope.$digest(); 121 | assert.equal(el.text(), "6 boats"); 122 | }); 123 | 124 | it("Changing the scope should update the translation, fixed count", function () { 125 | $rootScope.count = 3; 126 | var el = $compile("
One boat
")($rootScope); 127 | $rootScope.$digest(); 128 | assert.equal(el.text(), "3 boats"); 129 | $rootScope.$apply(function () { 130 | $rootScope.count = 2; 131 | }); 132 | assert.equal(el.text(), "2 boats"); 133 | }); 134 | 135 | it("Changing the scope should update the translation, changed count", function () { 136 | $rootScope.count = 3; 137 | var el = $compile("
One boat
")($rootScope); 138 | $rootScope.$digest(); 139 | assert.equal(el.text(), "3 boats"); 140 | $rootScope.$apply(function () { 141 | $rootScope.count = 1; 142 | }); 143 | assert.equal(el.text(), "One boat"); 144 | }); 145 | 146 | it("Child elements still respond to scope correctly", function () { 147 | $rootScope.name = "Ruben"; 148 | var el = $compile("
Hello {{name}}!
")($rootScope); 149 | $rootScope.$digest(); 150 | assert.equal(el.text(), "Hello Ruben!"); 151 | $rootScope.$apply(function () { 152 | $rootScope.name = "Joe"; 153 | }); 154 | assert.equal(el.text(), "Hello Joe!"); 155 | }); 156 | 157 | it("Child elements still respond to scope correctly, plural", function () { 158 | $rootScope.name = "Ruben"; 159 | $rootScope.count = 1; 160 | var el = $compile("
Hello {{name}} (one message)!
")($rootScope); 161 | $rootScope.$digest(); 162 | assert.equal(el.text(), "Hello Ruben (one message)!"); 163 | $rootScope.$apply(function () { 164 | $rootScope.name = "Joe"; 165 | }); 166 | assert.equal(el.text(), "Hello Joe (one message)!"); 167 | $rootScope.$apply(function () { 168 | $rootScope.count = 3; 169 | }); 170 | assert.equal(el.text(), "Hello Joe (3 messages)!"); 171 | $rootScope.$apply(function () { 172 | $rootScope.name = "Jack"; 173 | }); 174 | assert.equal(el.text(), "Hello Jack (3 messages)!"); 175 | $rootScope.$apply(function () { 176 | $rootScope.count = 1; 177 | $rootScope.name = "Jane"; 178 | }); 179 | assert.equal(el.text(), "Hello Jane (one message)!"); 180 | }); 181 | 182 | it("Changing language should translate again", function () { 183 | catalog.setCurrentLanguage("nl"); 184 | var el = $compile("
Hello
")($rootScope); 185 | $rootScope.$digest(); 186 | assert.equal(el.text(), "Hallo"); 187 | catalog.setCurrentLanguage("en"); 188 | $rootScope.$digest(); 189 | assert.equal(el.text(), "Hello"); 190 | }); 191 | 192 | it("Changing language should translate again not loosing scope", function () { 193 | catalog.setCurrentLanguage("nl"); 194 | $rootScope.providedName = "Ruben"; 195 | var el = $compile("
Hello {{name}}!
")($rootScope); 196 | $rootScope.$digest(); 197 | assert.equal(el.text(), "Hallo Ruben!"); 198 | catalog.setCurrentLanguage("en"); 199 | $rootScope.$digest(); 200 | assert.equal(el.text(), "Hello Ruben!"); 201 | }); 202 | 203 | it("Should warn if you forget to add attributes (n)", function () { 204 | assert.throws(function () { 205 | $compile("
Hello {{name}} (one message)!
")($rootScope); 206 | }, "You should add a translate-n attribute whenever you add a translate-plural attribute."); 207 | }); 208 | 209 | it("Should warn if you forget to add attributes (plural)", function () { 210 | assert.throws(function () { 211 | $compile("
Hello {{name}} (one message)!
")($rootScope); 212 | }, "You should add a translate-plural attribute whenever you add a translate-n attribute."); 213 | }); 214 | 215 | it("Translates inside an ngIf directive", function () { 216 | $rootScope.flag = true; 217 | catalog.setCurrentLanguage("nl"); 218 | var el = $compile("
Hello
")($rootScope); 219 | $rootScope.$digest(); 220 | assert.equal(el.text(), "Hallo"); 221 | }); 222 | 223 | it("Does not translate inside a false ngIf directive", function () { 224 | $rootScope.flag = false; 225 | catalog.setCurrentLanguage("nl"); 226 | var el = $compile("
Hello
")($rootScope); 227 | $rootScope.$digest(); 228 | assert.equal(el.text(), ""); 229 | }); 230 | 231 | it("Does not have a ng-binding class", function () { 232 | $rootScope.url = "http://google.com"; 233 | catalog.setCurrentLanguage("af"); 234 | var el = $compile("

This link: {{url}} will have the 'ng-binding' class attached before the translate directive can capture it.

")($rootScope); 235 | $rootScope.$digest(); 236 | assert.equal(el.text(), "Die skakel: http://google.com sal die 'ng-binding' klass aangevoeg hê voor die translate directive dit kan vasvat."); 237 | }); 238 | 239 | it("Should work as an element", function () { 240 | catalog.currentLanguage = "nl"; 241 | var el = $compile("Hello")($rootScope); 242 | $rootScope.$digest(); 243 | assert.equal(el.text(), "Hallo"); 244 | }); 245 | 246 | it("Should translate with context param", function () { 247 | $rootScope.name = "Ernest"; 248 | catalog.setCurrentLanguage("nl"); 249 | var el = $compile("

Hello {{author}}!

")($rootScope); 250 | $rootScope.$digest(); 251 | assert.equal(el.text(), "Hallo Ernest!"); 252 | }); 253 | 254 | it("Should translate with pure context param", function () { 255 | $rootScope.name = "Ernest"; 256 | catalog.setCurrentLanguage("nl"); 257 | var el = $compile("

{{author}}

")($rootScope); 258 | $rootScope.$digest(); 259 | assert.equal(el.text(), "Ernest"); 260 | }); 261 | 262 | it("Should translate with filters used in translate params", function () { 263 | $rootScope.name = "Ernest"; 264 | catalog.setCurrentLanguage("nl"); 265 | var el = $compile("

Hello {{author}}!

")($rootScope); 266 | $rootScope.$digest(); 267 | assert.equal(el.text(), "Hallo ERNEST!"); 268 | }); 269 | 270 | it("Should translate with multiple translate params", function () { 271 | $rootScope.item = "Headphones"; 272 | $rootScope.cost = 5; 273 | catalog.setCurrentLanguage("pl"); 274 | var el = $compile("

This product: {{product}} costs {{cost}}.

")($rootScope); 275 | $rootScope.$digest(); 276 | assert.equal(el.text(), "Ten produkt: HEADPHONES kosztuje $5.00."); 277 | }); 278 | 279 | it("Should translate with multiple translate params along with normal scope interpolation", function () { 280 | $rootScope.item = "Headphones"; 281 | $rootScope.cost = 5; 282 | $rootScope.currency = "$"; 283 | catalog.setCurrentLanguage("pl"); 284 | var el = $compile("

This product: {{product}} costs {{cost}}{{currency}}.

")($rootScope); 285 | $rootScope.$digest(); 286 | assert.equal(el.text(), "Ten produkt: HEADPHONES kosztuje 5$."); 287 | }); 288 | 289 | it("Should update translation with translate params when context changes", function () { 290 | $rootScope.item = "Headphones"; 291 | catalog.setCurrentLanguage("pl"); 292 | var el = $compile("

Product of the week: {{product}}.

")($rootScope); 293 | $rootScope.$digest(); 294 | assert.equal(el.text(), "Produkt tygodnia: HEADPHONES."); 295 | 296 | $rootScope.item = "Smart TV"; 297 | $rootScope.$digest(); 298 | assert.equal(el.text(), "Produkt tygodnia: SMART TV."); 299 | }); 300 | 301 | describe("Translation's with plurals", function () { 302 | var sourceString; 303 | 304 | beforeEach(inject(function () { 305 | sourceString = "Today {{someone}} meets with {{someoneElse}} for {{duration}} minute."; 306 | 307 | catalog.setStrings("pl", { 308 | "Today {{someone}} meets with {{someoneElse}} for {{duration}} minute.": [ 309 | "Dziś {{someone}} spotyka się z {{someoneElse}} przez jedną minutę.", 310 | "Dziś {{someone}} spotyka się z {{someoneElse}} przez {{duration}} minuty.", 311 | "Dziś {{someone}} spotyka się z {{someoneElse}} przez {{duration}} minut." 312 | ] 313 | }); 314 | })); 315 | 316 | it("Should properly handle plural translation for 1", function () { 317 | catalog.setCurrentLanguage("pl"); 318 | $rootScope.someone = "Ruben"; 319 | $rootScope.someoneElse = "Ernest"; 320 | 321 | var el = $compile("

Today {{someone}} meets with {{someoneElse}} for {{duration}} minute.

")($rootScope); 322 | 323 | $rootScope.duration = 1; 324 | $rootScope.$digest(); 325 | assert.equal(el.text(), "Dziś Ruben spotyka się z Ernest przez jedną minutę."); 326 | }); 327 | 328 | it("Should properly handle plural translation for 0, 5, 6, 7, ...", function () { 329 | catalog.setCurrentLanguage("pl"); 330 | $rootScope.someone = "Ruben"; 331 | $rootScope.someoneElse = "Ernest"; 332 | 333 | var el = $compile("

Today {{someone}} meets with {{someoneElse}} for {{duration}} minute.

")($rootScope); 334 | 335 | $rootScope.duration = 0; 336 | $rootScope.$digest(); 337 | assert.equal(el.text(), "Dziś Ruben spotyka się z Ernest przez 0 minut."); 338 | 339 | $rootScope.duration = 5; 340 | $rootScope.$digest(); 341 | assert.equal(el.text(), "Dziś Ruben spotyka się z Ernest przez 5 minut."); 342 | 343 | $rootScope.duration = 26; 344 | $rootScope.$digest(); 345 | assert.equal(el.text(), "Dziś Ruben spotyka się z Ernest przez 26 minut."); 346 | }); 347 | 348 | it("Should properly handle plural translation for 2, 3, 4, 22, ...", function () { 349 | catalog.setCurrentLanguage("pl"); 350 | $rootScope.someone = "Ruben"; 351 | $rootScope.someoneElse = "Ernest"; 352 | 353 | var el = $compile("

Today {{someone}} meets with {{someoneElse}} for {{duration}} minute.

")($rootScope); 354 | 355 | $rootScope.duration = 2; 356 | $rootScope.$digest(); 357 | assert.equal(el.text(), "Dziś Ruben spotyka się z Ernest przez 2 minuty."); 358 | 359 | $rootScope.duration = 3; 360 | $rootScope.$digest(); 361 | assert.equal(el.text(), "Dziś Ruben spotyka się z Ernest przez 3 minuty."); 362 | 363 | $rootScope.duration = 4; 364 | $rootScope.$digest(); 365 | assert.equal(el.text(), "Dziś Ruben spotyka się z Ernest przez 4 minuty."); 366 | 367 | $rootScope.duration = 22; 368 | $rootScope.$digest(); 369 | assert.equal(el.text(), "Dziś Ruben spotyka się z Ernest przez 22 minuty."); 370 | }); 371 | 372 | it("Should properly handle plural translation for 1 with language code that includes locale (e.g. pl_PL, en_US)", function () { 373 | catalog.setCurrentLanguage("pl_PL"); 374 | $rootScope.someone = "Ruben"; 375 | $rootScope.someoneElse = "Ernest"; 376 | 377 | var el = $compile("

Today {{someone}} meets with {{someoneElse}} for {{duration}} minute.

")($rootScope); 378 | 379 | $rootScope.duration = 1; 380 | $rootScope.$digest(); 381 | assert.equal(el.text(), "Dziś Ruben spotyka się z Ernest przez jedną minutę."); 382 | }); 383 | 384 | it("Should properly handle plural translation for 0, 5, 6, 7, ... with language code that includes locale (e.g. pl_PL, en_US)", function () { 385 | catalog.setCurrentLanguage("pl_PL"); 386 | $rootScope.someone = "Ruben"; 387 | $rootScope.someoneElse = "Ernest"; 388 | 389 | var el = $compile("

Today {{someone}} meets with {{someoneElse}} for {{duration}} minute.

")($rootScope); 390 | 391 | $rootScope.duration = 0; 392 | $rootScope.$digest(); 393 | assert.equal(el.text(), "Dziś Ruben spotyka się z Ernest przez 0 minut."); 394 | 395 | $rootScope.duration = 5; 396 | $rootScope.$digest(); 397 | assert.equal(el.text(), "Dziś Ruben spotyka się z Ernest przez 5 minut."); 398 | 399 | $rootScope.duration = 26; 400 | $rootScope.$digest(); 401 | assert.equal(el.text(), "Dziś Ruben spotyka się z Ernest przez 26 minut."); 402 | }); 403 | 404 | it("Should properly handle plural translation for 2, 3, 4, 22, ... with language code that includes locale (e.g. pl_PL, en_US)", function () { 405 | catalog.setCurrentLanguage("pl_PL"); 406 | $rootScope.someone = "Ruben"; 407 | $rootScope.someoneElse = "Ernest"; 408 | 409 | var el = $compile("

Today {{someone}} meets with {{someoneElse}} for {{duration}} minute.

")($rootScope); 410 | 411 | $rootScope.duration = 2; 412 | $rootScope.$digest(); 413 | assert.equal(el.text(), "Dziś Ruben spotyka się z Ernest przez 2 minuty."); 414 | 415 | $rootScope.duration = 3; 416 | $rootScope.$digest(); 417 | assert.equal(el.text(), "Dziś Ruben spotyka się z Ernest przez 3 minuty."); 418 | 419 | $rootScope.duration = 4; 420 | $rootScope.$digest(); 421 | assert.equal(el.text(), "Dziś Ruben spotyka się z Ernest przez 4 minuty."); 422 | 423 | $rootScope.duration = 22; 424 | $rootScope.$digest(); 425 | assert.equal(el.text(), "Dziś Ruben spotyka się z Ernest przez 22 minuty."); 426 | }); 427 | }); 428 | 429 | describe("Passthrough native HTML translate attribute", function () { 430 | it("Should ignore translate='no' attribute", function () { 431 | catalog.setCurrentLanguage("nl"); 432 | var el = $compile("

Hello

")($rootScope); 433 | $rootScope.$digest(); 434 | assert.equal(el.text(), "Hello"); 435 | assert.equal(el.find("h1").attr("translate"), "no"); 436 | }); 437 | 438 | it("Should ignore translate='yes' attribute", function () { 439 | catalog.setCurrentLanguage("nl"); 440 | var el = $compile("

Hello

")($rootScope); 441 | $rootScope.$digest(); 442 | assert.equal(el.text(), "Hello"); 443 | assert.equal(el.find("h1").attr("translate"), "yes"); 444 | }); 445 | 446 | it.only("Should not ignore the XHTML boolean form translate='translate'", function () { 447 | catalog.setCurrentLanguage("nl"); 448 | var el = $compile("

Hello

")($rootScope); 449 | $rootScope.$digest(); 450 | assert.equal(el.text(), "Hallo"); 451 | }); 452 | }); 453 | }); 454 | -------------------------------------------------------------------------------- /dist/angular-gettext.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @ngdoc module 3 | * @name gettext 4 | * @packageName angular-gettext 5 | * @description Super simple Gettext for Angular.JS 6 | * 7 | * A sample application can be found at https://github.com/rubenv/angular-gettext-example. 8 | * This is an adaptation of the [TodoMVC](http://todomvc.com/) example. You can use this as a guideline while adding {@link angular-gettext angular-gettext} to your own application. 9 | */ 10 | /** 11 | * @ngdoc factory 12 | * @module gettext 13 | * @name gettextPlurals 14 | * @param {String} [langCode=en] language code 15 | * @param {Number} [n=0] number to calculate form for 16 | * @returns {Number} plural form number 17 | * @description Provides correct plural form id for the given language 18 | * 19 | * Example 20 | * ```js 21 | * gettextPlurals('ru', 10); // 1 22 | * gettextPlurals('en', 1); // 0 23 | * gettextPlurals(); // 1 24 | * ``` 25 | */ 26 | angular.module('gettext', []); 27 | /** 28 | * @ngdoc object 29 | * @module gettext 30 | * @name gettext 31 | * @kind function 32 | * @param {String} str annotation key 33 | * @description Gettext constant function for annotating strings 34 | * 35 | * ```js 36 | * angular.module('myApp', ['gettext']).config(function(gettext) { 37 | * /// MyApp document title 38 | * gettext('my-app.title'); 39 | * ... 40 | * }) 41 | * ``` 42 | */ 43 | angular.module('gettext').constant('gettext', function (str) { 44 | /* 45 | * Does nothing, simply returns the input string. 46 | * 47 | * This function serves as a marker for `grunt-angular-gettext` to know that 48 | * this string should be extracted for translations. 49 | */ 50 | return str; 51 | }); 52 | 53 | /** 54 | * @ngdoc service 55 | * @module gettext 56 | * @name gettextCatalog 57 | * @requires gettextPlurals 58 | * @requires gettextFallbackLanguage 59 | * @requires https://docs.angularjs.org/api/ng/service/$http $http 60 | * @requires https://docs.angularjs.org/api/ng/service/$cacheFactory $cacheFactory 61 | * @requires https://docs.angularjs.org/api/ng/service/$interpolate $interpolate 62 | * @requires https://docs.angularjs.org/api/ng/service/$rootScope $rootScope 63 | * @description Provides set of method to translate strings 64 | */ 65 | angular.module('gettext').factory('gettextCatalog', ["gettextPlurals", "gettextFallbackLanguage", "$http", "$cacheFactory", "$interpolate", "$rootScope", function (gettextPlurals, gettextFallbackLanguage, $http, $cacheFactory, $interpolate, $rootScope) { 66 | var catalog; 67 | var noContext = '$$noContext'; 68 | 69 | // IE8 returns UPPER CASE tags, even though the source is lower case. 70 | // This can causes the (key) string in the DOM to have a different case to 71 | // the string in the `po` files. 72 | // IE9, IE10 and IE11 reorders the attributes of tags. 73 | var test = 'test'; 74 | var isHTMLModified = (angular.element('' + test + '').html() !== test); 75 | 76 | var prefixDebug = function (string) { 77 | if (catalog.debug && catalog.currentLanguage !== catalog.baseLanguage) { 78 | return catalog.debugPrefix + string; 79 | } else { 80 | return string; 81 | } 82 | }; 83 | 84 | var addTranslatedMarkers = function (string) { 85 | if (catalog.showTranslatedMarkers) { 86 | return catalog.translatedMarkerPrefix + string + catalog.translatedMarkerSuffix; 87 | } else { 88 | return string; 89 | } 90 | }; 91 | 92 | function broadcastUpdated() { 93 | /** 94 | * @ngdoc event 95 | * @name gettextCatalog#gettextLanguageChanged 96 | * @eventType broadcast on $rootScope 97 | * @description Fires language change notification without any additional parameters. 98 | */ 99 | $rootScope.$broadcast('gettextLanguageChanged'); 100 | } 101 | 102 | catalog = { 103 | /** 104 | * @ngdoc property 105 | * @name gettextCatalog#debug 106 | * @public 107 | * @type {Boolean} false 108 | * @see gettextCatalog#debug 109 | * @description Whether or not to prefix untranslated strings with `[MISSING]:` or a custom prefix. 110 | */ 111 | debug: false, 112 | /** 113 | * @ngdoc property 114 | * @name gettextCatalog#debugPrefix 115 | * @public 116 | * @type {String} [MISSING]: 117 | * @description Custom prefix for untranslated strings when {@link gettextCatalog#debug gettextCatalog#debug} set to `true`. 118 | */ 119 | debugPrefix: '[MISSING]: ', 120 | /** 121 | * @ngdoc property 122 | * @name gettextCatalog#showTranslatedMarkers 123 | * @public 124 | * @type {Boolean} false 125 | * @description Whether or not to wrap all processed text with markers. 126 | * 127 | * Example output: `[Welcome]` 128 | */ 129 | showTranslatedMarkers: false, 130 | /** 131 | * @ngdoc property 132 | * @name gettextCatalog#translatedMarkerPrefix 133 | * @public 134 | * @type {String} [ 135 | * @description Custom prefix to mark strings that have been run through {@link angular-gettext angular-gettext}. 136 | */ 137 | translatedMarkerPrefix: '[', 138 | /** 139 | * @ngdoc property 140 | * @name gettextCatalog#translatedMarkerSuffix 141 | * @public 142 | * @type {String} ] 143 | * @description Custom suffix to mark strings that have been run through {@link angular-gettext angular-gettext}. 144 | */ 145 | translatedMarkerSuffix: ']', 146 | /** 147 | * @ngdoc property 148 | * @name gettextCatalog#strings 149 | * @private 150 | * @type {Object} 151 | * @description An object of loaded translation strings. Shouldn't be used directly. 152 | */ 153 | strings: {}, 154 | /** 155 | * @ngdoc property 156 | * @name gettextCatalog#baseLanguage 157 | * @protected 158 | * @deprecated 159 | * @since 2.0 160 | * @type {String} en 161 | * @description The default language, in which you're application is written. 162 | * 163 | * This defaults to English and it's generally a bad idea to use anything else: 164 | * if your language has different pluralization rules you'll end up with incorrect translations. 165 | */ 166 | baseLanguage: 'en', 167 | /** 168 | * @ngdoc property 169 | * @name gettextCatalog#currentLanguage 170 | * @public 171 | * @type {String} 172 | * @description Active language. 173 | */ 174 | currentLanguage: 'en', 175 | /** 176 | * @ngdoc property 177 | * @name gettextCatalog#cache 178 | * @public 179 | * @type {String} en 180 | * @description Language cache for lazy load 181 | */ 182 | cache: $cacheFactory('strings'), 183 | 184 | /** 185 | * @ngdoc method 186 | * @name gettextCatalog#setCurrentLanguage 187 | * @public 188 | * @param {String} lang language name 189 | * @description Sets the current language and makes sure that all translations get updated correctly. 190 | */ 191 | setCurrentLanguage: function (lang) { 192 | this.currentLanguage = lang; 193 | broadcastUpdated(); 194 | }, 195 | 196 | /** 197 | * @ngdoc method 198 | * @name gettextCatalog#getCurrentLanguage 199 | * @public 200 | * @returns {String} current language 201 | * @description Returns the current language. 202 | */ 203 | getCurrentLanguage: function () { 204 | return this.currentLanguage; 205 | }, 206 | 207 | /** 208 | * @ngdoc method 209 | * @name gettextCatalog#setStrings 210 | * @public 211 | * @param {String} language language name 212 | * @param {Object.} strings set of strings where the key is the translation `key` and `value` is the translated text 213 | * @description Processes an object of string definitions. {@link guide:manual-setstrings More details here}. 214 | */ 215 | setStrings: function (language, strings) { 216 | if (!this.strings[language]) { 217 | this.strings[language] = {}; 218 | } 219 | 220 | var defaultPlural = gettextPlurals(language, 1); 221 | for (var key in strings) { 222 | var val = strings[key]; 223 | 224 | if (isHTMLModified) { 225 | // Use the DOM engine to render any HTML in the key (#131). 226 | key = angular.element('' + key + '').html(); 227 | } 228 | 229 | if (angular.isString(val) || angular.isArray(val)) { 230 | // No context, wrap it in $$noContext. 231 | var obj = {}; 232 | obj[noContext] = val; 233 | val = obj; 234 | } 235 | 236 | if (!this.strings[language][key]) { 237 | this.strings[language][key] = {}; 238 | } 239 | 240 | for (var context in val) { 241 | var str = val[context]; 242 | if (!angular.isArray(str)) { 243 | // Expand single strings 244 | this.strings[language][key][context] = []; 245 | this.strings[language][key][context][defaultPlural] = str; 246 | } else { 247 | this.strings[language][key][context] = str; 248 | } 249 | } 250 | } 251 | 252 | broadcastUpdated(); 253 | }, 254 | 255 | /** 256 | * @ngdoc method 257 | * @name gettextCatalog#getStringFormFor 258 | * @protected 259 | * @param {String} language language name 260 | * @param {String} string translation key 261 | * @param {Number=} n number to build string form for 262 | * @param {String=} context translation key context, e.g. {@link doc:context Verb, Noun} 263 | * @returns {String|Null} translated or annotated string or null if language is not set 264 | * @description Translate a string with the given language, count and context. 265 | */ 266 | getStringFormFor: function (language, string, n, context) { 267 | if (!language) { 268 | return null; 269 | } 270 | var stringTable = this.strings[language] || {}; 271 | var contexts = stringTable[string] || {}; 272 | var plurals = contexts[context || noContext] || []; 273 | return plurals[gettextPlurals(language, n)]; 274 | }, 275 | 276 | /** 277 | * @ngdoc method 278 | * @name gettextCatalog#getString 279 | * @public 280 | * @param {String} string translation key 281 | * @param {$rootScope.Scope=} scope scope to do interpolation against 282 | * @param {String=} context translation key context, e.g. {@link doc:context Verb, Noun} 283 | * @returns {String} translated or annotated string 284 | * @description Translate a string with the given scope and context. 285 | * 286 | * First it tries {@link gettextCatalog#currentLanguage gettextCatalog#currentLanguage} (e.g. `en-US`) then {@link gettextFallbackLanguage fallback} (e.g. `en`). 287 | * 288 | * When `scope` is supplied it uses Angular.JS interpolation, so something like this will do what you expect: 289 | * ```js 290 | * var hello = gettextCatalog.getString("Hello {{name}}!", { name: "Ruben" }); 291 | * // var hello will be "Hallo Ruben!" in Dutch. 292 | * ``` 293 | * Avoid using scopes - this skips interpolation and is a lot faster. 294 | */ 295 | getString: function (string, scope, context) { 296 | var fallbackLanguage = gettextFallbackLanguage(this.currentLanguage); 297 | string = this.getStringFormFor(this.currentLanguage, string, 1, context) || 298 | this.getStringFormFor(fallbackLanguage, string, 1, context) || 299 | prefixDebug(string); 300 | string = scope ? $interpolate(string)(scope) : string; 301 | return addTranslatedMarkers(string); 302 | }, 303 | 304 | /** 305 | * @ngdoc method 306 | * @name gettextCatalog#getPlural 307 | * @public 308 | * @param {Number} n number to build string form for 309 | * @param {String} string translation key 310 | * @param {String} stringPlural plural translation key 311 | * @param {$rootScope.Scope=} scope scope to do interpolation against 312 | * @param {String=} context translation key context, e.g. {@link doc:context Verb, Noun} 313 | * @returns {String} translated or annotated string 314 | * @see {@link gettextCatalog#getString gettextCatalog#getString} for details 315 | * @description Translate a plural string with the given context. 316 | */ 317 | getPlural: function (n, string, stringPlural, scope, context) { 318 | var fallbackLanguage = gettextFallbackLanguage(this.currentLanguage); 319 | string = this.getStringFormFor(this.currentLanguage, string, n, context) || 320 | this.getStringFormFor(fallbackLanguage, string, n, context) || 321 | prefixDebug(n === 1 ? string : stringPlural); 322 | if (scope) { 323 | scope.$count = n; 324 | string = $interpolate(string)(scope); 325 | } 326 | return addTranslatedMarkers(string); 327 | }, 328 | 329 | /** 330 | * @ngdoc method 331 | * @name gettextCatalog#loadRemote 332 | * @public 333 | * @param {String} url location of the translations 334 | * @description Load a set of translation strings from a given URL. 335 | * 336 | * This should be a JSON catalog generated with [angular-gettext-tools](https://github.com/rubenv/angular-gettext-tools). 337 | * {@link guide:lazy-loading More details here}. 338 | */ 339 | loadRemote: function (url) { 340 | return $http({ 341 | method: 'GET', 342 | url: url, 343 | cache: catalog.cache 344 | }).then(function (response) { 345 | var data = response.data; 346 | for (var lang in data) { 347 | catalog.setStrings(lang, data[lang]); 348 | } 349 | return response; 350 | }); 351 | } 352 | }; 353 | 354 | return catalog; 355 | }]); 356 | 357 | /** 358 | * @ngdoc directive 359 | * @module gettext 360 | * @name translate 361 | * @requires gettextCatalog 362 | * @requires gettextUtil 363 | * @requires https://docs.angularjs.org/api/ng/service/$parse $parse 364 | * @requires https://docs.angularjs.org/api/ng/service/$animate $animate 365 | * @requires https://docs.angularjs.org/api/ng/service/$compile $compile 366 | * @requires https://docs.angularjs.org/api/ng/service/$window $window 367 | * @restrict AE 368 | * @param {String} [translatePlural] plural form 369 | * @param {Number} translateN value to watch to substitute correct plural form 370 | * @param {String} translateContext context value, e.g. {@link doc:context Verb, Noun} 371 | * @description Annotates and translates text inside directive 372 | * 373 | * Full interpolation support is available in translated strings, so the following will work as expected: 374 | * ```js 375 | *
Hello {{name}}!
376 | * ``` 377 | * 378 | * You can also use custom context parameters while interpolating. This approach allows usage 379 | * of angular filters as well as custom logic inside your translated messages without unnecessary impact on translations. 380 | * 381 | * So for example when you have message like this: 382 | * ```js 383 | *
Last modified {{modificationDate | date:'yyyy-MM-dd HH:mm:ss Z'}} by {{author}}.
384 | * ``` 385 | * you will have it extracted in exact same version so it would look like this: 386 | * `Last modified {{modificationDate | date:'yyyy-MM-dd HH:mm:ss Z'}} by {{author}}`. 387 | * To start with it might be too complicated to read and handle by non technical translator. It's easy to make mistake 388 | * when copying format for example. Secondly if you decide to change format by some point of the project translation will broke 389 | * as it won't be the same string anymore. 390 | * 391 | * Instead your translator should only be concerned to place {{modificationDate}} correctly and you should have a free hand 392 | * to modify implementation details on how to present the results. This is how you can achieve the goal: 393 | * ```js 394 | *
Last modified {{modificationDate}} by {{author}}.
395 | * ``` 396 | * 397 | * There's a few more things worth to point out: 398 | * 1. You can use as many parameters as you want. Each parameter begins with `translate-params-` followed by snake-case parameter name. 399 | * Each parameter will be available for interpolation in camelCase manner (just like angular directive works by default). 400 | * ```js 401 | *
Param {{myCustomParam}} has been changed by {{name}}.
402 | * ``` 403 | * 2. You can rename your variables from current scope to simple ones if you like. 404 | * ```js 405 | *
Today's date is: {{date}}.
406 | * ``` 407 | * 3. You can use translate-params only for some interpolations. Rest would be treated as usual. 408 | * ```js 409 | *
This product: {{product}} costs {{cost}}.
410 | * ``` 411 | */ 412 | angular.module('gettext').directive('translate', ["gettextCatalog", "$parse", "$animate", "$compile", "$window", "gettextUtil", function (gettextCatalog, $parse, $animate, $compile, $window, gettextUtil) { 413 | var msie = parseInt((/msie (\d+)/i.exec($window.navigator.userAgent) || [])[1], 10); 414 | var PARAMS_PREFIX = 'translateParams'; 415 | 416 | function getCtxAttr(key) { 417 | return gettextUtil.lcFirst(key.replace(PARAMS_PREFIX, '')); 418 | } 419 | 420 | function handleInterpolationContext(scope, attrs, update) { 421 | var attributes = Object.keys(attrs).filter(function (key) { 422 | return gettextUtil.startsWith(key, PARAMS_PREFIX) && key !== PARAMS_PREFIX; 423 | }); 424 | 425 | if (!attributes.length) { 426 | return null; 427 | } 428 | 429 | var interpolationContext = scope.$new(); 430 | var unwatchers = []; 431 | attributes.forEach(function (attribute) { 432 | var unwatch = scope.$watch(attrs[attribute], function (newVal) { 433 | var key = getCtxAttr(attribute); 434 | interpolationContext[key] = newVal; 435 | update(interpolationContext); 436 | }); 437 | unwatchers.push(unwatch); 438 | }); 439 | scope.$on('$destroy', function () { 440 | unwatchers.forEach(function (unwatch) { 441 | unwatch(); 442 | }); 443 | 444 | interpolationContext.$destroy(); 445 | }); 446 | return interpolationContext; 447 | } 448 | 449 | return { 450 | restrict: 'AE', 451 | terminal: true, 452 | compile: function compile(element, attrs) { 453 | var translate = attrs.translate; 454 | if (translate && translate.match(/^yes|no$/i)) { 455 | // Ignore the translate attribute if it has a "yes" or "no" value, assuming that it is the HTML 456 | // native translate attribute, see 457 | // https://html.spec.whatwg.org/multipage/dom.html#the-translate-attribute 458 | // 459 | // In that case we skip processing as this attribute is intended for the user agent itself. 460 | return; 461 | } 462 | 463 | // Validate attributes 464 | gettextUtil.assert(!attrs.translatePlural || attrs.translateN, 'translate-n', 'translate-plural'); 465 | gettextUtil.assert(!attrs.translateN || attrs.translatePlural, 'translate-plural', 'translate-n'); 466 | 467 | var msgid = gettextUtil.trim(element.html()); 468 | var translatePlural = attrs.translatePlural; 469 | var translateContext = attrs.translateContext; 470 | 471 | if (msie <= 8) { 472 | // Workaround fix relating to angular adding a comment node to 473 | // anchors. angular/angular.js/#1949 / angular/angular.js/#2013 474 | if (msgid.slice(-13) === '') { 475 | msgid = msgid.slice(0, -13); 476 | } 477 | } 478 | 479 | return { 480 | post: function (scope, element, attrs) { 481 | var countFn = $parse(attrs.translateN); 482 | var pluralScope = null; 483 | var linking = true; 484 | 485 | function update(interpolationContext) { 486 | interpolationContext = interpolationContext || null; 487 | 488 | // Fetch correct translated string. 489 | var translated; 490 | if (translatePlural) { 491 | scope = pluralScope || (pluralScope = scope.$new()); 492 | scope.$count = countFn(scope); 493 | translated = gettextCatalog.getPlural(scope.$count, msgid, translatePlural, null, translateContext); 494 | } else { 495 | translated = gettextCatalog.getString(msgid, null, translateContext); 496 | } 497 | var oldContents = element.contents(); 498 | 499 | if (!oldContents && !translated){ 500 | return; 501 | } 502 | 503 | // Avoid redundant swaps 504 | if (translated === gettextUtil.trim(oldContents.html())){ 505 | // Take care of unlinked content 506 | if (linking){ 507 | $compile(oldContents)(scope); 508 | } 509 | return; 510 | } 511 | 512 | // Swap in the translation 513 | var newWrapper = angular.element('' + translated + ''); 514 | $compile(newWrapper.contents())(interpolationContext || scope); 515 | var newContents = newWrapper.contents(); 516 | 517 | $animate.enter(newContents, element); 518 | $animate.leave(oldContents); 519 | } 520 | 521 | var interpolationContext = handleInterpolationContext(scope, attrs, update); 522 | update(interpolationContext); 523 | linking = false; 524 | 525 | if (attrs.translateN) { 526 | scope.$watch(attrs.translateN, function () { 527 | update(interpolationContext); 528 | }); 529 | } 530 | 531 | /** 532 | * @ngdoc event 533 | * @name translate#gettextLanguageChanged 534 | * @eventType listen on scope 535 | * @description Listens for language updates and changes translation accordingly 536 | */ 537 | scope.$on('gettextLanguageChanged', function () { 538 | update(interpolationContext); 539 | }); 540 | 541 | } 542 | }; 543 | } 544 | }; 545 | }]); 546 | 547 | /** 548 | * @ngdoc factory 549 | * @module gettext 550 | * @name gettextFallbackLanguage 551 | * @param {String} langCode language code 552 | * @returns {String|Null} fallback language 553 | * @description Strips regional code and returns language code only 554 | * 555 | * Example 556 | * ```js 557 | * gettextFallbackLanguage('ru'); // "null" 558 | * gettextFallbackLanguage('en_GB'); // "en" 559 | * gettextFallbackLanguage(); // null 560 | * ``` 561 | */ 562 | angular.module("gettext").factory("gettextFallbackLanguage", function () { 563 | var cache = {}; 564 | var pattern = /([^_]+)_[^_]+$/; 565 | 566 | return function (langCode) { 567 | if (cache[langCode]) { 568 | return cache[langCode]; 569 | } 570 | 571 | var matches = pattern.exec(langCode); 572 | if (matches) { 573 | cache[langCode] = matches[1]; 574 | return matches[1]; 575 | } 576 | 577 | return null; 578 | }; 579 | }); 580 | /** 581 | * @ngdoc filter 582 | * @module gettext 583 | * @name translate 584 | * @requires gettextCatalog 585 | * @param {String} input translation key 586 | * @param {String} context context to evaluate key against 587 | * @returns {String} translated string or annotated key 588 | * @see {@link doc:context Verb, Noun} 589 | * @description Takes key and returns string 590 | * 591 | * Sometimes it's not an option to use an attribute (e.g. when you want to annotate an attribute value). 592 | * There's a `translate` filter available for this purpose. 593 | * 594 | * ```html 595 | * 596 | * ``` 597 | * This filter does not support plural strings. 598 | * 599 | * You may want to use {@link guide:custom-annotations custom annotations} to avoid using the `translate` filter all the time. * Is 600 | */ 601 | angular.module('gettext').filter('translate', ["gettextCatalog", function (gettextCatalog) { 602 | function filter(input, context) { 603 | return gettextCatalog.getString(input, null, context); 604 | } 605 | filter.$stateful = true; 606 | return filter; 607 | }]); 608 | 609 | // Do not edit this file, it is autogenerated using genplurals.py! 610 | angular.module("gettext").factory("gettextPlurals", function () { 611 | var languageCodes = { 612 | "pt_BR": "pt_BR", 613 | "pt-BR": "pt_BR" 614 | }; 615 | return function (langCode, n) { 616 | switch (getLanguageCode(langCode)) { 617 | case "ay": // Aymará 618 | case "bo": // Tibetan 619 | case "cgg": // Chiga 620 | case "dz": // Dzongkha 621 | case "fa": // Persian 622 | case "id": // Indonesian 623 | case "ja": // Japanese 624 | case "jbo": // Lojban 625 | case "ka": // Georgian 626 | case "kk": // Kazakh 627 | case "km": // Khmer 628 | case "ko": // Korean 629 | case "ky": // Kyrgyz 630 | case "lo": // Lao 631 | case "ms": // Malay 632 | case "my": // Burmese 633 | case "sah": // Yakut 634 | case "su": // Sundanese 635 | case "th": // Thai 636 | case "tt": // Tatar 637 | case "ug": // Uyghur 638 | case "vi": // Vietnamese 639 | case "wo": // Wolof 640 | case "zh": // Chinese 641 | // 1 form 642 | return 0; 643 | case "is": // Icelandic 644 | // 2 forms 645 | return (n%10!=1 || n%100==11) ? 1 : 0; 646 | case "jv": // Javanese 647 | // 2 forms 648 | return n!=0 ? 1 : 0; 649 | case "mk": // Macedonian 650 | // 2 forms 651 | return n==1 || n%10==1 ? 0 : 1; 652 | case "ach": // Acholi 653 | case "ak": // Akan 654 | case "am": // Amharic 655 | case "arn": // Mapudungun 656 | case "br": // Breton 657 | case "fil": // Filipino 658 | case "fr": // French 659 | case "gun": // Gun 660 | case "ln": // Lingala 661 | case "mfe": // Mauritian Creole 662 | case "mg": // Malagasy 663 | case "mi": // Maori 664 | case "oc": // Occitan 665 | case "pt_BR": // Brazilian Portuguese 666 | case "tg": // Tajik 667 | case "ti": // Tigrinya 668 | case "tr": // Turkish 669 | case "uz": // Uzbek 670 | case "wa": // Walloon 671 | case "zh": // Chinese 672 | // 2 forms 673 | return n>1 ? 1 : 0; 674 | case "lv": // Latvian 675 | // 3 forms 676 | return (n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2); 677 | case "lt": // Lithuanian 678 | // 3 forms 679 | return (n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2); 680 | case "be": // Belarusian 681 | case "bs": // Bosnian 682 | case "hr": // Croatian 683 | case "ru": // Russian 684 | case "sr": // Serbian 685 | case "uk": // Ukrainian 686 | // 3 forms 687 | return (n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2); 688 | case "mnk": // Mandinka 689 | // 3 forms 690 | return (n==0 ? 0 : n==1 ? 1 : 2); 691 | case "ro": // Romanian 692 | // 3 forms 693 | return (n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2); 694 | case "pl": // Polish 695 | // 3 forms 696 | return (n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2); 697 | case "cs": // Czech 698 | case "sk": // Slovak 699 | // 3 forms 700 | return (n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2; 701 | case "sl": // Slovenian 702 | // 4 forms 703 | return (n%100==1 ? 1 : n%100==2 ? 2 : n%100==3 || n%100==4 ? 3 : 0); 704 | case "mt": // Maltese 705 | // 4 forms 706 | return (n==1 ? 0 : n==0 || ( n%100>1 && n%100<11) ? 1 : (n%100>10 && n%100<20 ) ? 2 : 3); 707 | case "gd": // Scottish Gaelic 708 | // 4 forms 709 | return (n==1 || n==11) ? 0 : (n==2 || n==12) ? 1 : (n > 2 && n < 20) ? 2 : 3; 710 | case "cy": // Welsh 711 | // 4 forms 712 | return (n==1) ? 0 : (n==2) ? 1 : (n != 8 && n != 11) ? 2 : 3; 713 | case "kw": // Cornish 714 | // 4 forms 715 | return (n==1) ? 0 : (n==2) ? 1 : (n == 3) ? 2 : 3; 716 | case "ga": // Irish 717 | // 5 forms 718 | return n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n<11 ? 3 : 4; 719 | case "ar": // Arabic 720 | // 6 forms 721 | return (n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5); 722 | default: // Everything else 723 | return n != 1 ? 1 : 0; 724 | } 725 | }; 726 | 727 | /** 728 | * Method extracts iso639-2 language code from code with locale e.g. pl_PL, en_US, etc. 729 | * If it's provided with standalone iso639-2 language code it simply returns it. 730 | * @param {String} langCode 731 | * @returns {String} iso639-2 language Code 732 | */ 733 | function getLanguageCode(langCode) { 734 | if (!languageCodes[langCode]) { 735 | languageCodes[langCode] = langCode.split(/\-|_/).shift(); 736 | } 737 | return languageCodes[langCode]; 738 | } 739 | }); 740 | 741 | /** 742 | * @ngdoc factory 743 | * @module gettext 744 | * @name gettextUtil 745 | * @description Utility service for common operations and polyfills. 746 | */ 747 | angular.module('gettext').factory('gettextUtil', function gettextUtil() { 748 | /** 749 | * @ngdoc method 750 | * @name gettextUtil#trim 751 | * @public 752 | * @param {string} value String to be trimmed. 753 | * @description Trim polyfill for old browsers (instead of jQuery). Based on AngularJS-v1.2.2 (angular.js#620). 754 | * 755 | * Example 756 | * ```js 757 | * gettextUtil.assert(' no blanks '); // "no blanks" 758 | * ``` 759 | */ 760 | var trim = (function () { 761 | if (!String.prototype.trim) { 762 | return function (value) { 763 | return (typeof value === 'string') ? value.replace(/^\s*/, '').replace(/\s*$/, '') : value; 764 | }; 765 | } 766 | return function (value) { 767 | return (typeof value === 'string') ? value.trim() : value; 768 | }; 769 | })(); 770 | 771 | /** 772 | * @ngdoc method 773 | * @name gettextUtil#assert 774 | * @public 775 | * @param {bool} condition condition to check 776 | * @param {String} missing name of the directive missing attribute 777 | * @param {String} found name of attribute that has been used with directive 778 | * @description Throws error if condition is not met, which means that directive was used with certain parameter 779 | * that requires another one (which is missing). 780 | * 781 | * Example 782 | * ```js 783 | * gettextUtil.assert(!attrs.translatePlural || attrs.translateN, 'translate-n', 'translate-plural'); 784 | * //You should add a translate-n attribute whenever you add a translate-plural attribute. 785 | * ``` 786 | */ 787 | function assert(condition, missing, found) { 788 | if (!condition) { 789 | throw new Error('You should add a ' + missing + ' attribute whenever you add a ' + found + ' attribute.'); 790 | } 791 | } 792 | 793 | /** 794 | * @ngdoc method 795 | * @name gettextUtil#startsWith 796 | * @public 797 | * @param {string} target String on which checking will occur. 798 | * @param {string} query String expected to be at the beginning of target. 799 | * @returns {boolean} Returns true if object has no ownProperties. For arrays returns true if length == 0. 800 | * @description Checks if string starts with another string. 801 | * 802 | * Example 803 | * ```js 804 | * gettextUtil.startsWith('Home sweet home.', 'Home'); //true 805 | * gettextUtil.startsWith('Home sweet home.', 'sweet'); //false 806 | * ``` 807 | */ 808 | function startsWith(target, query) { 809 | return target.indexOf(query) === 0; 810 | } 811 | 812 | /** 813 | * @ngdoc method 814 | * @name gettextUtil#lcFirst 815 | * @public 816 | * @param {string} target String to transform. 817 | * @returns {string} Strings beginning with lowercase letter. 818 | * @description Makes first letter of the string lower case 819 | * 820 | * Example 821 | * ```js 822 | * gettextUtil.lcFirst('Home Sweet Home.'); //'home Sweet Home' 823 | * gettextUtil.lcFirst('ShouldBeCamelCase.'); //'shouldBeCamelCase' 824 | * ``` 825 | */ 826 | function lcFirst(target) { 827 | var first = target.charAt(0).toLowerCase(); 828 | return first + target.substr(1); 829 | } 830 | 831 | return { 832 | trim: trim, 833 | assert: assert, 834 | startsWith: startsWith, 835 | lcFirst: lcFirst 836 | }; 837 | }); 838 | --------------------------------------------------------------------------------