handlebars-i18n 1.8.3 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,12 +2,11 @@
2
2
  * handlebars-i18n.js
3
3
  *
4
4
  * @author: Florian Walzel
5
- * @date: 2024-08
6
5
  *
7
6
  * handlebars-i18n adds features for localization/
8
7
  * internationalization to handlebars.js
9
8
  *
10
- * Copyright (c) 2020-24 Florian Walzel, MIT License
9
+ * Copyright (c) 2020-25 Florian Walzel, MIT License
11
10
  *
12
11
  *********************************************************************/
13
12
 
@@ -381,12 +380,34 @@
381
380
  * use like: {{__ "key_name"}}
382
381
  * or with attributes: {{__ "key_with_count" count=7}}
383
382
  *
384
- * @param str
385
- * @param attributes
383
+ * @param key
384
+ * @param options
386
385
  * @returns {*}
387
386
  */
388
- function (str, attributes) {
389
- return new handlebars.SafeString((typeof (i18next) !== 'undefined' ? i18next.t(str, attributes.hash) : str));
387
+ function (key, options) {
388
+ const hash = options.hash || {};
389
+
390
+ // Force object/array return if needed
391
+ const result = (typeof i18next !== "undefined")
392
+ ? i18next.t(key, {...hash, returnObjects: true})
393
+ : key;
394
+
395
+ if (typeof result === "string") {
396
+ return new handlebars.SafeString(result);
397
+ }
398
+ return result;
399
+ }
400
+ );
401
+ handlebars.registerHelper('keyExists',
402
+ /**
403
+ * checks if a translation key exists
404
+ * use like: {{#if (keyExists "myKey")}} {{__ "myKey"}} {{/if}}
405
+ *
406
+ * @param {string} key - The translation key to check
407
+ * @returns {boolean}
408
+ */
409
+ function (key) {
410
+ return i18next.exists(key);
390
411
  }
391
412
  );
392
413
  handlebars.registerHelper('_locale',
@@ -502,7 +523,7 @@
502
523
  break;
503
524
  default:
504
525
  throw new Error('@ handlebars-i18n: invalid argument "unit" was given for _dateAdd.' +
505
- 'Unit must be either "second" | "minute" | "hour" | "day" | "week" | "month" | "quarter" | "year".');
526
+ 'Unit must be one of "second" | "minute" | "hour" | "day" | "week" | "month" | "quarter" | "year".');
506
527
  }
507
528
 
508
529
  const dateFormat = new Intl.DateTimeFormat(i18next.language, opts);
@@ -534,6 +555,7 @@
534
555
  );
