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 | [](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("
")($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("
")($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("
")($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("
")($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 |
--------------------------------------------------------------------------------