diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..adb99af --- /dev/null +++ b/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["@babel/preset-env"], + "plugins": ["@babel/plugin-transform-modules-umd"] +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..10023b3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +/tests/ export-ignore +.* export-ignore + +# Set the line ending configuration +* text=lf diff --git a/.gitignore b/.gitignore index 5b07f03..be43bd8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ yarn.lock -package-lock.json \ No newline at end of file +package-lock.json +dist \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ad3b8e..eb7c932 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,39 @@ # Changelog - All notable changes to this project will be documented in this file. -The format is based on [Keep a Changelog](http://keepachangelog.com/) +The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## 2.1.0 - 2018-06-12 +## [3.0.2] - 2024-06-19 +### Added +- Missing `dn__` shortcut function [#8]. +## [3.0.1] - 2019-11-24 ### Added +- Support for UMD [#5] +### Fixed +- Tests + +## [3.0.0] - 2019-11-03 +### Added +- Support for ES6 modules. +- The long functions (gettext, ngettext, etc) supports arguments to format the result (before, only short functions __, n__, etc had this) +- You can use objects to format the text by search and replace. For example: `t.gettext('Hello _world', {_world: 'World'})` + +### Removed +- Support for AMD and Global js. +- Sprintf dependency by default. Now the library has a (very) limited sprintf implementation but open to extend and improved. + +## [2.1.0] - 2018-06-12 +### Added - Allow to include the plural function in the translations to prevent CSP errors [#4] -[#4]: https://github.com/oscarotero/gettext-translator/issues/4 +[#4]: https://github.com/php-gettext/gettext-translator/issues/4 +[#5]: https://github.com/php-gettext/gettext-translator/issues/5 +[#8]: https://github.com/php-gettext/gettext-translator/issues/8 +[3.0.2]: https://github.com/php-gettext/gettext-translator/compare/v3.0.1...v3.0.2 +[3.0.1]: https://github.com/php-gettext/gettext-translator/compare/v3.0.0...v3.0.1 +[3.0.0]: https://github.com/php-gettext/gettext-translator/compare/v2.1.0...v3.0.0 +[2.1.0]: https://github.com/php-gettext/gettext-translator/releases/tag/v2.1.0 diff --git a/README.md b/README.md index e014767..8c4967a 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,6 @@ # Gettext translator -Javascript gettext translator. Use [gettext/gettext](https://github.com/oscarotero/Gettext) PHP library to generate and modify the messages. - -Supports: - -* AMD -* CommonJS -* Global js +Javascript gettext translations replacement to use with [gettext/gettext](https://github.com/php-gettext/Gettext). Use [gettext/json](https://github.com/php-gettext/Json) to generate the json data. ## Installation @@ -16,47 +10,78 @@ npm install gettext-translator ## Usage -Use the Json generator of the [gettext/gettext](https://github.com/oscarotero/Gettext) library to export the translations to json: +Use the Json generator [gettext/json](https://github.com/php-gettext/Json) library to export the translations to json: ```php -use Gettext\Translations; +use Gettext\Loader\PoLoader; +use Gettext\Generator\JsonGenerator; //Load the po file with the translations -$translations = Translations::fromPoFile('locales/gl.po'); +$translations = (new PoLoader())->loadFile('locales/gl.po'); //Export to a json file -$translations->toJsonFile('locales/gl.json'); +(new JsonGenerator())->generateFile($translations, 'locales/gl.json'); +``` + +Load the json file in your browser + +```js +import Translator from 'gettext-translator'; + +async function getTranslator() { + const response = await fetch('locales/gl.json'); + const translations = await response.json(); + + return new Translator(translations); +} + +const t = await getTranslator(); + +t.gettext('hello world'); //ola mundo ``` -Load the json file in your browser (for example, using webpack) and use it +## Variables + +You can add variables to the translations. For example: ```js -var Translator = require('gettext-translator'); -var translations = require('locales/gl.json'); +t.gettext('hello :who', {':who': 'world'}); //ola world +``` -var i18n = new Translator(translations); +There's also a basic support o sprintf (only `%s` and `%d`) -console.log(i18n.gettext('hello world')); //ola mundo +```js +t.gettext('hello %s', 'world'); //ola world ``` -## Sprintf +To customize the translator formatter, just override the `format` method: -This library includes [sprintf](https://github.com/alexei/sprintf.js) dependency implemented in the short methods like `__`, `n__`, etc...: +```js +t.format = function (text, ...args) { + //Your custom format here +} +``` + +## Short names + +Like in the [php version](https://github.com/php-gettext/Translator), there are the `__` functions that are alias of the long version: ```js -i18n.__('Hello %s', 'world'); //Hello world -i18n.n__('One comment', '%s comments', 12, 12); //12 comments +//Both functions does the same + +t.gettext('Foo'); +t.__('Foo'); ``` ## API -Long method | Short + sprintf | description ------- | ----- | ----------- -gettext | __ | Returns a translation -ngettext | n__ | Returns a translation with singular/plural variations -dngettext | dn__ | Returns a translation with domain and singular/plural variations -npgettext | np__ | Returns a translation with context and singular/plural variations -pgettext | p__ | Returns a translation with a specific context -dgettext | d__ | Returns a translation with a specific domain -dpgettext | dp__ | Returns a translation with a specific domain and context -dnpgettext | dnp__ | Returns a translation with a specific domain, context and singular/plural variations +Long name | Short name | Description +-----------| -----------| ----------- +gettext | __ | Returns a translation +ngettext | n__ | Returns a translation with singular/plural variations +dngettext | dn__ | Returns a translation with domain and singular/plural variations +npgettext | np__ | Returns a translation with context and singular/plural variations +pgettext | p__ | Returns a translation with a specific context +dgettext | d__ | Returns a translation with a specific domain +dpgettext | dp__ | Returns a translation with a specific domain and context +dnpgettext | dnp__ | Returns a translation with a specific domain, context and singular/plural variations diff --git a/package.json b/package.json index a705296..00d1c2b 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,17 @@ { "name": "gettext-translator", - "version": "2.1.0", + "version": "3.0.2", "description": "Javascript gettext translator", - "main": "src/translator.js", - "directories": { - "test": "tests" - }, + "main": "dist/translator.js", + "module": "src/translator.js", + "files": [ + "dist", + "src" + ], "scripts": { - "test": "mocha tests/test.js" + "test": "mocha --require @babel/register tests/test.js", + "build": "babel src --out-dir dist", + "prettier": "prettier src/*.js tests/*.js --single-quote --tab-width 4 --write --print-width 120" }, "keywords": [ "gettext", @@ -16,10 +20,13 @@ ], "author": "oscarotero ", "license": "MIT", - "dependencies": { - "sprintf-js": "^1.0.3" - }, "devDependencies": { - "mocha": "^5.0.0" + "@babel/cli": "^7.24.7", + "@babel/core": "^7.24.7", + "@babel/plugin-transform-modules-umd": "^7.24.7", + "@babel/preset-env": "^7.24.7", + "@babel/register": "^7.24.6", + "mocha": "^6.2.3", + "prettier": "^1.19.1" } } diff --git a/src/translator.js b/src/translator.js index e783d61..338d19d 100644 --- a/src/translator.js +++ b/src/translator.js @@ -1,206 +1,193 @@ -(function (root, factory) { - //amd - if (typeof define === "function" && define.amd) { - define(['sprintf-js'], function (sprintf) { - return factory(sprintf.vsprintf); - }); - - //commonjs - } else if (typeof module === "object" && module.exports) { - module.exports = factory(require('sprintf-js').vsprintf); - - //global - } else { - root.Translator = factory(window.vsprintf); - } -}(this, function (vsprintf) { - - function Translator (translations) { +export default class Translator { + constructor(translations) { this.dictionary = {}; this.plurals = {}; - this.domain = null; + this.domain = undefined; if (translations) { this.loadTranslations(translations); } } - Translator.prototype = { - loadTranslations: function (translations) { - var domain = translations.domain || ''; + loadTranslations(translations) { + const domain = translations.domain || ''; - if (this.domain === null) { - this.domain = domain; - } + if (this.domain === undefined) { + this.domain = domain; + } - if (this.dictionary[domain]) { - mergeTranslations(this.dictionary[domain], translations.messages); - return this; - } + if (this.dictionary[domain]) { + mergeTranslations(this.dictionary[domain], translations.messages); + return this; + } - if (translations.fn) { - this.plurals[domain] = { fn: translations.fn }; - } else if (translations['plural-forms']) { - var plural = translations['plural-forms'].split(';', 2); + if (translations.fn) { + this.plurals[domain] = { fn: translations.fn }; + } else if (translations['plural-forms']) { + const plural = translations['plural-forms'].split(';', 2); - this.plurals[domain] = { - count: parseInt(plural[0].replace('nplurals=', '')), - code: plural[1].replace('plural=', 'return ') + ';' - }; - } + this.plurals[domain] = { + count: parseInt(plural[0].replace('nplurals=', '')), + code: plural[1].replace('plural=', 'return ') + ';' + }; + } - this.dictionary[domain] = translations.messages; + this.dictionary[domain] = translations.messages; - return this; - }, + return this; + } - defaultDomain: function (domain) { - this.domain = domain; + defaultDomain(domain) { + this.domain = domain; + return this; + } - return this; - }, + gettext(original, ...args) { + return this.format(this.translate(undefined, undefined, original), ...args); + } - gettext: function (original) { - return this.dpgettext(this.domain, null, original); - }, + ngettext(original, plural, counter, ...args) { + return this.format(this.translatePlural(undefined, undefined, original, plural, counter), ...args); + } - ngettext: function (original, plural, value) { - return this.dnpgettext(this.domain, null, original, plural, value); - }, + dngettext(domain, original, plural, counter, ...args) { + return this.format(this.translatePlural(domain, undefined, original, plural, counter), ...args); + } - dngettext: function (domain, original, plural, value) { - return this.dnpgettext(domain, null, original, plural, value); - }, + npgettext(context, original, plural, counter, ...args) { + return this.format(this.translatePlural(undefined, context, original, plural, counter), ...args); + } - npgettext: function (context, original, plural, value) { - return this.dnpgettext(this.domain, context, original, plural, value); - }, + pgettext(context, original, ...args) { + return this.format(this.translate(undefined, context, original), ...args); + } - pgettext: function (context, original) { - return this.dpgettext(this.domain, context, original); - }, + dgettext(domain, original, ...args) { + return this.format(this.translate(domain, undefined, original), ...args); + } - dgettext: function (domain, original) { - return this.dpgettext(domain, null, original); - }, + dpgettext(domain, context, original, ...args) { + return this.format(this.translate(domain, context, original), ...args); + } - dpgettext: function (domain, context, original) { - var translation = getTranslation(this.dictionary, domain, context, original); + dnpgettext(domain, context, original, plural, counter, ...args) { + return this.format(this.translatePlural(domain, context, original, plural, counter), ...args); + } - if (translation !== false && translation[0] !== '') { - return translation[0]; - } + __(original, ...args) { + return this.gettext(original, ...args); + } - return original; - }, + n__(original, plural, value, ...args) { + return this.ngettext(original, plural, value, ...args); + } - dnpgettext: function (domain, context, original, plural, value) { - var index = getPluralIndex(this.plurals, domain, value); - var translation = getTranslation(this.dictionary, domain, context, original); + p__(context, original, ...args) { + return this.pgettext(context, original, ...args); + } - if (translation[index] && translation[index] !== '') { - return translation[index]; - } + d__(domain, original, ...args) { + return this.dgettext(domain, original, ...args); + } - return (index === 0) ? original : plural; - }, - - __: function (original) { - return format( - this.gettext(original), - Array.prototype.slice.call(arguments, 1) - ); - }, - - n__: function (original, plural, value) { - return format( - this.ngettext(original, plural, value), - Array.prototype.slice.call(arguments, 3) - ); - }, - - p__: function (context, original) { - return format( - this.pgettext(context, original), - Array.prototype.slice.call(arguments, 2) - ); - }, - - d__: function (domain, original) { - return format( - this.dgettext(domain, original), - Array.prototype.slice.call(arguments, 2) - ); - }, - - dp__: function (domain, context, original) { - return format( - this.dgettext(domain, context, original), - Array.prototype.slice.call(arguments, 3) - ); - }, - - np__: function (context, original, plural, value) { - return format( - this.npgettext(context, original, plural, value), - Array.prototype.slice.call(arguments, 4) - ); - }, - - dnp__: function (domain, context, original, plural, value) { - return format( - this.dnpgettext(domain, context, original, plural, value), - Array.prototype.slice.call(arguments, 5) - ); - } - }; + dn__(domain, original, plural, counter, ...args) { + return this.dngettext(domain, original, plural, counter, ...args); + } - function getTranslation(dictionary, domain, context, original) { - context = context || ''; + dp__(domain, context, original, ...args) { + return this.dpgettext(domain, context, original, ...args); + } - if (!dictionary[domain] || !dictionary[domain][context] || !dictionary[domain][context][original]) { - return false; - } + np__(context, original, plural, value, ...args) { + return this.npgettext(context, original, plural, value, ...args); + } - return dictionary[domain][context][original]; + dnp__(domain, context, original, plural, value, ...args) { + return this.dnpgettext(domain, context, original, plural, value, ...args); } - function getPluralIndex(plurals, domain, value) { - if (!plurals[domain]) { - return value == 1 ? 0 : 1; + format(text, ...args) { + if (!args.length) { + return text; } - if (!plurals[domain].fn) { - plurals[domain].fn = new Function('n', plurals[domain].code); - } + if (typeof args[0] === 'object') { + Object.keys(args[0]).forEach(search => { + text = text.replace(search, args[0][search]); + }); - return plurals[domain].fn.call(this, value) + 0; - } + return text; + } - function mergeTranslations(translations, newTranslations) { - for (var context in newTranslations) { - if (!translations[context]) { - translations[context] = newTranslations[context]; - continue; + return text.replace(/(%[sd])/g, function(match) { + if (!args.length) { + return match; } - for (var original in newTranslations[context]) { - translations[context][original] = newTranslations[context][original]; + switch (match) { + case '%s': + return args.shift(); + + case '%d': + return parseFloat(args.shift()); } + }); + } + + translate(domain, context, original) { + const translation = this.getTranslation(domain, context, original); + + return translation && translation[0] ? translation[0] : original; + } + + translatePlural(domain, context, original, plural, counter) { + const translation = this.getTranslation(domain, context, original); + const index = this.getPluralIndex(domain, counter); + + return translation && translation[index] ? translation[index] : index === 0 ? original : plural; + } + + getTranslation(domain, context, original) { + domain = domain || this.domain; + context = context || ''; + + if ( + !this.dictionary[domain] || + !this.dictionary[domain][context] || + !this.dictionary[domain][context][original] + ) { + return undefined; } + + const translation = this.dictionary[domain][context][original]; + + return Array.isArray(translation) ? translation : [translation]; } - function format (text, args) { - if (!args.length) { - return text; + getPluralIndex(domain, value) { + domain = domain || this.domain; + + if (!this.plurals[domain]) { + return value == 1 ? 0 : 1; } - if (args[0] instanceof Array) { - return vsprintf(text, args[0]); + if (!this.plurals[domain].fn) { + this.plurals[domain].fn = new Function('n', this.plurals[domain].code); } - return vsprintf(text, args); + return this.plurals[domain].fn.call(this, value) + 0; } +} - return Translator; -})); +function mergeTranslations(translations, newTranslations) { + for (let context in newTranslations) { + if (!translations[context]) { + translations[context] = newTranslations[context]; + continue; + } + + for (let original in newTranslations[context]) { + translations[context][original] = newTranslations[context][original]; + } + } +} diff --git a/tests/test.js b/tests/test.js index a33d881..69e0fd0 100644 --- a/tests/test.js +++ b/tests/test.js @@ -1,51 +1,55 @@ -var assert = require('assert'); -var Translator = require(__dirname + '/../src/translator'); +import assert from 'assert'; +import Translator from '../src/translator.js'; -var i18n = new Translator(require(__dirname + '/translations.json')); -i18n.loadTranslations(require(__dirname + '/translations2.json')); +import translations1 from './translations.json'; +import translations2 from './translations2.json'; +import translations3 from './translations3.json'; + +const translator = new Translator(translations1); +translator.loadTranslations(translations2); describe('Basic functions', function() { - it('gettext', function () { - assert.equal('vaca', i18n.gettext('cow')); + it('gettext', function() { + assert.equal('vaca', translator.gettext('cow')); }); - it('ngettext', function () { - assert.equal('un arquivo', i18n.ngettext('one file', '%s files', 1)); - assert.equal('%s arquivos', i18n.ngettext('one file', '%s files', 0)); - assert.equal('%s arquivos', i18n.ngettext('one file', '%s files', 2)); + it('ngettext', function() { + assert.equal('un arquivo', translator.ngettext('one file', '%s files', 1)); + assert.equal('%s arquivos', translator.ngettext('one file', '%s files', 0)); + assert.equal('%s arquivos', translator.ngettext('one file', '%s files', 2)); }); }); -describe('vsprintf functions', function() { - it('__', function () { - assert.equal('vaca', i18n.__('cow')); +describe('Formatter functions', function() { + it('__', function() { + assert.equal('this is a cow', translator.__('this is a :animal', { ':animal': 'cow' })); }); - it('n__', function () { - assert.equal('un arquivo', i18n.n__('one file', '%s files', 1, 1)); - assert.equal('0 arquivos', i18n.n__('one file', '%s files', 0, 0)); - assert.equal('2 arquivos', i18n.n__('one file', '%s files', 2, 2)); + it('n__', function() { + assert.equal('un arquivo', translator.n__('one file', '%s files', 1, 1)); + assert.equal('0 arquivos', translator.n__('one file', '%s files', 0, 0)); + assert.equal('2 arquivos', translator.n__('one file', '%s files', 2, 2)); }); }); describe('custom plural functions', function() { - var translations = require(__dirname + '/translations3.json'); - var called = false; - translations.fn = function (n) { + let called = false; + + translations3.fn = function(n) { called = true; return 2; }; - i18n.loadTranslations(translations); + translator.loadTranslations(translations3); - it('should return always the second plural', function () { - assert.equal('foo 3', i18n.dngettext('foo', 'foo x', 'foo y', 0)); - assert.equal('foo 3', i18n.dngettext('foo', 'foo x', 'foo y', 1)); - assert.equal('foo 3', i18n.dngettext('foo', 'foo x', 'foo y', 2)); - assert.equal('foo 3', i18n.dngettext('foo', 'foo x', 'foo y', 3)); + it('should return always the second plural', function() { + assert.equal('foo 3', translator.dngettext('foo', 'foo x', 'foo y', 0)); + assert.equal('foo 3', translator.dngettext('foo', 'foo x', 'foo y', 1)); + assert.equal('foo 3', translator.dngettext('foo', 'foo x', 'foo y', 2)); + assert.equal('foo 3', translator.dngettext('foo', 'foo x', 'foo y', 3)); }); - it('the custom function was called', function () { - assert(called, 'The custom function was not called') + it('the custom function was called', function() { + assert(called, 'The custom function was not called'); }); }); diff --git a/tests/translations.json b/tests/translations.json index 12428a7..4f70bbd 100644 --- a/tests/translations.json +++ b/tests/translations.json @@ -3,9 +3,6 @@ "plural-forms": "nplurals=3; plural=n == 1 ? 0 : 1;", "messages": { "": { - "": [ - "Content-Transfer-Encoding: 8bit\nContent-Type: text\/plain; charset=UTF-8\nLanguage: \nLanguage-Team: \nLast-Translator: \nMIME-Version: 1.0\nPlural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\nProject-Id-Version: \nReport-Msgid-Bugs-To: \n" - ], "one file": [ "un arquivo", "%s arquivos" diff --git a/tests/translations2.json b/tests/translations2.json index 55d68d8..f00fad4 100644 --- a/tests/translations2.json +++ b/tests/translations2.json @@ -3,9 +3,6 @@ "plural-forms": "nplurals=3; plural=n == 1 ? 0 : 1;", "messages": { "": { - "": [ - "Content-Transfer-Encoding: 8bit\nContent-Type: text\/plain; charset=UTF-8\nLanguage: \nLanguage-Team: \nLast-Translator: \nMIME-Version: 1.0\nPlural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\nProject-Id-Version: \nReport-Msgid-Bugs-To: \n" - ], "one file": [ "un arquivo", "%s arquivos"