Repository: buxlabs/boxwood
Pull request #601 (2020-04-08 20:00:57)

Data tag


Patch:
diff --git a/README.md b/README.md
index f9d2767..261bc30 100644
--- a/README.md
+++ b/README.md
@@ -361,20 +361,28 @@ You can reference the parameters that were passed to the template via the `globa
 
 ### Internationalization
 
-#### i18n tag
+#### data tag
 
 You can keep translations in every file. They're scoped so you can use same names in multiple files.
 
 ```html
 <h1><translate hello/></h1>
-<i18n yaml>
-hello:
-- 'Hej!'
-- 'Hello!'
-</i18n>
+<data yaml>
+i18n:
+  hello:
+    pl: Hej
+    en: Hello
+</data>
 ```
 
-The compiler needs to know the `languages`. The template needs to get the `language` to know which text to render.
+You also need to pass `languages` to the compiler. The template needs to get the `language` to know which text to render.
+
+```js
+// ...
+const { template } = await compile(source, { languages: ['pl', 'en'])
+const html = template({ language: 'en' }, escape)
+// ...
+```
 
 Translations can also use filters:
 
diff --git a/src/Component/curlyTagReduction.js b/src/Component/curlyTagReduction.js
index efde74a..70207ed 100644
--- a/src/Component/curlyTagReduction.js
+++ b/src/Component/curlyTagReduction.js
@@ -195,11 +195,7 @@ function optimizeText (text, variables, newVariables) {
     return token
   })
   const value = tokens.map(token => token.value).join('')
-  return optimizeWhitespace(value)
-}
-
-function optimizeWhitespace (text) {
-  return text.trim().replace(/\s\s+/, ' ')
+  return value.trim()
 }
 
 module.exports = curlyTagReduction
diff --git a/src/Component/curlyTagReduction.spec.js b/src/Component/curlyTagReduction.spec.js
index a902dca..200eb9f 100644
--- a/src/Component/curlyTagReduction.spec.js
+++ b/src/Component/curlyTagReduction.spec.js
@@ -46,7 +46,7 @@ test('curlyTagReduction: simplifies expressions in curly tags', assert => {
   assert.deepEqual(curlyTagReduction('<div class="{foo}"></div>', [{ key: 'foo', value: '  ' }]), '<div></div>')
   assert.deepEqual(curlyTagReduction('<div class="{foo}"></div>', [{ key: 'foo', value: undefined }]), '<div class=\'{undefined}\'></div>')
   assert.deepEqual(curlyTagReduction('<div class="{foo}"></div>', [{ key: 'foo', value: null }]), '<div class=\'{null}\'></div>')
-  assert.deepEqual(curlyTagReduction('<div padding="{{bottom:30}}"></div>', []), '<div padding=\'{({ bottom: 30\n})}\'></div>')
+  assert.deepEqual(curlyTagReduction('<div padding="{{bottom:30}}"></div>', []), '<div padding=\'{({\n  bottom: 30\n})}\'></div>')
 
   assert.deepEqual(curlyTagReduction('<style>.foo { color: red }</style>', []), '<style>.foo { color: red }</style>')
 })
diff --git a/src/plugins/InternationalizationPlugin.js b/src/plugins/InternationalizationPlugin.js
index 695bb2b..f13fa71 100644
--- a/src/plugins/InternationalizationPlugin.js
+++ b/src/plugins/InternationalizationPlugin.js
@@ -43,7 +43,7 @@ function merge (value, translations, languages) {
   if (value.includes('{') && value.includes('}')) return translations
   if (!translations[value]) throw new TranslationError(`There is no translation for the ${value} key`)
   languages.forEach((language, index) => {
-    if (!translations[value][index]) throw new TranslationError(`There is no translation for the ${value} key in ${language} language.`)
+    if (!translations[value][index] && !translations[value][language]) throw new TranslationError(`There is no translation for the ${value} key in ${language} language.`)
   })
   return translations
 }
@@ -63,6 +63,10 @@ function isI18nTag (tag, keys) {
   return (tag === 'script' && keys.includes('i18n')) || tag === 'i18n'
 }
 
+function isDataTag (tag) {
+  return tag === 'data'
+}
+
 function hasTranslateModifier (attribute) {
   return attribute.key.endsWith('|translate')
 }
@@ -96,6 +100,15 @@ class InternationalizationPlugin extends Plugin {
         if (this.translations[key]) { throw new TranslationError('Translation already exists') }
         this.translations[`${key}_${id}`] = data[key]
       }
+    } else if (isDataTag(tag)) {
+      const format = getTranslationsFormat(keys)
+      const leaf = fragment.children[0]
+      const data = parse(format, leaf.content)
+      const { i18n } = data
+      for (const key in i18n) {
+        if (this.translations[key]) { throw new TranslationError('Translation already exists') }
+        this.translations[`${key}_${id}`] = i18n[key]
+      }
     }
     attrs.forEach(attr => {
       if (hasTranslateModifier(attr)) {
diff --git a/src/tags/data.js b/src/tags/data.js
new file mode 100644
index 0000000..665626a
--- /dev/null
+++ b/src/tags/data.js
@@ -0,0 +1,7 @@
+'use strict'
+
+module.exports = function ({ fragment }) {
+  fragment.children.forEach(node => {
+    node.used = true
+  })
+}
diff --git a/src/tags/index.js b/src/tags/index.js
index cd0bc7b..61a0eb1 100644
--- a/src/tags/index.js
+++ b/src/tags/index.js
@@ -8,6 +8,7 @@ const unlessTag = require('./unless')
 const elseunlessTag = require('./elseunless')
 const switchTag = require('./switch')
 const caseTag = require('./case')
+const dataTag = require('./data')
 const defaultTag = require('./default')
 const doctypeTag = require('./doctype')
 const markdownTag = require('./markdown')
@@ -36,6 +37,7 @@ module.exports = {
   elseunless: elseunlessTag,
   switch: switchTag,
   case: caseTag,
+  data: dataTag,
   default: defaultTag,
   doctype: doctypeTag,
   markdown: markdownTag,
diff --git a/src/utilities/collect.js b/src/utilities/collect.js
index c2dfc41..7a49288 100644
--- a/src/utilities/collect.js
+++ b/src/utilities/collect.js
@@ -309,6 +309,8 @@ async function collect ({ source, tree, fragment, assets, variables, filters, co
       tags.script({ tree, fragment, keys, attrs, assets, variables, promises, warnings, filters, translations, languages, append, options })
     } else if (tag === 'template') {
       tags.template({ tree, fragment, options })
+    } else if (tag === 'data') {
+      tags.data({ tree, fragment })
     } else if (tag === '!doctype') {
       tags.doctype({ tree, options })
     } else if (fragment.type === 'element' && !SPECIAL_TAGS.includes(tag)) {
diff --git a/src/utilities/filters.js b/src/utilities/filters.js
index 797a436..e81a669 100644
--- a/src/utilities/filters.js
+++ b/src/utilities/filters.js
@@ -5,6 +5,7 @@ const utilities = require('pure-utilities')
 const { CompilerError } = require('./errors')
 const { getObjectMemberExpression } = require('./factory')
 const { wrap } = require('pure-utilities/string')
+const { isPlainObject } = require('pure-conditions')
 
 const aliases = {
   json: 'prettify',
@@ -39,7 +40,45 @@ function getPropertyKey (value) {
   return { type: 'Identifier', name: value }
 }
 
-function serializeProperties (translations) {
+function getTranslations (translations, key, languages) {
+  const translation = translations[key]
+  if (Array.isArray(translation)) {
+    return translation.map(text => {
+      text = wrap(text.replace(/{{1}[^}]+}{1}/g, match => `$${match}`), '`')
+      const tree = new AbstractSyntaxTree(text)
+      tree.replace({
+        leave: node => {
+          if (node.type === 'Identifier') {
+            return getObjectMemberExpression(node.name)
+          }
+        }
+      })
+      return tree.first('TemplateLiteral')
+    })
+  } else if (isPlainObject(translation)) {
+    const result = []
+    for (const key in translation) {
+      const index = languages.indexOf(key)
+      if (Object.prototype.hasOwnProperty.call(translation, key)) {
+        const value = translation[key]
+        const text = wrap(value.replace(/{{1}[^}]+}{1}/g, match => `$${match}`), '`')
+        const tree = new AbstractSyntaxTree(text)
+        tree.replace({
+          leave: node => {
+            if (node.type === 'Identifier') {
+              return getObjectMemberExpression(node.name)
+            }
+          }
+        })
+        result[index] = tree.first('TemplateLiteral')
+      }
+    }
+    return result
+  }
+  return []
+}
+
+function serializeProperties (translations, languages) {
   const properties = []
   for (const key in translations) {
     properties.push({
@@ -47,18 +86,7 @@ function serializeProperties (translations) {
       key: getPropertyKey(key),
       value: {
         type: 'ArrayExpression',
-        elements: translations[key].map(text => {
-          text = wrap(text.replace(/{{1}[^}]+}{1}/g, match => `$${match}`), '`')
-          const tree = new AbstractSyntaxTree(text)
-          tree.replace({
-            leave: node => {
-              if (node.type === 'Identifier') {
-                return getObjectMemberExpression(node.name)
-              }
-            }
-          })
-          return tree.first('TemplateLiteral')
-        })
+        elements: getTranslations(translations, key, languages)
       },
       kind: 'init'
     })
@@ -83,6 +111,7 @@ module.exports = {
   getFilterName,
   extractFilterName,
   getFilter (filter, translations, options = {}) {
+    const { languages } = options
     let name = extractFilterName(filter)
     name = getFilterName(name)
     const method = utilities.string[name] ||
@@ -102,8 +131,8 @@ module.exports = {
     const leaf = new AbstractSyntaxTree(source)
     const fn = leaf.body[0]
     if (name === 'translate') {
-      fn.body.body[0].declarations[0].init.properties = serializeProperties(translations)
-      fn.body.body[1].declarations[0].init.elements = serializeLanguages(options.languages)
+      fn.body.body[0].declarations[0].init.properties = serializeProperties(translations, languages)
+      fn.body.body[1].declarations[0].init.elements = serializeLanguages(languages)
     }
     fn.id.name = name
     return fn
diff --git a/src/utilities/translations.js b/src/utilities/translations.js
index 74b17a2..b175521 100644
--- a/src/utilities/translations.js
+++ b/src/utilities/translations.js
@@ -2,6 +2,7 @@
 
 const { getObjectMemberExpression } = require('./factory')
 const { convertText, modify } = require('./convert')
+const { isPlainObject } = require('pure-conditions')
 
 function isStaticTranslationKey (key) {
   return !isDynamicTranslationKey(key)
@@ -30,9 +31,18 @@ function getTranslateCallExpression (key, variables, content, filters, translati
     return ''
   })
   if (content) {
-    translations[key] = translations[key].map((translation) => {
-      return translation.replace(/{\s*slot\s*}/, content)
-    })
+    const translation = translations[key]
+    if (Array.isArray(translation)) {
+      translations[key] = translations[key].map((translation) => {
+        return translation.replace(/{\s*slot\s*}/, content)
+      })
+    } else if (isPlainObject(translation)) {
+      for (const key in translation) {
+        if (Object.prototype.hasOwnProperty.call(translation, key)) {
+          translation[key] = translation[key].replace(/{\s*slot\s*}/, content)
+        }
+      }
+    }
   }
   const node = getTranslationNode(key, variables, filters, translations, languages)
   const objectMemberExpression = getObjectMemberExpression('language')


Test patch:
diff --git a/test/fixtures/acceptance/data/actual.html b/test/fixtures/acceptance/data/actual.html
new file mode 100644
index 0000000..b43199c
--- /dev/null
+++ b/test/fixtures/acceptance/data/actual.html
@@ -0,0 +1,3 @@
+<import foo from="components/foo.html" />
+<import bar from="components/bar.html" />
+<foo /><bar />
diff --git a/test/fixtures/acceptance/data/components/bar.html b/test/fixtures/acceptance/data/components/bar.html
new file mode 100644
index 0000000..5a6012f
--- /dev/null
+++ b/test/fixtures/acceptance/data/components/bar.html
@@ -0,0 +1,7 @@
+<translate qux />
+<data yaml>
+i18n:
+  qux:
+    en: NE
+    pl: LP
+</data>
diff --git a/test/fixtures/acceptance/data/components/foo.html b/test/fixtures/acceptance/data/components/foo.html
new file mode 100644
index 0000000..1b676b0
--- /dev/null
+++ b/test/fixtures/acceptance/data/components/foo.html
@@ -0,0 +1,7 @@
+<translate qux />
+<data yaml>
+i18n:
+  qux:
+    en: EN
+    pl: PL
+</data>
diff --git a/test/fixtures/acceptance/data/data.json b/test/fixtures/acceptance/data/data.json
new file mode 100644
index 0000000..a6042a4
--- /dev/null
+++ b/test/fixtures/acceptance/data/data.json
@@ -0,0 +1,3 @@
+{
+  "language": "en"
+}
diff --git a/test/fixtures/acceptance/data/expected.html b/test/fixtures/acceptance/data/expected.html
new file mode 100644
index 0000000..39f09c5
--- /dev/null
+++ b/test/fixtures/acceptance/data/expected.html
@@ -0,0 +1,1 @@
+ENNE
diff --git a/test/spec/acceptance.spec.js b/test/spec/acceptance.spec.js
index ecf4670..0cd5461 100644
--- a/test/spec/acceptance.spec.js
+++ b/test/spec/acceptance.spec.js
@@ -163,6 +163,11 @@ test('acceptance: invoice', async assert => {
   assert.deepEqual(actual, expected)
 })
 
+test.only('acceptance: data', async assert => {
+  const { actual, expected } = await suite('data')
+  assert.deepEqual(actual, expected)
+})
+
 test.skip('acceptance: slot as a variable', async assert => {
   const { actual, expected } = await suite('slot-as-variable')
   assert.deepEqual(actual, expected)
diff --git a/test/spec/basic.spec.js b/test/spec/basic.spec.js
index 15da987..a186e41 100644
--- a/test/spec/basic.spec.js
+++ b/test/spec/basic.spec.js
@@ -418,27 +418,3 @@ test('basic', async assert => {
   var { template } = await compile('<img src="./placeholder.png">')
   assert.deepEqual(template({ photos: ['foo.jpg', 'bar.jpg'] }, escape), '<img src="./placeholder.png">')
 })
-
-test('removes unnecessary whitespace', async function (assert) {
-  var { template } = await compile(`
-    <template foo>
-      <div class="{ color || '' } { size || '' }"></div>
-    </template>
-    <foo/>
-  `)
-  assert.deepEqual(template({}, escape), '<div></div>')
-
-  var { template } = await compile(`
-    <template foo>
-      <div class="
-        { color || '' }
-        { size || '' }
-        { width || ''}
-      "></div>
-    </template>
-    <foo color="{color}" size="{size}"/>
-  `)
-  assert.deepEqual(template({}, escape), '<div class=" "></div>')
-  assert.deepEqual(template({ color: 'white' }, escape), '<div class="white "></div>')
-  assert.deepEqual(template({ color: 'white', size: 'big' }, escape), '<div class="white big"></div>')
-})
diff --git a/test/spec/features/i18n.spec.js b/test/spec/features/i18n.spec.js
index 5ea6558..b8bea29 100644
--- a/test/spec/features/i18n.spec.js
+++ b/test/spec/features/i18n.spec.js
@@ -515,3 +515,69 @@ test('i18n: translations with slots', async assert => {
   assert.deepEqual(template({ language: 'pl' }, escape), 'qux <a href=\'/bar\'>baz</a>')
   assert.deepEqual(template({ language: 'en' }, escape), '<a href=\'/bar\'>baz</a> qux')
 })
+
+test('i18n: translations with named languages', async assert => {
+  const { template } = await compile(`
+    <translate foo/>
+    <i18n yaml>
+      foo:
+        en: Hello
+        pl: Siema
+    </i18n>
+  `, {
+    languages: ['en', 'pl']
+  })
+  assert.deepEqual(template({ language: 'en' }, escape), 'Hello')
+  assert.deepEqual(template({ language: 'pl' }, escape), 'Siema')
+})
+
+
+test('i18n: translations with named languages in random order', async assert => {
+  const { template } = await compile(`
+    <translate foo/><translate bar/>
+    <i18n yaml>
+      foo:
+        en: Hello
+        pl: Siema
+      bar:
+        pl: Hej
+        en: Bye
+    </i18n>
+  `, {
+    languages: ['en', 'pl']
+  })
+  assert.deepEqual(template({ language: 'en' }, escape), 'HelloBye')
+  assert.deepEqual(template({ language: 'pl' }, escape), 'SiemaHej')
+})
+
+test('i18n: keeping translations in data tag', async assert => {
+  const { template } = await compile(`
+    <translate foo/>
+    <data yaml>
+      i18n:
+        foo:
+          en: Hello
+          pl: Siema
+    </data>
+  `, {
+    languages: ['en', 'pl']
+  })
+  assert.deepEqual(template({ language: 'en' }, escape), 'Hello')
+  assert.deepEqual(template({ language: 'pl' }, escape), 'Siema')
+})
+
+test('i18n: keeping translations in data tag with keys in random order', async assert => {
+  const { template } = await compile(`
+    <translate foo/>
+    <data yaml>
+      i18n:
+        foo:
+          pl: Siema
+          en: Hello
+    </data>
+  `, {
+    languages: ['en', 'pl']
+  })
+  assert.deepEqual(template({ language: 'en' }, escape), 'Hello')
+  assert.deepEqual(template({ language: 'pl' }, escape), 'Siema')
+})