535
556
  handlebars.registerHelper('_dateDiff',
536
557
  /**
558
+ * Outputs the relative time difference between two given dates in the requested unit.
537
559
  *
538
560
  * @param dateInputA
539
561
  * @param dateInputB
@@ -629,4 +651,4 @@
629
651
  }
630
652
  },
631
653
  }
632
- });
654
+ });
@@ -1 +1 @@
1
- !function(e,t){if("object"==typeof exports&&"object"==typeof module){const e=require("handlebars"),r=require("i18next"),n=require("intl"),a=require("relative-time-format");module.exports=t(e,r,n,a,"TEST"===process?.env?.NODE_ENV)}else if("function"==typeof define&&define.amd)define(["Handlebars","i18next","Intl"],t);else{if("object"!=typeof e.Handlebars||"object"!=typeof e.i18next||"object"!=typeof e.Intl)return console.error("@ handlebars-i18n: One or more dependencies are missing. Check for Handlebars, i18next and Intl."),!1;e.HandlebarsI18n=t(e.Handlebars,e.i18next,e.Intl)}}(this,(function(e,t,r,n,a){"use strict";const o={DateTimeFormat:{standard:{},custom:{}},RelativeTimeFormat:{standard:{all:{unit:"hours"}},custom:{}},NumberFormat:{standard:{},custom:{}},PriceFormat:{standard:{all:{style:"currency",currency:"EUR"}},custom:{}}};let i=JSON.parse(JSON.stringify(o));const s={};function u(e,t){let r=[null].concat(t);return new(e.bind.apply(e,r))}function l(e,t,r){if("object"==typeof e&&"object"==typeof e.hash&&Object.keys(e.hash).length>0){let n=e.hash;if(void 0===n.format)return n;if(void 0!==r.custom[n.format]&&void 0!==r.custom[n.format][t])return r.custom[n.format][t]}return void 0!==r.standard[t]?r.standard[t]:void 0!==r.standard.all?r.standard.all:{}}function c(e,t,r,n){return"string"!=typeof e?(console.error("@ handlebars-i18n.configure(): Invalid argument <"+e+'> First argument must be a string with language code such as "en".'),!1):["DateTimeFormat","RelativeTimeFormat","NumberFormat","PriceFormat"].includes(t)?"object"!=typeof r?(console.error("@ handlebars-i18n.configure(): Invalid argument <"+r+"> Third argument must be an object containing the configuration parameters."),!1):(null==n||"string"==typeof n)&&""!==n&&" "!==n||(console.error("@ handlebars-i18n.configure(): Invalid argument <"+n+"> Fourth argument (optional) must be a string naming your custom format configuration."),!1):(console.error("@ handlebars-i18n.configure(): Invalid argument <"+t+'>. Second argument must be a string with the options key. Use either "DateTimeFormat", "RelativeTimeFormat", "NumberFormat", or "PriceFormat".'),!1)}function m(e,t,r,n){return null!=n?(void 0===i[t].custom[n]&&(i[t].custom[n]={}),i[t].custom[n][e]=r):i[t].standard[e]=r,!0}function g(e){return"number"==typeof e||"string"==typeof e&&""!==e}function d(e){let t;if("number"==typeof e)t=new Date(e);else if("string"==typeof e)if("["===e.charAt(0)&&"]"===e.slice(-1)){let r=(e=e.substring(1,e.length-1).replace(/ /g,"")).split(",");t=u.bind(null,Date)(r)}else t="now"===e.toLowerCase()||"today"===e.toLowerCase()?new Date:new Date(e);else t=new Date;return t}function f(e,t){if("function"==typeof r.RelativeTimeFormat)return new r.RelativeTimeFormat(e,t);if(void 0===s[e])try{s[e]=require(`relative-time-format/locale/${e}`)}catch(e){console.error(e)}return n.addLocale(s[e]),new n(e,t)}function b(e,t){return t=t||"hour",Math.trunc(e/{second:1e3,seconds:1e3,minute:6e4,minutes:6e4,hour:36e5,hours:36e5,day:864e5,days:864e5,week:6048e5,weeks:6048e5,month:2629746e3,months:2629746e3,quarter:78894e5,quarters:78894e5,year:315576e5,years:315576e5}[t])}return{configure:function(e,t,r,n){if("string"!=typeof e&&!Array.isArray(e))return console.error("@ handlebars-i18n.configure(): Invalid argument <"+e+'> First argument must be a string with language code such as "en" or an array with language parameters.'),!1;if(Array.isArray(e)){if(e.length<1)return console.log("@ handlebars-i18n.configure(): You passed an empty array, no parameters taken."),!1;e.forEach((e=>{if(!c(e[0],e[1],e[2],e[3]))return!1;m(e[0],e[1],e[2],e[3])}))}else{if(!c(e,t,r,n))return!1;m(e,t,r,n)}return!0},reset:function(){return i=JSON.parse(JSON.stringify(o)),!0},init:function(n,a){return"object"==typeof n&&null!==n?e=n:null!=n&&console.error("@ handlebars-i18n.init(): Invalid Argument [1] given for overrideHndlbrs. Argument must be the Handlebars object. Using generic Handlebars object instead."),"object"==typeof a&&null!==a?t=a:null!=a&&console.error("@ handlebars-i18n.init(): Invalid Argument [2] given for overrideI18n. Argument must be the i18next object. Using generic i18next object on module level instead."),e.registerHelper("__",(function(r,n){return new e.SafeString(void 0!==t?t.t(r,n.hash):r)})),e.registerHelper("_locale",(function(){return t.language})),e.registerHelper("localeIs",(function(e){return t.language===e})),e.registerHelper("_date",(function(e,n){const a=d(e),o=l(n,t.language,i.DateTimeFormat);return new r.DateTimeFormat(t.language,o).format(a)})),e.registerHelper("_dateAdd",(function(e,n,a){if("number"!=typeof e&&"string"!=typeof e)throw new Error('@ handlebars-i18n: invalid first argument "dateInput" was given for _dateAdd.');if("number"!=typeof n)throw new Error('@ handlebars-i18n: invalid second argument "offset" was given for _dateAdd.');const o=d(e),s=l(a,t.language,i.DateTimeFormat);switch(s.unit=s.unit||"hour",s.unit){case"second":case"seconds":o.setSeconds(o.getSeconds()+n);break;case"minute":case"minutes":o.setMinutes(o.getMinutes()+n);break;case"hour":case"hours":o.setHours(o.getHours()+n);break;case"day":case"days":o.setDate(o.getDate()+n);break;case"week":case"weeks":o.setDate(o.getDate()+7*n);break;case"month":case"months":o.setMonth(o.getMonth()+n);break;case"quarter":case"quarters":o.setMonth(o.getMonth()+3*n);break;case"year":case"years":o.setFullYear(o.getFullYear()+n);break;default:throw new Error('@ handlebars-i18n: invalid argument "unit" was given for _dateAdd.Unit must be either "second" | "minute" | "hour" | "day" | "week" | "month" | "quarter" | "year".')}return new r.DateTimeFormat(t.language,s).format(o)})),e.registerHelper("_dateRel",(function(e,r){const n=parseInt(e),a=l(r,t.language,i.RelativeTimeFormat);return f(t.language,a).format(n,a.unit)})),e.registerHelper("_dateDiff",(function(e,r,n){let a,o=l(n,t.language,i.RelativeTimeFormat);if(!g(e))return console.error("@ handlebars-i18n: invalid first argument dateInputA was given for _dateDiff."),null;r=r||"now",a=d(e)-d(r);const s=b(a,o.unit);return f(t.language,o).format(s,o.unit)})),e.registerHelper("_num",(function(e,n){let a=l(n,t.language,i.NumberFormat);return new r.NumberFormat(t.language,a).format(e)})),e.registerHelper("_price",(function(e,n){let a=l(n,t.language,i.PriceFormat);"string"!=typeof a.style&&"string"==typeof a.currency&&(a.style="currency");return new r.NumberFormat(t.language,a).format(e)})),e},...a&&{private:{applyToConstructor:u,configLookup:l,validateArgs:c,setArgs:m,isNumOrString:g,createDateObj:d,getRelDateFormatPolyfill:f,getDateDiff:b}}}}));
1
+ !function(e,t){if("object"==typeof exports&&"object"==typeof module){const e=require("handlebars"),r=require("i18next"),n=require("intl"),a=require("relative-time-format");module.exports=t(e,r,n,a,"TEST"===process?.env?.NODE_ENV)}else if("function"==typeof define&&define.amd)define(["Handlebars","i18next","Intl"],t);else{if("object"!=typeof e.Handlebars||"object"!=typeof e.i18next||"object"!=typeof e.Intl)return console.error("@ handlebars-i18n: One or more dependencies are missing. Check for Handlebars, i18next and Intl."),!1;e.HandlebarsI18n=t(e.Handlebars,e.i18next,e.Intl)}}(this,(function(e,t,r,n,a){"use strict";const o={DateTimeFormat:{standard:{},custom:{}},RelativeTimeFormat:{standard:{all:{unit:"hours"}},custom:{}},NumberFormat:{standard:{},custom:{}},PriceFormat:{standard:{all:{style:"currency",currency:"EUR"}},custom:{}}};let i=JSON.parse(JSON.stringify(o));const s={};function u(e,t){let r=[null].concat(t);return new(e.bind.apply(e,r))}function l(e,t,r){if("object"==typeof e&&"object"==typeof e.hash&&Object.keys(e.hash).length>0){let n=e.hash;if(void 0===n.format)return n;if(void 0!==r.custom[n.format]&&void 0!==r.custom[n.format][t])return r.custom[n.format][t]}return void 0!==r.standard[t]?r.standard[t]:void 0!==r.standard.all?r.standard.all:{}}function c(e,t,r,n){return"string"!=typeof e?(console.error("@ handlebars-i18n.configure(): Invalid argument <"+e+'> First argument must be a string with language code such as "en".'),!1):["DateTimeFormat","RelativeTimeFormat","NumberFormat","PriceFormat"].includes(t)?"object"!=typeof r?(console.error("@ handlebars-i18n.configure(): Invalid argument <"+r+"> Third argument must be an object containing the configuration parameters."),!1):(null==n||"string"==typeof n)&&""!==n&&" "!==n||(console.error("@ handlebars-i18n.configure(): Invalid argument <"+n+"> Fourth argument (optional) must be a string naming your custom format configuration."),!1):(console.error("@ handlebars-i18n.configure(): Invalid argument <"+t+'>. Second argument must be a string with the options key. Use either "DateTimeFormat", "RelativeTimeFormat", "NumberFormat", or "PriceFormat".'),!1)}function m(e,t,r,n){return null!=n?(void 0===i[t].custom[n]&&(i[t].custom[n]={}),i[t].custom[n][e]=r):i[t].standard[e]=r,!0}function g(e){return"number"==typeof e||"string"==typeof e&&""!==e}function f(e){let t;if("number"==typeof e)t=new Date(e);else if("string"==typeof e)if("["===e.charAt(0)&&"]"===e.slice(-1)){let r=(e=e.substring(1,e.length-1).replace(/ /g,"")).split(",");t=u.bind(null,Date)(r)}else t="now"===e.toLowerCase()||"today"===e.toLowerCase()?new Date:new Date(e);else t=new Date;return t}function d(e,t){if("function"==typeof r.RelativeTimeFormat)return new r.RelativeTimeFormat(e,t);if(void 0===s[e])try{s[e]=require(`relative-time-format/locale/${e}`)}catch(e){console.error(e)}return n.addLocale(s[e]),new n(e,t)}function b(e,t){return t=t||"hour",Math.trunc(e/{second:1e3,seconds:1e3,minute:6e4,minutes:6e4,hour:36e5,hours:36e5,day:864e5,days:864e5,week:6048e5,weeks:6048e5,month:2629746e3,months:2629746e3,quarter:78894e5,quarters:78894e5,year:315576e5,years:315576e5}[t])}return{configure:function(e,t,r,n){if("string"!=typeof e&&!Array.isArray(e))return console.error("@ handlebars-i18n.configure(): Invalid argument <"+e+'> First argument must be a string with language code such as "en" or an array with language parameters.'),!1;if(Array.isArray(e)){if(e.length<1)return console.log("@ handlebars-i18n.configure(): You passed an empty array, no parameters taken."),!1;e.forEach((e=>{if(!c(e[0],e[1],e[2],e[3]))return!1;m(e[0],e[1],e[2],e[3])}))}else{if(!c(e,t,r,n))return!1;m(e,t,r,n)}return!0},reset:function(){return i=JSON.parse(JSON.stringify(o)),!0},init:function(n,a){return"object"==typeof n&&null!==n?e=n:null!=n&&console.error("@ handlebars-i18n.init(): Invalid Argument [1] given for overrideHndlbrs. Argument must be the Handlebars object. Using generic Handlebars object instead."),"object"==typeof a&&null!==a?t=a:null!=a&&console.error("@ handlebars-i18n.init(): Invalid Argument [2] given for overrideI18n. Argument must be the i18next object. Using generic i18next object on module level instead."),e.registerHelper("__",(function(r,n){const a=n.hash||{},o=void 0!==t?t.t(r,{...a,returnObjects:!0}):r;return"string"==typeof o?new e.SafeString(o):o})),e.registerHelper("keyExists",(function(e){return t.exists(e)})),e.registerHelper("_locale",(function(){return t.language})),e.registerHelper("localeIs",(function(e){return t.language===e})),e.registerHelper("_date",(function(e,n){const a=f(e),o=l(n,t.language,i.DateTimeFormat);return new r.DateTimeFormat(t.language,o).format(a)})),e.registerHelper("_dateAdd",(function(e,n,a){if("number"!=typeof e&&"string"!=typeof e)throw new Error('@ handlebars-i18n: invalid first argument "dateInput" was given for _dateAdd.');if("number"!=typeof n)throw new Error('@ handlebars-i18n: invalid second argument "offset" was given for _dateAdd.');const o=f(e),s=l(a,t.language,i.DateTimeFormat);switch(s.unit=s.unit||"hour",s.unit){case"second":case"seconds":o.setSeconds(o.getSeconds()+n);break;case"minute":case"minutes":o.setMinutes(o.getMinutes()+n);break;case"hour":case"hours":o.setHours(o.getHours()+n);break;case"day":case"days":o.setDate(o.getDate()+n);break;case"week":case"weeks":o.setDate(o.getDate()+7*n);break;case"month":case"months":o.setMonth(o.getMonth()+n);break;case"quarter":case"quarters":o.setMonth(o.getMonth()+3*n);break;case"year":case"years":o.setFullYear(o.getFullYear()+n);break;default:throw new Error('@ handlebars-i18n: invalid argument "unit" was given for _dateAdd.Unit must be one of "second" | "minute" | "hour" | "day" | "week" | "month" | "quarter" | "year".')}return new r.DateTimeFormat(t.language,s).format(o)})),e.registerHelper("_dateRel",(function(e,r){const n=parseInt(e),a=l(r,t.language,i.RelativeTimeFormat);return d(t.language,a).format(n,a.unit)})),e.registerHelper("_dateDiff",(function(e,r,n){let a,o=l(n,t.language,i.RelativeTimeFormat);if(!g(e))return console.error("@ handlebars-i18n: invalid first argument dateInputA was given for _dateDiff."),null;r=r||"now",a=f(e)-f(r);const s=b(a,o.unit);return d(t.language,o).format(s,o.unit)})),e.registerHelper("_num",(function(e,n){let a=l(n,t.language,i.NumberFormat);return new r.NumberFormat(t.language,a).format(e)})),e.registerHelper("_price",(function(e,n){let a=l(n,t.language,i.PriceFormat);"string"!=typeof a.style&&"string"==typeof a.currency&&(a.style="currency");return new r.NumberFormat(t.language,a).format(e)})),e},...a&&{private:{applyToConstructor:u,configLookup:l,validateArgs:c,setArgs:m,isNumOrString:g,createDateObj:f,getRelDateFormatPolyfill:d,getDateDiff:b}}}}));
@@ -53,7 +53,12 @@
53
53
  'key2': '{{what}} is good.',
54
54
  'key3WithCount': '{{count}} item',
55
55
  'key3WithCount_plural': '{{count}} items',
56
- 'key4': 'Selected Language is:'
56
+ 'key4': 'Selected Language is:',
57
+ 'fruits': ['Apple', 'Banana', 'Cherry'],
58
+ 'steps': [
59
+ { 'title': 'Step 1', 'text': 'Open the app' },
60
+ { 'title': 'Step 2', text: 'Click start' }
61
+ ]
57
62
  }
58
63
  },
59
64
  'de' : {
@@ -63,12 +68,18 @@
63
68
  'key2': '{{what}} ist gut.',
64
69
  'key3WithCount': '{{count}} Gegenstand',
65
70
  'key3WithCount_plural': '{{count}} Gegenstände',
66
- 'key4': 'Die ausgewählte Sprache ist:'
71
+ 'key4': 'Die ausgewählte Sprache ist:',
72
+ 'fruits': ['Apfel', 'Banane', 'Kirsche'],
73
+ 'steps': [
74
+ { 'title': 'Schritt 1', 'text': 'App öffnen' },
75
+ { 'title': 'Schritt 2', 'text': 'Klicke auf Start' }
76
+ ]
67
77
  }
68
78
  }
69
79
  },
70
80
  lng : 'en',
71
- compatibilityJSON: 'v2'
81
+ compatibilityJSON: 'v2', // important: defines the JSON standard for variable replacement
82
+ returnObjects: true // set to true if you want to loop over an array or objects of properties
72
83
  });
73
84
 
74
85
  // -- Handlebars' example data object
@@ -130,6 +141,16 @@
130
141
  <code>{{{{raw}}}} {{__ "key1" lng="de"}} {{{{/raw}}}}</code>
131
142
  <p>&#x2192; {{__ "key1" lng="de"}}</p>
132
143
 
144
+ <h4>Loop over Array of Translations:</h4>
145
+ <code>
146
+ {{{{raw}}}} {{#each (__ "fruits")}} {{this}} {{/each}} {{{{/raw}}}}
147
+ </code>
148
+ <p><ul>
149
+ {{#each (__ "fruits")}}
150
+ <li>{{this}}</li>
151
+ {{/each}}
152
+ </ul></p>
153
+
133
154
  <h3>Output selected language</h3>
134
155
 
135
156
  <button onclick="changeLang()">{{__ "key0"}} {{#if (localeIs "en")}}German {{else}}Englisch {{/if}}</button>
@@ -267,4 +288,4 @@
267
288
 
268
289
  </script>
269
290
  </body>
270
- </html>
291
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "handlebars-i18n",
3
- "version": "1.8.3",
3
+ "version": "1.10.0",
4
4
  "description": "handlebars-i18n adds internationalization to handlebars.js using i18next and Intl.",
5
5
  "main": "dist/handlebars-i18n.js",
6
6
  "scripts": {
@@ -45,7 +45,7 @@
45
45
  "@commitlint/cli": "17.6.1",
46
46
  "@commitlint/config-conventional": "16.2.1",
47
47
  "@types/intl": "^1.2.2",
48
- "@types/node": "^24.5.2",
48
+ "@types/node": "^24.7.0",
49
49
  "c8": "^10.1.3",
50
50
  "chai": "4.3.6",
51
51
  "coveralls-next": "^4.2.1",
package/readme.md CHANGED
@@ -1,11 +1,8 @@
1
1
  # handlebars-i18n
2
2
 
3
- `handlebars-i18n` adds the internationalization features of [i18next](https://www.i18next.com/)
3
+ *What it is about:* `handlebars-i18n` adds the translation features of [i18next](https://www.i18next.com/)
4
4
  to [handlebars.js](https://handlebarsjs.com/). It also provides **date**, **number**, and **currency formatting**
5
- via [Intl](https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Intl). Use as node module or in
6
- the web browser. Supports Typescript.
7
-
8
- Handlebars-i18n is listed amongst i18next’s [framework helpers](https://www.i18next.com/overview/supported-frameworks).
5
+ via [Intl](https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Intl). Use as node module or in the web browser. `handlebars-i18n` is listed amongst i18next’s official [framework helpers](https://www.i18next.com/overview/supported-frameworks).
9
6
 
10
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
11
8
  ![Node.js version](https://img.shields.io/badge/node-%3E%3D14-brightgreen)
@@ -17,16 +14,22 @@ Handlebars-i18n is listed amongst i18next’s [framework helpers](https://www.i1
17
14
  ![npm](https://img.shields.io/npm/dm/handlebars-i18n)
18
15
  ![GitHub stars](https://img.shields.io/github/stars/fwalzel/handlebars-i18n?style=social)
19
16
 
20
- ## License
21
17
 
22
- Copyright (c) 2020–25 Florian Walzel,
18
+ ## Key Features & Advantages
19
+
20
+ - handlebars-i18n comes lightweight, well tested, and with detailed [examples](#detailed-examples)
21
+ - allows granular custom [presets](#generic-language-format-settings) per language
22
+ - supports Typescript
23
+ - has an optional [CLI](#additional-cli-helper-for-handlebars-i18n-available) for automatic Translations via DeepL
24
+
23
25
 
24
- MIT License
26
+ ## Please Support
25
27
 
26
- If you use handlebars-i18n in a professional context, you could
28
+ `handlebars-i18n` is free but not free of value. If you make serious use of handlebars-i18n, I’d be delighted if you
27
29
 
28
30
  [![BuyMeACoffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-ffdd00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black)](https://www.buymeacoffee.com/fwalzel)
29
31
 
32
+
30
33
  ## Install
31
34
 
32
35
  ```sh
@@ -35,17 +38,17 @@ npm i handlebars-i18n
35
38
 
36
39
  ## Import
37
40
 
38
- Import as commonJS within node environment:
41
+ Import with ES6 syntax:
39
42
 
40
- ```javascript
41
- const HandlebarsI18n = require("handlebars-i18n");
43
+ ```typescript
44
+ import HandlebarsI18n from "handlebars-i18n";
42
45
  HandlebarsI18n.init();
43
46
  ```
44
47
 
45
- With ES6 import syntax:
48
+ As commonJS within node environment:
46
49
 
47
- ```typescript
48
- import * as HandlebarsI18n from "handlebars-i18n";
50
+ ```javascript
51
+ const HandlebarsI18n = require("handlebars-i18n");
49
52
  HandlebarsI18n.init();
50
53
  ```
51
54
 
@@ -61,12 +64,12 @@ Usage in web browser (old school):
61
64
  </script>
62
65
  ```
63
66
 
64
- Via jsDelivr CDN:
67
+ Via CDN:
65
68
  ```javascript
66
- <script src="https://cdn.jsdelivr.net/npm/handlebars-i18n@1.8.3/dist/handlebars-i18n.min.js"></script>
69
+ <script src="https://cdn.jsdelivr.net/npm/handlebars-i18n@1.10.0/dist/handlebars-i18n.min.js"></script>
67
70
  ```
68
71
 
69
- ## Quick example
72
+ ## Quick Example
70
73
 
71
74
  Initialize i18next with your language strings and default settings:
72
75
 
@@ -88,7 +91,8 @@ i18next.init({
88
91
  }
89
92
  }
90
93
  },
91
- lng: "en"
94
+ lng: "en",
95
+ compatibilityJSON: 'v2'
92
96
  });
93
97
  ```
94
98
 
@@ -100,7 +104,6 @@ let data = {
100
104
  myPrice: 1200.99,
101
105
  myDate: "2020-03-11T03:24:00"
102
106
  }
103
-
104
107
  ```
105
108
 
106
109
  Initialize handlebars-i18n:
@@ -120,66 +123,66 @@ HandlebarsI18n.configure([
120
123
 
121
124
  Finally use in template:
122
125
 
123
- ```
126
+ ```hbs
124
127
  <p> {{__ "phrase1"}} </p>
125
128
  ```
126
129
 
127
- * returns for "en" &#x2192; **What is good?**
130
+ * output: en **What is good?** | de → **Was ist gut?**
128
131
 
129
- ```
132
+ ```hbs
130
133
  <p> {{__ "phrase2" thing=myItem}} </p>
131
134
  ```
132
135
 
133
- * returns for "en" &#x2192; **handlebars-i18n is good.**
136
+ * output: en **handlebars-i18n is good.** | de → **handlebars-i18n ist gut.**
134
137
 
135
- ```
138
+ ```hbs
136
139
  <p> {{_date myDate}} </p>
137
140
  ```
138
141
 
139
- * returns for "en" &#x2192; **March 11, 2020, 4:24 AM**
142
+ * output: en **March 11, 2020 at 3:24 AM** | de → **11.3.2020, 03:24**
140
143
 
141
- ```
144
+ ```hbs
142
145
  <p> {{_price myPrice}} </p>
143
146
  ```
144
147
 
145
- * returns for "en" &#x2192; **$1,200.99**
148
+ * output: en **\$1,200.99** | de → **1.200,99 $**
146
149
 
147
- ## Detailed examples
150
+ ## Detailed Examples
148
151
 
149
152
  :point_right: See the *examples folder* in the repo for more use cases and details.
150
153
 
151
- - Open `examples/browser-example/index.html` in your Web browser to see an implementation with a simple UI.
152
- - Run `npm run example:js` in the console to get a very basic node example logged.
153
- - Run `npm run example:ts` to compile and log a typescript example.
154
+ - Open `examples/browser-example/index.html` in your web browser to see an implementation with a simple UI.
155
+ - Prompt `npm run example:js` from the root of this repo to log a very basic node example to console.
156
+ - Prompt `npm run example:ts` to compile and log a typescript example.
154
157
 
155
- ## Additional CLI Helper for Handlebars-i18n available :metal:
158
+ ## Additional CLI Helper for Handlebars-i18n available
156
159
 
157
- Handlebars-i18n has its own command line
160
+ :metal: `handlebars-i18n` has its own command line
158
161
  interface [handlebars-i18n-cli](https://www.npmjs.com/package/handlebars-i18n-cli).
159
162
 
160
163
  ```sh
161
164
  npm i handlebars-i18n-cli --save-dev
162
165
  ```
163
166
 
164
- Automatically extract translation strings from handlebars templates and generate i18next conform json files from it.
165
- Handlebars-i18n-cli also helps to keep[](https://) your translations up to date when changes are made in the templates
166
- over time.
167
+ * programmatically extract/ update translation strings from handlebars templates and generate i18next conform
168
+ JSON files from it
169
+ * automatic translation of i18next JSON via [DeepL’s](https://www.deepl.com/en/pro-api/) free API
167
170
 
168
- ## API
171
+ ## Template Functions
169
172
 
170
- ### __
173
+ ### `__`
171
174
 
172
- Returns the phrase associated with the given key for the selected language. __ will take all options
175
+ Returns the phrase associated with the given key for the selected language. `__` will take all options
173
176
  i18next’s [t-function](https://www.i18next.com/overview/api#t) would take.
174
- The primary key can be passed hard encoded in the template when written in quotes:
177
+ The key can be passed hard encoded in the template when written in quotes:
175
178
 
176
- ```
179
+ ```hbs
177
180
  {{__ "keyToTranslationPhrase"}}
178
181
  ```
179
182
 
180
183
  … or it can be referenced via a handlebars variable:
181
184
 
182
- ```
185
+ ```hbs
183
186
  {{__ keyFromHandlebarsData}}
184
187
  ```
185
188
 
@@ -187,7 +190,7 @@ The primary key can be passed hard encoded in the template when written in quote
187
190
 
188
191
  Template usage:
189
192
 
190
- ```
193
+ ```hbs
191
194
  {{__ "whatIsWhat" a="Everything" b="fine"}}
192
195
  ```
193
196
 
@@ -201,9 +204,11 @@ The i18next resource:
201
204
  }
202
205
  ```
203
206
 
207
+ * output: en → **Everything is fine.**
208
+
204
209
  **Plurals**
205
210
 
206
- ```
211
+ ```hbs
207
212
  {{__ "keyWithCount" count=8}}
208
213
  ```
209
214
 
@@ -213,194 +218,226 @@ The i18next resource:
213
218
  "keyWithCount" : "{{count}} item",
214
219
  "keyWithCount_plural" : "{{count}} items"
215
220
  }
216
- },
221
+ }, ...
217
222
  ```
218
223
 
219
- **Override globally selected language**
224
+ **Override the globally selected language**
220
225
 
221
- ```
226
+ ```hbs
222
227
  {{__ "key1" lng="de"}}
223
228
  ```
224
229
 
225
- Will output the contents for "**de**" even though other language is selected.
230
+ Will output the contents for `de` even though a different language is globally set.
231
+
232
+ **Looping over an array (or object) of translations**
233
+
234
+ ```hbs
235
+ <ul>
236
+ {{#each (__ "fruits")}}
237
+ <li>{{this}}</li>
238
+ {{/each}}
239
+ </ul>
240
+ ```
241
+ In this case the key `fruits` would contain an array of translation strings, like:
242
+
243
+ ```javascript
244
+ {
245
+ en: {
246
+ translation: {
247
+ fruits: ["Apple", "Banana", "Cherry"]
248
+ }
249
+ },
250
+ returnObjects: true
251
+ }
252
+ ```
253
+
254
+ It is recommended to set `returnObjects` actively to `true` in the `i18next.init` object if you want to loop over
255
+ an array or objects of properties.
226
256
 
227
257
  ---
228
258
 
229
- ### _locale
259
+ ### `keyExists`
230
260
 
231
- Returns the shortcode of i18next’s currently selected language such as "**en**", "**de**", "**fi**", "**ja**" … etc.
261
+ Checks if a i18next translation key exists. Returns `true` or `false`.
232
262
 
263
+ ```hbs
264
+ {{#if (keyExists "myKey")}} {{__ "myKey"}} {{/if}}
233
265
  ```
266
+
267
+ ---
268
+
269
+ ### `_locale`
270
+
271
+ Returns the shortcode of i18next’s currently selected language such as `en`, `de`, `ja` … etc.
272
+
273
+ ```hbs
234
274
  {{_locale}}
235
275
  ```
236
276
 
237
277
  ---
238
278
 
239
- ### localeIs
279
+ ### `localeIs`
240
280
 
241
- Checks a string against i18next’s currently selected language. Returns **true** or **false**.
281
+ Checks a string against i18next’s currently selected language. Returns `true` or `false`.
242
282
 
243
- ```
283
+ ```hbs
244
284
  {{#if (localeIs "en")}} ... {{/if}}
245
285
  ```
246
286
 
247
287
  ---
248
288
 
249
- ### _date
289
+ ### `_date`
250
290
 
251
291
  Outputs a formatted date according to the language specific conventions.
252
292
 
253
- ```
293
+ ```hbs
254
294
  {{_date}}
255
295
  ```
256
296
 
257
297
  If called without argument the current date is returned. Any other input date can be passed as a conventional date
258
- string, a number (timestamp in milliseconds), or a date array. _date accepts all arguments
298
+ string, a number (timestamp in milliseconds), or a date array. `_date` accepts all arguments
259
299
  Javascript’s [new Date()](https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Date)
260
300
  constructor would accept.
261
301
 
262
302
  **Date argument given as date string:**
263
303
 
264
- ```
304
+ ```hbs
265
305
  {{_date "2020-03-11T03:24:00"}}
266
306
  ```
267
307
 
268
308
  or
269
309
 
270
- ```
310
+ ```hbs
271
311
  {{_date "December 17, 1995 03:24:00"}}
272
312
  ```
273
313
 
274
314
  **Date argument given as number (milliseconds since begin of unix epoch):**
275
315
 
276
- ```
316
+ ```hbs
277
317
  {{_date 1583922952743}}
278
318
  ```
279
319
 
280
320
  **Date argument given as javascript date array** [year, monthIndex [, day [, hour [, minutes [,
281
321
  seconds [, milliseconds]]]]]]:
282
322
 
283
- ```
323
+ ```hbs
284
324
  {{_date "[2012, 11, 20, 3, 0, 0]"}}
285
325
  ```
286
326
 
287
- **Additional arguments for formatting**
327
+ **Additional arguments for formatting:**
288
328
 
289
329
  You can add multiple arguments for individual formatting.
290
330
  See [Intl DateTimeFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat)
291
331
  for your option arguments. Alternatively check this repo’s TS types
292
332
  in [handlebars-i18n.d.ts](./dist/handlebars-i18n.d.ts).
293
333
 
294
- ```
334
+ ```hbs
295
335
  {{_date 1583922952743 year="2-digit" day="2-digit" timeZone="America/Los_Angeles"}}
296
336
  ```
297
337
 
298
338
  ---
299
339
 
300
- ### _dateAdd :tada: new in 1.8
340
+ ### `_dateAdd`
301
341
 
302
342
  Adds a time offset in a given unit to a date, returns the modified date.
303
343
 
304
- ```
344
+ ```hbs
305
345
  {{_dateAdd "1996-12-17" 24 unit="hour"}}
306
346
  ```
307
347
 
308
- Will output for "en" &#x2192; **12/18/1996**
348
+ * output: en **12/18/1996**
309
349
 
310
- The first argument is a date (see function **_date** for valid date inputs). The second argument is a time amount given
350
+ The first argument is a date (see function `_date` for valid date inputs). The second argument is a time amount given
311
351
  as number. The option **unit** specifies the time amount. Possible units
312
352
  are `"second"` | `"minute"` | `"hour"` | `"day"` | `"week"` | `"month"` | `"quarter"` |`"year"` (default is `"hour"`).
313
- Further options as for function **_date** can be applied.
353
+ Further options as for function `_date` can be applied.
314
354
 
315
355
  ---
316
356
 
317
- ### _dateDiff
357
+ ### `_dateDiff`
318
358
 
319
- Outputs the relative time difference between two given dates.
359
+ Outputs the relative time difference between two given dates in the requested unit.
320
360
 
361
+ ```hbs
362
+ {{_dateDiff "2000-12-17" "2001-12-17" unit="year"}}
321
363
  ```
322
- {{_dateDiff "1996-12-17T00:00:00" "1995-12-17T00:00:00" unit="year"}}
323
- ```
324
-
325
- Will output for "en" &#x2192; **in 1 year**
364
+ * output: en → **in 1 year**
326
365
 
327
366
  The second date argument is subtracted from the first. If the difference is a positive value, a future event statement
328
- is made. A negative value refers to a past date. (If no second argument is given, the default date is the present moment).
329
- Allowed date input formats are similar to **_date**, options equal **_dateRel**. Default unit is `"hour"`.
367
+ is made. A negative value refers to a past date. (If no second argument is given, the default date is the present moment).
368
+ Allowed date input formats are similar to `_date`, options equal `_dateRel`. Default unit is `"hour"`.
330
369
 
331
370
  ---
332
371
 
333
- ### _dateRel
372
+ ### `_dateRel`
334
373
 
335
374
  Outputs a string with a relative date statement, formatted according to the language specific conventions.
336
375
 
337
- ```
376
+ ```hbs
338
377
  {{_dateRel 7 unit="hour"}}
339
378
  ```
340
379
 
341
- Will output for "en" &#x2192; **in 7 hours**
380
+ * output: en **in 7 hours**
342
381
 
343
- ```
382
+ ```hbs
344
383
  {{_dateRel -7 unit="hour"}}
345
384
  ```
346
385
 
347
- Will output for "en" &#x2192; **7 hours ago**
386
+ * output: en **7 hours ago**
348
387
 
349
388
  A positive number argument leads to a future event statement, a negative refers to a past date. Possible units
350
- are `"second"` | `"minute"` | `"hour"` | `"day"` | `"week"` | `"month"` | `"quarter"` |`"year"` (default is `"hour"`).
389
+ are `"second"` | `"minute"` | `"hour"` | `"day"` | `"week"` | `"month"` | `"quarter"` |`"year"` (default is `"hour"`).
351
390
  For a complete set of options (such as `numberingSystem` or `localeMatcher`)
352
391
  see [Intl.RelativeTimeFormat Constructor](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat/RelativeTimeFormat).
353
392
  Alternatively check this repo’s TS types in [handlebars-i18n.d.ts](./dist/handlebars-i18n.d.ts).
354
393
 
355
394
  ---
356
395
 
357
- ### _num
396
+ ### `_num`
358
397
 
359
- Outputs a formatted number according to the language specific conventions of number representation, e.g.
360
- **4,100,000.8314** for "**en**", but **4.100.000,8314** for "**de**".
398
+ Outputs a formatted number according to the language specific conventions of number representation.
361
399
 
362
- ```
363
- {{_num 4100000.8314 }}
400
+ ```hbs
401
+ {{_num 3.14159}}
364
402
  ```
365
403
 
366
- **Additional arguments for formatting**
404
+ **Additional arguments for formatting:**
367
405
 
368
406
  You can add multiple arguments for individual formatting.
369
407
  See [Intl NumberFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat)
370
408
  for your option arguments. Alternatively check this repo’s TS types
371
409
  in [handlebars-i18n.d.ts](./dist/handlebars-i18n.d.ts).
372
410
 
373
- ```
411
+ ```hbs
374
412
  {{_num 3.14159 maximumFractionDigits=2}}
375
413
  ```
376
414
 
377
- Will output **3.14** for "**en**", but **3,14** for "**de**".
415
+ * output: en → **3.14** | de **3,14**
378
416
 
379
417
  ---
380
418
 
381
- ### _price
419
+ ### `_price`
382
420
 
383
- Outputs a formatted currency string according to the language specific conventions of price representation, e.g.
384
- **€9,999.99** for "**en**", but **9.999,99 €** for "**de**".
421
+ Outputs a formatted currency string according to the language specific conventions of price representation.
385
422
 
386
- ```
423
+ ```hbs
387
424
  {{_price 9999.99}}
388
425
  ```
389
426
 
390
- **Additional arguments for formatting**
427
+ **Additional arguments for formatting:**
391
428
 
392
429
  You can add multiple arguments for individual currency formatting.
393
430
  See [Intl NumberFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat)
394
431
  for your option arguments. Alternatively check this repo’s TS types
395
432
  in [handlebars-i18n.d.ts](./dist/handlebars-i18n.d.ts).
396
433
 
397
- ```
434
+ ```hbs
398
435
  {{_price 1000 currency="JPY" minimumFractionDigits=2}}
399
436
  ```
400
437
 
401
438
  ---
402
439
 
403
- ## How to use HandlebarsI18n.configure method
440
+ ## How to use `HandlebarsI18n.configure` method
404
441
 
405
442
  ### Generic language format settings
406
443
 
@@ -411,11 +448,11 @@ all languages or only for specific languages.
411
448
  HandlebarsI18n.configure("all", "DateTimeFormat", {timeZone: "America/Los_Angeles"});
412
449
  ```
413
450
 
414
- First argument is the language shortcode or "**all**" for all languages. Second is the format option you want to
451
+ First argument is the language shortcode or `"all"` for all languages. Second is the format option you want to
415
452
  address (`DateTimeFormat`, `RelativeTimeFormat`, `NumberFormat`, or `PriceFormat`). Third argument is the options object
416
453
  with the specific settings.
417
454
 
418
- Examples for generic settings:
455
+ **Examples for generic settings:**
419
456
 
420
457
  ```javascript
421
458
  HandlebarsI18n.configure("all", "RelativeTimeFormat", {style: "long", unit: "second"});
@@ -433,9 +470,9 @@ HandlebarsI18n.configure("all", "PriceFormat", {currency: "HKD", currencyDisplay
433
470
 
434
471
  You can also define specific subsets to be used in the template, i.e. if you want the date in different formats such as:
435
472
 
436
- - **2020** (year-only)
437
- - **11.3.2020** (standard-date)
438
- - **7:24:02** (time-only)
473
+ - **2020** (`year-only`)
474
+ - **11.3.2020** (`standard-date`)
475
+ - **7:24:02** (`time-only`)
439
476
 
440
477
  To do this, define a 4th parameter with a custom name:
441
478
 
@@ -447,9 +484,9 @@ HandlebarsI18n.configure([
447
484
  ]);
448
485
  ```
449
486
 
450
- Call a subset in template with the parameter format="custom-name", like:
487
+ Call a subset in template with the parameter `format="custom-name"`, like:
451
488
 
452
- ```
489
+ ```hbs
453
490
  {{_date myDate format="year-only"}}
454
491
  ```
455
492
 
@@ -502,14 +539,14 @@ it instead of the generic Handlebars object like so:
502
539
  ```javascript
503
540
  const HandlebarsModified = require("handlebars");
504
541
  HandlebarsModified.registerHelper("foo", function () {
505
- return "what you want"
542
+ return "bar"
506
543
  });
507
544
  HandlebarsI18n.init(HandlebarsModified);
508
545
  ```
509
546
 
510
- HandlebarsI18n will have your previously defined method **foo()** by now.
547
+ HandlebarsI18n will have your previously defined method `foo` by now.
511
548
 
512
- The same can be done for a custom instance of i18next. Pass it as the second argument to the init function.
549
+ The same can be done for a custom instance of i18next. Pass it as the second argument to the `init` function.
513
550
 
514
551
  ```javascript
515
552
  const i18nextCustom = require("i18next");
@@ -523,12 +560,20 @@ HandlebarsI18n.init(null, i18nextCustom);
523
560
  npm test
524
561
  ```
525
562
 
563
+ ## License
564
+
565
+ MIT License, Copyright (c) 2020–25 Florian Walzel
566
+
567
+ ## Contributing
568
+
569
+ Contributions are welcome! Please open an issue or submit a pull request on GitHub.
570
+
526
571
  ## Merci à vous
527
572
 
528
573
  For your contribution, I would like to
529
574
  thank [@MickL](https://github.com/MickL), [@dargmuesli](https://github.com/dargmuesli), and [@DiefBell](DiefBell).
530
575
 
531
- ## Note
576
+ ## Note on alternatives
532
577
 
533
578
  There is a *different* package named [handlebars-i18next](https://www.npmjs.com/package/handlebars-i18next)
534
579
  by [@jgonggrijp](https://github.com/jgonggrijp) which might also suit your needs. Cheers!
@@ -19,13 +19,23 @@ describe('handlebars-i18n Tests', function () {
19
19
  'en': {
20
20
  translation: {
21
21
  'key1': 'What is good?',
22
- 'key2': '{{what}} is {{adverb}}.'
22
+ 'key2': '{{what}} is {{adverb}}.',
23
+ 'fruits': ["Apple", "Banana", "Cherry"],
24
+ 'steps': [
25
+ { 'title': 'Step 1', 'text': 'Open the app' },
26
+ { 'title': 'Step 2', text: 'Click start' }
27
+ ]
23
28
  }
24
29
  },
25
30
  'de': {
26
31
  translation: {
27
32
  'key1': 'Was ist gut?',
28
- 'key2': '{{what}} ist {{adverb}}.'
33
+ 'key2': '{{what}} ist {{adverb}}.',
34
+ 'fruits': ["Apfel", "Banane", "Kirsche"],
35
+ 'steps': [
36
+ { 'title': 'Schritt 1', 'text': 'App öffnen' },
37
+ { 'title': 'Schritt 2', 'text': 'Klicke auf Start' }
38
+ ]
29
39
  }
30
40
  }
31
41
  },
@@ -177,6 +187,61 @@ describe('handlebars-i18n Tests', function () {
177
187
  assert.equal("handlebarsI18next ist gut.", res.string);
178
188
  });
179
189
 
190
+ it("__ should loop over array of strings with #each", () => {
191
+ i18next.changeLanguage('en');
192
+ const template = Handlebars.compile(`
193
+ <ul>
194
+ {{#each (__ "fruits")}}
195
+ <li>{{this}}</li>
196
+ {{/each}}
197
+ </ul>
198
+ `);
199
+
200
+ const output = template({});
201
+ console.log(output);
202
+ expect(output).to.contain("Apple");
203
+ expect(output).to.contain("Banana");
204
+ expect(output).to.contain("Cherry");
205
+ });
206
+
207
+ it("__ should loop over array of objects with #each", () => {
208
+ const template = Handlebars.compile(`
209
+ <ol>
210
+ {{#each (__ "steps")}}
211
+ <li><strong>{{title}}</strong>: {{text}}</li>
212
+ {{/each}}
213
+ </ol>
214
+ `);
215
+
216
+ const output = template({});
217
+ expect(output).to.contain("<strong>Step 1</strong>: Open the app");
218
+ expect(output).to.contain("<strong>Step 2</strong>: Click start");
219
+ });
220
+
221
+
222
+ /****************************************
223
+ Tests against function keyExists
224
+ ****************************************/
225
+
226
+ it('should return true for an existing translation key', function() {
227
+ const exists = hI18n.helpers.keyExists('key1');
228
+ assert.isTrue(exists);
229
+ });
230
+
231
+ it('should return false for a non-existing translation key', function() {
232
+ const exists = hI18n.helpers.keyExists('nonExistentKey');
233
+ assert.isFalse(exists);
234
+ });
235
+
236
+ it('should return false when called with no parameters', function() {
237
+ const exists = hI18n.helpers.keyExists();
238
+ assert.isFalse(exists);
239
+ });
240
+
241
+ it('should return false when called with null or undefined', function() {
242
+ assert.isFalse(hI18n.helpers.keyExists(null));
243
+ assert.isFalse(hI18n.helpers.keyExists(undefined));
244
+ });
180
245
 
181
246
  /****************************************
182
247
  Tests against function _date
@@ -1165,4 +1230,4 @@ describe('handlebars-i18n Private helper Function Tests (in production not expor
1165
1230
  expect(result).to.deep.equal({});
1166
1231
  });
1167
1232
 
1168
- });
1233
+ });