Browse Source

I18n (#2385)

I18n
pull/2407/head
Artur Arseniev 7 years ago
committed by GitHub
parent
commit
8f1d660a37
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .gitignore
  2. 961
      dist/grapes.js
  3. 6
      dist/grapes.min.js
  4. 2
      dist/grapes.min.js.map
  5. 2
      docs/.vuepress/config.js
  6. BIN
      docs/.vuepress/public/margin-strings.jpg
  7. BIN
      docs/.vuepress/public/sm-empty-state.jpg
  8. 1
      docs/api.js
  9. 13
      docs/api/device_manager.md
  10. 24
      docs/api/editor.md
  11. 157
      docs/api/i18n.md
  12. 466
      docs/modules/Components-TOREMOVE.md
  13. 236
      docs/modules/I18n.md
  14. 2
      docs/modules/Plugins.md
  15. 2086
      package-lock.json
  16. 29
      package.json
  17. 28
      scripts/build-locale.js
  18. 12
      src/asset_manager/config/config.js
  19. 10
      src/asset_manager/view/AssetsView.js
  20. 10
      src/asset_manager/view/FileUploader.js
  21. 3
      src/block_manager/view/BlockView.js
  22. 15
      src/block_manager/view/CategoryView.js
  23. 2
      src/commands/view/OpenAssets.js
  24. 2
      src/commands/view/OpenStyleManager.js
  25. 7
      src/commands/view/OpenTraitManager.js
  26. 4
      src/device_manager/config/config.js
  27. 29
      src/device_manager/index.js
  28. 24
      src/device_manager/view/DevicesView.js
  29. 13
      src/dom_components/model/Component.js
  30. 7
      src/editor/config/config.js
  31. 24
      src/editor/index.js
  32. 5
      src/editor/model/Editor.js
  33. 20
      src/i18n/config.js
  34. 247
      src/i18n/index.js
  35. 117
      src/i18n/locale/en.js
  36. 103
      src/i18n/locale/it.js
  37. 141
      src/i18n/locale/tr.js
  38. 5
      src/keymaps/messages.js
  39. 16
      src/panels/config/config.js
  40. 7
      src/panels/view/ButtonView.js
  41. 14
      src/selector_manager/config/config.js
  42. 6
      src/selector_manager/view/ClassTagView.js
  43. 40
      src/selector_manager/view/ClassTagsView.js
  44. 6
      src/style_manager/config/config.js
  45. 4
      src/style_manager/model/PropertyInteger.js
  46. 5
      src/style_manager/view/LayerView.js
  47. 7
      src/style_manager/view/PropertyFileView.js
  48. 10
      src/style_manager/view/PropertyView.js
  49. 17
      src/style_manager/view/SectorView.js
  50. 16
      src/trait_manager/config/config.js
  51. 12
      src/trait_manager/model/TraitFactory.js
  52. 8
      src/trait_manager/view/TraitSelectView.js
  53. 12
      src/trait_manager/view/TraitView.js
  54. 10
      test/specs/asset_manager/view/FileUploader.js
  55. 204
      test/specs/i18n/index.js

1
.gitignore

@ -14,6 +14,7 @@ images/
private/
vendor/
coverage/
/locale/
node_modules/
bower_components/
grapesjs-*.tgz

961
dist/grapes.js

File diff suppressed because it is too large

6
dist/grapes.min.js

File diff suppressed because one or more lines are too long

2
dist/grapes.min.js.map

File diff suppressed because one or more lines are too long

2
docs/.vuepress/config.js

@ -58,6 +58,7 @@ module.exports = {
'/api/': [
'',
['/api/editor', 'Editor'],
['/api/i18n', 'I18n'],
['/api/canvas', 'Canvas'],
['/api/assets', 'Asset Manager'],
['/api/block_manager', 'Block Manager'],
@ -88,6 +89,7 @@ module.exports = {
['/modules/Commands', 'Commands'],
['/modules/Components', 'Components'],
['/modules/Components-js', 'Components & JS'],
['/modules/I18n', 'I18n'],
['/modules/Traits', 'Traits'],
['/modules/Style-manager', 'Style Manager'],
['/modules/Storage', 'Storage Manager'],

BIN
docs/.vuepress/public/margin-strings.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
docs/.vuepress/public/sm-empty-state.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

1
docs/api.js

@ -22,6 +22,7 @@ const cmds = [
['keymaps/index.js', 'keymaps.md'],
['undo_manager/index.js', 'undo_manager.md'],
['canvas/index.js', 'canvas.md'],
['i18n/index.js', 'i18n.md'],
].map(entry =>
`${binRoot}documentation build ${srcRoot}/${entry[0]} -o ${docRoot}/api/${entry[1]} -f md --shallow --markdown-toc false`)
.join(' && ');

13
docs/api/device_manager.md

@ -28,16 +28,19 @@ Add new device to the collection. URLs are supposed to be unique
### Parameters
- `name` **[string][5]** Device name
- `width` **[string][5]** Width of the device
- `opts` **[Object][6]** Custom options
- `id` **[String][5]** Device id
- `width` **[String][5]** Width of the device
- `opts` **[Object][6]?** Custom options (optional, default `{}`)
### Examples
```javascript
deviceManager.add('Tablet', '900px');
deviceManager.add('Tablet2', '900px', {
deviceManager.add('tablet', '900px');
deviceManager.add('tablet2', '900px', {
height: '300px',
// At first, GrapesJS tries to localize the name by device id.
// In case is not found, the `name` property is used (or `id` if name is missing)
name: 'Tablet 2',
widthMedia: '810px', // the width that will be used for the CSS media
});
```

24
docs/api/editor.md

@ -538,6 +538,30 @@ editor.log('Something done!', { ns: 'from-plugin-x', level: 'info' });
Returns **this**
## t
Translate label
### Parameters
- `args` **...any**
- `key` **[String][2]** Label to translate
- `opts` **[Object][3]?** Options for the translation
- `opts.params` **[Object][3]?** Params for the translation
- `opts.noWarn` **[Boolean][4]?** Avoid warnings in case of missing resources
### Examples
```javascript
editor.t('msg');
// use params
editor.t('msg2', { params: { test: 'hello' } });
// custom local
editor.t('msg2', { params: { test: 'hello' }, l: 'it' });
```
Returns **[String][2]**
## on
Attach event

157
docs/api/i18n.md

@ -0,0 +1,157 @@
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
## I18n
You can customize the initial state of the module from the editor initialization, by passing the following [Configuration Object][1]
```js
const editor = grapesjs.init({
i18n: {
locale: 'en',
localeFallback: 'en',
messages: {
it: { hello: 'Ciao', ... },
...
}
}
})
```
Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance
```js
const i18n = editor.I18n;
```
### Events
- `i18n:add` - New set of messages is added
- `i18n:update` - The set of messages is updated
- `i18n:locale` - Locale changed
## getConfig
Get module configurations
Returns **[Object][2]** Configuration object
## setLocale
Update current locale
### Parameters
- `locale` **[String][3]** Locale value
### Examples
```javascript
i18n.setLocale('it');
```
Returns **this**
## getLocale
Get current locale
Returns **[String][3]** Current locale value
## getMessages
Get all messages
### Parameters
- `lang` **[String][3]?** Specify the language of messages to return
- `opts` **[Object][2]?** Options (optional, default `{}`)
- `opts.debug` **[Boolean][4]?** Show warnings in case of missing language
### Examples
```javascript
i18n.getMessages();
// -> { en: { hello: '...' }, ... }
i18n.getMessages('en');
// -> { hello: '...' }
```
Returns **[Object][2]**
## setMessages
Set new set of messages
### Parameters
- `msg` **[Object][2]** Set of messages
### Examples
```javascript
i18n.getMessages();
// -> { en: { msg1: 'Msg 1', msg2: 'Msg 2', } }
i18n.setMessages({ en: { msg2: 'Msg 2 up', msg3: 'Msg 3', } });
// Set replaced
i18n.getMessages();
// -> { en: { msg2: 'Msg 2 up', msg3: 'Msg 3', } }
```
Returns **this**
## addMessages
Update messages
### Parameters
- `msg` **[Object][2]** Set of messages to add
### Examples
```javascript
i18n.getMessages();
// -> { en: { msg1: 'Msg 1', msg2: 'Msg 2', } }
i18n.addMessages({ en: { msg2: 'Msg 2 up', msg3: 'Msg 3', } });
// Set updated
i18n.getMessages();
// -> { en: { msg1: 'Msg 1', msg2: 'Msg 2 up', msg3: 'Msg 3', } }
```
Returns **this**
## t
Translate the locale message
### Parameters
- `key` **[String][3]** Label to translate
- `opts` **[Object][2]?** Options for the translation (optional, default `{}`)
- `opts.params` **[Object][2]?** Params for the translation
- `opts.debug` **[Boolean][4]?** Show warnings in case of missing resources
### Examples
```javascript
obj.setMessages({
en: { msg: 'Msg', msg2: 'Msg {test}'},
it: { msg2: 'Msg {test} it'},
});
obj.t('msg');
// -> outputs `Msg`
obj.t('msg2', { params: { test: 'hello' } }); // use params
// -> outputs `Msg hello`
obj.t('msg2', { l: 'it', params: { test: 'hello' } }); // custom local
// -> outputs `Msg hello it`
```
Returns **[String][3]**
[1]: https://github.com/artf/grapesjs/blob/master/src/i18n/config.js
[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean

466
docs/modules/Components-TOREMOVE.md

@ -1,466 +0,0 @@
---
title: Component Manager
---
# Component Manager
The Component is the base element for template composition. It is atomic, so elements like images, text boxes, maps, etc. fit the definition of a Component. The concept of the component was made to allow the developer to bind different behaviors to different elements. Like for example, opening the Asset Manager on double click of the image.
[[toc]]
## Built-in components
* default (Basic)
* wrapper
* text
* textnode
* svg
* script
* image
* video
* label
* link
* map
* table
* row (for the table)
* cell (for the table)
## How Components work?
When we pass an HTML string to the editor like this:
```html
<div>
<img src="https://path/image" />
<span title="foo">bar</span>
</div>
```
For each DOM element (`div`, `img`, `span`, etc.) the editor will create and store an object representation. Every future change to the template will be made on top of this structure, which will then reflect on the canvas. So each object, usually called *Model* (or state/store), will be the source of truth for the template, but what exactly does that mean?
In more practical example, once the template is rendered on the canvas, if you try to remove one of its elements (eg. by using the browser inspector) and ask the editor to print the HTML (using `editor.getHtml()`) you'll see that the element will still be there. This is because the editor relies on Models and not on the DOM elements inside the canvas. This approach allows us to be extremely flexible on how we generate the final code (from the *Model*) and how to render it inside the canvas (from the *View*).
# Manage Components
## Component recognition
But now, how does the editor recognize which Component to bind to the `img` element and what to do with the `span` one?
Each Component inherits, from the base one, a particular static method
```js
/**
* @param {HTMLElement} el
* @return {Object}
*/
isComponent: function(el) {
...
}
```
This method gives us the possibility to recognize and bind component types to each HTMLElement (div, img, iframe, etc.). Each **HTML string/element** introduced inside the canvas will be processed by `isComponent` of all available types and if it matches, the object represented the type should be returned. The method `isComponent` **is skipped** if you add the component object (`{ type: 'my-custom-type', tagName: 'div', attribute: {...}, ...}`) or declare the type explicitly on the element (`<div data-gjs-type="my-custom-type">...</div>`)
For example, with the image component this method looks like:
```js
// Image component
isComponent: function(el) {
if(el.tagName == 'IMG')
return {type: 'image'};
}
```
Let's try with something that might look a little bit tricky. What about a Google Map?!? Google Maps are generally embedded as `iframe`s, but the template can be composed by a lot of different `iframe`s. How can I tell the editor that a particular iframe is actually a Google's Map? Well, you'll have to figure out the right pattern, you have the `HTMLElement` so you can make all the checks you want. In this particular case this pattern is used:
```js
// Map component
isComponent: function(el) {
if(el.tagName == 'IFRAME' && /maps\.google\.com/.test(el.src)) {
return {type: 'map', src: el.src};
}
},
```
In addition to `tagName` check, we also used the `src` property, but you can actually override it with your own logic by extending the built-in component.
## Define new Component
Let's see an example with another HTML element that is not handled by default Component types. What about `input` elements?
With the default GrapesJS configuration `input`s are treated like any other element; you can move it around, style it, etc. However, we'd like to handle this type of element more specifically. In this case, we have to create a new Component type.
Let's define few specs for our new *Input* type:
* Can be dropped only inside `form` elements
* Can't drop other elements inside it
* Can change the type of the input (text, password, email, etc.)
* Can make it required for the form
To define a new Component type you need to choose from which built-in Component inherit its properties, in our case we just gonna choose the default one. Let's see a complete example of the new type definition
```js
// Get DomComponents module
var comps = editor.DomComponents;
// Get the model and the view from the default Component type
var defaultType = comps.getType('default');
var defaultModel = defaultType.model;
var defaultView = defaultType.view;
var inputTypes = [
{value: 'text', name: 'Text'},
{value: 'email', name: 'Email'},
{value: 'password', name: 'Password'},
{value: 'number', name: 'Number'},
];
// The `input` will be the Component type ID
comps.addType('input', {
// Define the Model
model: defaultModel.extend({
// Extend default properties
defaults: Object.assign({}, defaultModel.prototype.defaults, {
// Can be dropped only inside `form` elements
draggable: 'form, form *',
// Can't drop other elements inside it
droppable: false,
// Traits (Settings)
traits: ['name', 'placeholder', {
// Change the type of the input (text, password, email, etc.)
type: 'select',
label: 'Type',
name: 'type',
options: inputTypes,
},{
// Can make it required for the form
type: 'checkbox',
label: 'Required',
name: 'required',
}],
}),
},
// The second argument of .extend are static methods and we'll put inside our
// isComponent() method. As you're putting a new Component type on top of the stack,
// not declaring isComponent() might probably break stuff, especially if you extend
// the default one.
{
isComponent: function(el) {
if(el.tagName == 'INPUT'){
return {type: 'input'};
}
},
}),
// Define the View
view: defaultType.view,
});
```
The code above is pretty much self-explanatory and as you see a lot of work is basically done on top of the Model properties.
The *View* is just extending the default one, so to cover also this part let's add some random behavior.
```js
comps.addType('input', {
model: {...},
view: defaultType.view.extend({
// Bind events
events: {
// If you want to bind the event to children elements
// 'click .someChildrenClass': 'methodName',
click: 'handleClick',
dblclick: function(){
alert('Hi!');
}
},
// It doesn't make too much sense this method inside the component
// but it's ok as an example
randomHex: function() {
return '#' + Math.floor(Math.random()*16777216).toString(16);
},
handleClick: function(e) {
this.model.set('style', {color: this.randomHex()}); // <- Affects the final HTML code
this.el.style.backgroundColor = this.randomHex(); // <- Doesn't affect the final HTML code
// Tip: updating the model will reflect the changes to the view, so, in this case,
// if you put the model change after the DOM one this will override the backgroundColor
// change made before
},
// The render() should return 'this'
render: function () {
// Extend the original render method
defaultType.view.prototype.render.apply(this, arguments);
this.el.placeholder = 'Text here'; // <- Doesn't affect the final HTML code
return this;
},
}),
});
```
From the example above you can notice few interesting things: how to bind events, how to update directly the DOM and how to update the model. The difference between updating the DOM and the model is that the HTML code (the one you get with `editor.getHtml()`) is generated from the *Model* so updating directly the DOM will not affect it, it's just the change for the canvas.
## Update Component type
Here an example of how easily you can update/override the component
```js
var originalMap = comps.getType('map');
comps.addType('map', {
model: originalMap.model.extend({
// Override how the component is rendered to HTML
toHTML: function() {
return '<div>My Custom Map</div>';
},
}, {
isComponent: function(el) {
// ... new logic for isComponent
},
}),
view: originalMap.view
});
```
## Improvement over addType <Badge text="0.14.50+"/>
Now, with the [0.14.50](https://github.com/artf/grapesjs/releases/tag/v0.14.50) release, defining new components or extending them is a bit easier (without breaking the old process)
* If you don't specify the type to extend, the `default` one will be used. In that case, you just
use objects for `model` and `view`
* The `defaults` property, in the `model`, will be merged automatically with defaults of the parent component
* If you use an object in `model` you can specify `isComponent` outside or omit it. In this case,
the `isComponent` is not mandatory but without it means the parser won't be able to identify the component
if not explicitly declared (eg. `<div data-gjs-type="new-component">...</div>`)
**Before**
```js
const defaultType = comps.getType('default');
comps.addType('new-component', {
model: defaultType.model.extend({
defaults: {
...defaultType.model.prototype.defaults,
someprop: 'somevalue',
},
...
}, {
// Even if it returns false, declaring isComponent is mandatory
isComponent(el) {
return false;
},
}),
view: defaultType.view.extend({ ... });
});
```
**After**
```js
comps.addType('new-component', {
// We can even omit isComponent here, as `false` return will be the default behavior
isComponent: el => false,
model: {
defaults: {
someprop: 'somevalue',
},
...
},
view: { ... };
});
```
* If you need to extend some component, you can use `extend` and `extendView` property.
* You can now omit `view` property if you don't need to change it
**Before**
```js
const originalMap = comps.getType('map');
comps.addType('map', {
model: originalMap.model.extend({
...
}, {
isComponent(el) {
// ... usually, you'd reuse the same logic
},
}),
// Even if I do nothing in view, I have to specify it
view: originalMap.view
});
```
**After**
The `map` type is already defined, so it will be used as a base for the model and view.
We can skip `isComponent` if the recognition logic is the same of the extended component.
```js
comps.addType('map', {
model: { ... },
});
```
Extend the `model` and `view` with some other, already defined, components.
```js
comps.addType('map', {
extend: 'other-defined-component',
model: { ... }, // Will extend 'other-defined-component'
view: { ... }, // Will extend 'other-defined-component'
// `isComponent` will be taken from `map`
});
```
```js
comps.addType('map', {
extend: 'other-defined-component',
model: { ... }, // Will extend 'other-defined-component'
extendView: 'other-defined-component-2',
view: { ... }, // Will extend 'other-defined-component-2'
// `isComponent` will be taken from `map`
});
```
### Extend parent functions <Badge text="0.14.60+"/>
When you need to reuse functions, of the parent you're extending, you can avoid writing something like this in any function:
```js
domc.getType('parent-type').model.prototype.init.apply(this, arguments);
```
by using `extendFn` and `extendFnView` arrays:
```js
domc.addType('new-type', {
extend: 'parent-type',
extendFn: ['init'], // array of model functions to extend
model: {
init() {
// do something;
},
}
});
```
The same would be for the view by using `extendFnView`
## Lifecycle Hooks
Each component triggers different lifecycle hooks, which allows you to add custom actions at their specific stages.
We can distinguish 2 different types of hooks: **global** and **local**.
You define **local** hooks when you create/extend a component type (usually via some `model`/`view` method) and the reason is to react to an event of that
particular component type. Instead, the **global** one, will be called indistinctly on any component (you listen to them via `editor.on`) and you can make
use of them for a more generic use case or also listen to them inside other components.
Let's see below the flow of all hooks:
* **Local hook**: `model.init()` method, executed once the model of the component is initiliazed
* **Global hook**: `component:create` event, called right after `model.init()`. The model is passed as an argument to the callback function.
Es. `editor.on('component:create', model => console.log('created', model))`
* **Local hook**: `view.init()` method, executed once the view of the component is initiliazed
* **Local hook**: `view.onRender()` method, executed once the component is rendered on the canvas
* **Global hook**: `component:mount` event, called right after `view.onRender()`. The model is passed as an argument to the callback function.
* **Local hook**: `model.updated()` method, executes when some property of the model is updated.
* **Global hook**: `component:update` event, called after `model.updated()`. The model is passed as an argument to the callback function.
You can also listen to specific property change via `component:update:{propertyName}`
* **Local hook**: `model.removed()` method, executed when the component is removed.
* **Global hook**: `component:remove` event, called after `model.removed()`. The model is passed as an argument to the callback function.
Below you can find an example usage of all the hooks
```js
editor.DomComponents.addType('test-component', {
model: {
defaults: {
testprop: 1,
},
init() {
console.log('Local hook: model.init');
this.listenTo(this, 'change:testprop', this.handlePropChange);
// Here we can listen global hooks with editor.on('...')
},
updated(property, value, prevValue) {
console.log('Local hook: model.updated',
'property', property, 'value', value, 'prevValue', prevValue);
},
removed() {
console.log('Local hook: model.removed');
},
handlePropChange() {
console.log('The value of testprop', this.get('testprop'));
}
},
view: {
init() {
console.log('Local hook: view.init');
},
onRender() {
console.log('Local hook: view.onRender');
},
},
});
// A block for the custom component
editor.BlockManager.add('test-component', {
label: 'Test Component',
content: '<div data-gjs-type="test-component">Test Component</div>',
});
// Global hooks
editor.on(`component:create`, model => console.log('Global hook: component:create', model.get('type')));
editor.on(`component:mount`, model => console.log('Global hook: component:mount', model.get('type')));
editor.on(`component:update:testprop`, model => console.log('Global hook: component:update:testprop', model.get('type')));
editor.on(`component:remove`, model => console.log('Global hook: component:remove', model.get('type')));
```
## Components & JS
If you want to know how to create Components with javascript attached (eg. counters, galleries, slideshows, etc.) check the dedicated page
[Components & JS](Components-js.html)
## Hints
```html
<div id="gjs">
...
<cutom-element></cutom-element>
...
</div>
<script>
var editor = grapesjs.init({
container : '#gjs',
fromElement: true,
});
editor.DomComponents.addType('cutom-element-type', {...});
</script>
```
In the example above the editor will not get the new type from the HTML because the content is already parsed and appended, so it'll get it only with new components (eg. from Blocks)
Solution 1: turn off `autorender`
```html
<script>
var editor = grapesjs.init({
autorender: 0,
container : '#gjs',
fromElement: true,
});
editor.DomComponents.addType('cutom-element-type', {...});
// after all new types
editor.render();
</script>
```
Solution 2: put all the stuff inside a plugin ([Creating plugins](Plugins.html))

236
docs/modules/I18n.md

@ -0,0 +1,236 @@
---
title: I18n (Internalization)
---
# Internalization
The **I18n** module allows the internalization and updates of strings in the editor UI
::: warning
This guide is referring to GrapesJS v0.15.9 or higher
The module was added recently so we're open to receive support in [translating strings in other languages](#adding-new-language). Your help will be much appreciated!
:::
[[toc]]
## Configuration
By default, the editor includes only the English language, if you need other languages you have to import them manually.
**Note**: The language code is defined in the [ISO 639-1] standard.
```js
import grapesjs from 'grapesjs';
import it from 'grapesjs/locale/it';
import tr from 'grapesjs/locale/tr';
const editor = grapesjs.init({
...
i18n: {
// locale: 'en', // default locale
// detectLocale: true, // by default, the editor will detect the language
// localeFallback: 'en', // default fallback
messages: { it, tr },
}
});
```
Now the editor will be translated in Italian for those browsers which default language is Italian (by default `detectLocale` option is enabled)
## Update strings
If you need to change some default language strings you can easily update them by using [I18n API](/api/i18n.html).
To find the correth path of the string you can check the [`en` locale file] and follow its inner path inside the locale object.
Let's say we want to update the default message of the empty state in Style Manager when no elements are selected.
<img :src="$withBase('/sm-empty-state.jpg')">
From the `en` locale file you can see it by following the path below
```js
{
...
styleManager: {
empty: 'Select an element before using Style Manager',
...
},
...
}
```
So now to update it you'll do this
```js
editor.I18n.addMessages({
en: { // indicate the locale to update
styleManager: {
empty: 'New empty state message',
}
}
});
```
Even if the UI shows correctly the updated message, we highly suggest to do all the API calls wrapped in a [plugin](Plugins.html)
```js
const myPlugin = editor => {
editor.I18n.addMessages({ ... });
// ...
}
grapesjs.init({
// ...
plugins: [myPlugin],
});
```
### Generated strings
Not all the strings are indicated in the `en` local file as some of them can be generated from `id`s, `name`s, etc.
If you look back at the `styleManager` path from the `en` file you'll notice the empty `properties` key
```js
...
styleManager: {
...
properties: {
// float: 'Float',
},
...
},
...
```
This object is used to translate property names inside StyleManager, so if you need, for instance, to change the auto-generated names for the `margin` properties
<img :src="$withBase('/margin-strings.jpg')">
you'd this
```js
editor.I18n.addMessages({
en: {
styleManager: {
properties: {
// The key is the property name (or id)
'margin-top': 'Top',
'margin-right': 'Right',
'margin-left': 'Left',
'margin-bottom': 'Bottom',
},
}
}
});
```
<!--
### Updates post rendering
If you try to update strings, by using API, once the UI is rendered you'll see no changes.
...
We need to find the way to update the UI
-->
## Adding new language
If you want to support GrapesJS by adding a new language to our repository all you need to do is to follow steps below:
1. First of all, be sure to check the language file in [`src/i18n/locale`](https://github.com/artf/grapesjs/blob/master/src/i18n/locale) doesn't exist already
1. [Open a new issue](https://github.com/artf/grapesjs/issues/new?title=XX%20Language%20support) to avoid overlap with other contributos. To be sure, check also no one else has opened already an issue for the same language
1. Start a new branch from `dev`
1. Copy (in the same folder) and rename the [`en` locale file] to the name of your language of choice (be sure to be compliant to [ISO 639-1])
1. Now you can start translating strings
1. By following comments you'll probably notice that some keys are not indicated (eg. `styleManager.properties`), for the reference you can check other locale files
1. Once you've done, you can create a new Pull Request on GitHub from your branch to `dev` by making also a reference to your issue in order to close it automatically once it's merged (your PR message should contain `Closes #1234` where 1234 is the issue ID)
## Plugin development
::: warning
This section is dedicated **only** to plugin developers and can also be skipped in case you use [grapesjs-cli](https://github.com/artf/grapesjs-cli) to init your plugin project
:::
If you're developing a plugin for GrapesJS and you need to support some string localization or simply change the default one, we recommend the following structure.
```
plugin-dir
- package.json
- README.md
- ...
- src
- index.js
- locale // create the locale foldar in your src
- en.js // All default strings should be placed here
```
For your plugin specific strings, place them under the plugin name key
```js
// src/locale/en.js
export default {
'grapesjs-plugin-name': {
yourKey: 'Your value',
}
}
```
In your `index.js` use the `en.js` file and add `i18n` option to allow import of other local files
```js
// src/index.js
import en from 'locale/en';
export default (editor, opts = {}) => {
const options = {
i18n: {},
// ...
...opts,
};
// ...
editor.I18n.addMessages({
en,
...options.i18n,
});
}
```
The next step would be to compile your locale files into `<rootDir>/locale` directory to make them easily accessible by your users. This folder could be ignored in your git repository be should be deployd to the npm registry
::: warning
Remember that you can skip all these long steps and init your project with [grapesjs-cli](https://github.com/artf/grapesjs-cli). This will create all the necessary folders/files/commands for you (during `init` command this step is flagged `true` by default and we recommend to keep it even in case the i18n is not required in your project)
:::
At the end, your plugin users will be able to import other locale files (if they exist) in this way
```js
import grapesjs from 'grapesjs';
// Import from your plugin
import yourPlugin from 'grapesjs-your-plugin';
import ch from 'grapesjs-your-plugin/locale/ch';
import fr from 'grapesjs-your-plugin/locale/fr';
const editor = grapesjs.init({
...
plugins: [ yourPlugin ],
pluginsOpts: {
[yourPlugin]: {
i18n: { ch, fr }
}
}
});
```
[ISO 639-1]: <https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes>
[`en` locale file]: <https://github.com/artf/grapesjs/blob/master/src/i18n/locale/en.js>

2
docs/modules/Plugins.md

@ -142,7 +142,7 @@ In this example, the plugin name is `my-plugin-name` and can't be used by other
## Boilerplate
If you want to start with a production-ready boilerplate for a named plugin, you might want to try [grapesjs-plugin-boilerplate](https://github.com/artf/grapesjs-plugin-boilerplate) which you can clone and start developing a named plugin immediately. For more informations check the repository
For fast plugin development, we highly recommend using [grapesjs-cli](https://github.com/artf/grapesjs-cli) which helps to avoid the hassle of setting up all the dependencies and configurations for development and building (no need to touch Webpack o Babel configurations). For more information check the repository
## Popular Plugins

2086
package-lock.json

File diff suppressed because it is too large

29
package.json

@ -16,11 +16,11 @@
"url": "https://github.com/artf/grapesjs.git"
},
"dependencies": {
"@babel/runtime": "^7.6.0",
"@babel/runtime": "^7.7.1",
"backbone": "1.3.3",
"backbone-undo": "^0.2.5",
"cash-dom": "^1.3.7",
"codemirror": "^5.48.4",
"codemirror": "^5.49.2",
"codemirror-formatting": "^1.0.0",
"font-awesome": "^4.7.0",
"keymaster": "^1.6.2",
@ -29,25 +29,26 @@
"underscore": "^1.9.1"
},
"devDependencies": {
"@babel/core": "^7.6.0",
"@babel/plugin-transform-runtime": "^7.6.0",
"@babel/preset-env": "^7.6.0",
"@babel/cli": "^7.7.0",
"@babel/core": "^7.7.0",
"@babel/plugin-transform-runtime": "^7.6.2",
"@babel/preset-env": "^7.7.1",
"babel-loader": "^8.0.6",
"documentation": "^8.1.2",
"eslint": "^6.4.0",
"eslint": "^6.6.0",
"html-webpack-plugin": "^3.2.0",
"husky": "^2.7.0",
"jest": "^24.9.0",
"lint-staged": "^8.2.1",
"node-sass": "^4.12.0",
"node-sass": "^4.13.0",
"npm-run-all": "^4.1.5",
"prettier": "^1.18.2",
"sinon": "^7.4.2",
"sinon": "^7.5.0",
"string-replace-loader": "^2.2.0",
"vuepress": "^0.10.2",
"webpack": "^4.40.2",
"webpack-cli": "^3.3.8",
"webpack-dev-server": "^3.8.0",
"webpack": "^4.41.2",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.9.0",
"whatwg-fetch": "^3.0.0"
},
"keywords": [
@ -70,8 +71,7 @@
"> 1%",
"ie 11",
"safari 8"
],
"modules": false
]
}
]
],
@ -122,8 +122,9 @@
"check": "npm run lint && npm run test",
"build": "npm run check && npm run v:patch && npm run build-dev && webpack --env=prod",
"build-n": "npm run check && npm run build:css && webpack --env=prod",
"build-dev": "webpack --env=dev && npm run build:css",
"build-dev": "webpack --env=dev && npm run build:css && npm run build:locale",
"build:css": "node-sass src/styles/scss/main.scss dist/css/grapes.min.css --output-style compressed",
"build:locale": "rm -rf ./locale && node scripts/build-locale.js && babel locale -d locale --copy-files --no-comments",
"v:patch": "npm version --no-git-tag-version patch",
"start": "run-p start:*",
"start:css": "npm run build:css -- -w",

28
scripts/build-locale.js

@ -0,0 +1,28 @@
const fs = require('fs');
const path = require('path');
const localeDir = './locale';
const localeSrcDir = './src/i18n/locale';
const copyRecursiveSync = (src, dest) => {
const exists = fs.existsSync(src);
const isDir = exists && fs.statSync(src).isDirectory();
if (isDir) {
fs.mkdirSync(dest);
fs.readdirSync(src).forEach((file) => {
copyRecursiveSync(path.join(src, file), path.join(dest, file));
});
} else if (exists) {
fs.createReadStream(src).pipe(fs.createWriteStream(dest));
}
};
copyRecursiveSync(localeSrcDir, localeDir);
// Create locale/index.js file
let result = '';
fs.readdirSync(localeDir).forEach(file => {
const name = file.replace('.js', '');
result += `export { default as ${name} } from './${name}'\n`;
});
fs.writeFileSync(`${localeDir}/index.js`, result);

12
src/asset_manager/config/config.js

@ -49,12 +49,6 @@ export default {
// }
autoAdd: 1,
// Text on upload input
uploadText: 'Drop files here or click to upload',
// Label for the add button
addBtnText: 'Add image',
// To upload your assets, the module uses Fetch API, with this option you
// overwrite it with something else.
// It should return a Promise
@ -96,12 +90,6 @@ export default {
// Any dropzone content to append inside dropzone element
dropzoneContent: '',
// Default title for the asset manager modal
modalTitle: 'Select Image',
//Default placeholder for input
inputPlaceholder: 'http://path/to/the/image.jpg',
//method called before upload, on return false upload is canceled.
// @example
// beforeUpload: (files) => {

10
src/asset_manager/view/AssetsView.js

@ -5,17 +5,16 @@ export default Backbone.View.extend({
submit: 'handleSubmit'
},
template(view) {
const pfx = view.pfx;
const ppfx = view.ppfx;
template({ pfx, ppfx, em, ...view }) {
return `
<div class="${pfx}assets-cont">
<div class="${pfx}assets-header">
<form class="${pfx}add-asset">
<div class="${ppfx}field ${pfx}add-field">
<input placeholder="${view.config.inputPlaceholder}"/>
<input placeholder="${em && em.t('assetManager.inputPlh')}"/>
</div>
<button class="${ppfx}btn-prim">${view.config.addBtnText}</button>
<button class="${ppfx}btn-prim">${em &&
em.t('assetManager.addButton')}</button>
<div style="clear:both"></div>
</form>
</div>
@ -30,6 +29,7 @@ export default Backbone.View.extend({
this.config = o.config;
this.pfx = this.config.stylePrefix || '';
this.ppfx = this.config.pStylePrefix || '';
this.em = this.config.em;
const coll = this.collection;
this.listenTo(coll, 'reset', this.renderAssets);
this.listenTo(coll, 'add', this.addToAsset);

10
src/asset_manager/view/FileUploader.js

@ -18,6 +18,7 @@ export default Backbone.View.extend(
this.options = opts;
const c = opts.config || {};
this.config = c;
this.em = this.config.em;
this.pfx = c.stylePrefix || '';
this.ppfx = c.pStylePrefix || '';
this.target = this.options.globalCollection || {};
@ -245,17 +246,18 @@ export default Backbone.View.extend(
},
render() {
this.$el.html(
const { $el, pfx, em } = this;
$el.html(
this.template({
title: this.config.uploadText,
title: em && em.t('assetManager.uploadTitle'),
uploadId: this.uploadId,
disabled: this.disabled,
multiUpload: this.multiUpload,
pfx: this.pfx
pfx
})
);
this.initDrop();
this.$el.attr('class', this.pfx + 'file-uploader');
$el.attr('class', pfx + 'file-uploader');
return this;
}
},

3
src/block_manager/view/BlockView.js

@ -139,7 +139,8 @@ export default Backbone.View.extend({
render() {
const { em, el, ppfx, model } = this;
const className = `${ppfx}block`;
const label = model.get('label');
const label =
(em && em.t(`blockManager.labels.${model.id}`)) || model.get('label');
const render = model.get('render');
const media = model.get('media');
el.className += ` ${className} ${ppfx}one-bg ${ppfx}four-color-h`;

15
src/block_manager/view/CategoryView.js

@ -14,7 +14,8 @@ export default Backbone.View.extend({
initialize(o = {}, config = {}) {
this.config = config;
const pfx = this.config.pStylePrefix || '';
const pfx = config.pStylePrefix || '';
this.em = config.em;
this.pfx = pfx;
this.caretR = 'fa fa-caret-right';
this.caretD = 'fa fa-caret-down';
@ -69,13 +70,17 @@ export default Backbone.View.extend({
},
render() {
this.el.innerHTML = this.template({
const { em, el, $el, model } = this;
const label =
em.t(`blockManager.categories.${model.id}`) || model.get('label');
el.innerHTML = this.template({
pfx: this.pfx,
label: this.model.get('label')
label
});
this.el.className = this.className;
this.$el.css({ order: this.model.get('order') });
el.className = this.className;
$el.css({ order: model.get('order') });
this.updateVisibility();
return this;
}
});

2
src/commands/view/OpenAssets.js

@ -4,7 +4,7 @@ export default {
const am = editor.AssetManager;
const config = am.getConfig();
const amContainer = am.getContainer();
const title = opts.modalTitle || config.modalTitle || '';
const title = opts.modalTitle || editor.t('assetManager.modalTitle') || '';
const types = opts.types;
const accept = opts.accept;

2
src/commands/view/OpenStyleManager.js

@ -33,7 +33,7 @@ export default {
const pfx = smConfig.stylePrefix;
// Create header
this.$header = $(
`<div class="${pfx}header">${smConfig.textNoElement}</div>`
`<div class="${pfx}header">${em.t('styleManager.empty')}</div>`
);
this.$cn.append(this.$header);

7
src/commands/view/OpenTraitManager.js

@ -5,6 +5,7 @@ const $ = Backbone.$;
export default {
run(editor, sender) {
this.sender = sender;
const em = editor.getModel();
var config = editor.Config;
var pfx = config.stylePrefix;
@ -18,11 +19,13 @@ export default {
this.$cn2 = $('<div></div>');
this.$cn.append(this.$cn2);
this.$header = $('<div>').append(
`<div class="${confTm.stylePrefix}header">${confTm.textNoElement}</div>`
`<div class="${confTm.stylePrefix}header">${em.t(
'traitManager.empty'
)}</div>`
);
this.$cn.append(this.$header);
this.$cn2.append(
`<div class="${pfx}traits-label">${confTm.labelContainer}</div>`
`<div class="${pfx}traits-label">${em.t('traitManager.label')}</div>`
);
this.$cn2.append(tmView.render().el);
var panels = editor.Panels;

4
src/device_manager/config/config.js

@ -1,5 +1,3 @@
export default {
devices: [],
deviceLabel: 'Device'
devices: []
};

29
src/device_manager/index.js

@ -58,7 +58,8 @@ export default () => {
if (!(name in c)) c[name] = defaults[name];
}
devices = new Devices(c.devices);
devices = new Devices();
(c.devices || []).forEach(dv => this.add(dv.id || dv.name, dv.width, dv));
view = new DevicesView({
collection: devices,
config: c
@ -68,21 +69,27 @@ export default () => {
/**
* Add new device to the collection. URLs are supposed to be unique
* @param {string} name Device name
* @param {string} width Width of the device
* @param {Object} opts Custom options
* @return {Device} Added device
* @param {String} id Device id
* @param {String} width Width of the device
* @param {Object} [opts] Custom options
* @returns {Device} Added device
* @example
* deviceManager.add('Tablet', '900px');
* deviceManager.add('Tablet2', '900px', {
* deviceManager.add('tablet', '900px');
* deviceManager.add('tablet2', '900px', {
* height: '300px',
* // At first, GrapesJS tries to localize the name by device id.
* // In case is not found, the `name` property is used (or `id` if name is missing)
* name: 'Tablet 2',
* widthMedia: '810px', // the width that will be used for the CSS media
* });
*/
add(name, width, opts) {
var obj = opts || {};
obj.name = name;
obj.width = width;
add(id, width, opts = {}) {
const obj = {
...opts,
id,
name: opts.name || id,
width: width
};
return devices.add(obj);
},

24
src/device_manager/view/DevicesView.js

@ -67,25 +67,29 @@ export default Backbone.View.extend({
* @private
*/
getOptions() {
var result = '';
this.collection.each(device => {
var name = device.get('name');
result += '<option value="' + name + '">' + name + '</option>';
const { collection, em } = this;
let result = '';
collection.each(device => {
const { name, id } = device.attributes;
const label = (em && em.t && em.t(`deviceManager.devices.${id}`)) || name;
result += `<option value="${name}">${label}</option>`;
});
return result;
},
render() {
var pfx = this.ppfx;
this.$el.html(
const { em, ppfx, $el, el } = this;
$el.html(
this.template({
ppfx: pfx,
deviceLabel: this.config.deviceLabel
ppfx,
deviceLabel: em && em.t && em.t('deviceManager.device')
})
);
this.devicesEl = this.$el.find('.' + pfx + 'devices');
this.devicesEl = $el.find(`.${ppfx}devices`);
this.devicesEl.append(this.getOptions());
this.el.className = pfx + 'devices-c';
el.className = `${ppfx}devices-c`;
return this;
}
});

13
src/dom_components/model/Component.js

@ -865,12 +865,17 @@ const Component = Backbone.Model.extend(Styleable).extend(
* @return {String}
* */
getName() {
let customName = this.get('name') || this.get('custom-name');
let tag = this.get('tagName');
const { em } = this;
const { type, tagName } = this.attributes;
const customName = this.get('name') || this.get('custom-name');
let tag = tagName;
tag = tag == 'div' ? 'box' : tag;
let name = this.get('type') || tag;
let name = type || tag;
name = name.charAt(0).toUpperCase() + name.slice(1);
return customName || name;
const i18nPfx = 'domComponents.names.';
const i18nStr =
em && (em.t(`${i18nPfx}${type}`) || em.t(`${i18nPfx}${tagName}`));
return i18nStr || customName || name;
},
/**

7
src/editor/config/config.js

@ -151,6 +151,9 @@ export default {
// Dom element
el: '',
// Configurations for I18n
i18n: {},
// Configurations for Undo Manager
undoManager: {},
@ -194,20 +197,24 @@ export default {
deviceManager: {
devices: [
{
id: 'desktop',
name: 'Desktop',
width: ''
},
{
id: 'tablet',
name: 'Tablet',
width: '768px',
widthMedia: '992px'
},
{
id: 'mobileLandscape',
name: 'Mobile landscape',
width: '568px',
widthMedia: '768px'
},
{
id: 'mobilePortrait',
name: 'Mobile portrait',
width: '320px',
widthMedia: '480px'

24
src/editor/index.js

@ -125,6 +125,12 @@ export default (config = {}) => {
*/
editor: em,
/**
* @property {I18n}
* @private
*/
I18n: em.get('I18n'),
/**
* @property {DomComponents}
* @private
@ -649,6 +655,24 @@ export default (config = {}) => {
return this;
},
/**
* Translate label
* @param {String} key Label to translate
* @param {Object} [opts] Options for the translation
* @param {Object} [opts.params] Params for the translation
* @param {Boolean} [opts.noWarn] Avoid warnings in case of missing resources
* @returns {String}
* @example
* editor.t('msg');
* // use params
* editor.t('msg2', { params: { test: 'hello' } });
* // custom local
* editor.t('msg2', { params: { test: 'hello' }, l: 'it' });
*/
t(...args) {
return em.t(...args);
},
/**
* Attach event
* @param {string} event Event name

5
src/editor/model/Editor.js

@ -5,6 +5,7 @@ import { getModel } from 'utils/mixins';
const deps = [
require('utils'),
require('i18n'),
require('keymaps'),
require('undo_manager'),
require('storage_manager'),
@ -629,6 +630,10 @@ export default Backbone.Model.extend({
return this.set('dmode', value);
},
t(...args) {
return this.get('I18n').t(...args);
},
/**
* Returns true if the editor is in absolute mode
* @returns {Boolean}

20
src/i18n/config.js

@ -0,0 +1,20 @@
import en from './locale/en';
export default {
// Locale value
locale: 'en',
// Fallback locale
localeFallback: 'en',
// Detect locale by checking browser language
detectLocale: 1,
// Show warnings when some of the i18n resources are missing
debug: 0,
// Messages to translate
messages: {
en
}
};

247
src/i18n/index.js

@ -0,0 +1,247 @@
/**
* You can customize the initial state of the module from the editor initialization, by passing the following [Configuration Object](https://github.com/artf/grapesjs/blob/master/src/i18n/config.js)
* ```js
* const editor = grapesjs.init({
* i18n: {
* locale: 'en',
* localeFallback: 'en',
* messages: {
* it: { hello: 'Ciao', ... },
* ...
* }
* }
* })
* ```
*
* Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance
*
* ```js
* const i18n = editor.I18n;
* ```
*
* ### Events
* * `i18n:add` - New set of messages is added
* * `i18n:update` - The set of messages is updated
* * `i18n:locale` - Locale changed
*
* @module I18n
*/
import { isUndefined, isString } from 'underscore';
import config from './config';
const isObj = el => !Array.isArray(el) && el !== null && typeof el === 'object';
const deepAssign = (...args) => {
const target = { ...args[0] };
for (let i = 1; i < args.length; i++) {
const source = { ...args[i] };
for (let key in source) {
const targValue = target[key];
const srcValue = source[key];
if (isObj(targValue) && isObj(srcValue)) {
target[key] = deepAssign(targValue, srcValue);
} else {
target[key] = srcValue;
}
}
}
return target;
};
export default () => {
return {
name: 'I18n',
config,
/**
* Initialize module
* @param {Object} config Configurations
* @private
*/
init(opts = {}) {
this.config = {
...config,
...opts,
messages: {
...config.messages,
...(opts.messages || {})
}
};
if (this.config.detectLocale) {
this.config.locale = this._localLang();
}
this.em = opts.em;
return this;
},
/**
* Get module configurations
* @returns {Object} Configuration object
*/
getConfig() {
return this.config;
},
/**
* Update current locale
* @param {String} locale Locale value
* @returns {this}
* @example
* i18n.setLocale('it');
*/
setLocale(locale) {
const { em, config } = this;
const evObj = { value: locale, valuePrev: config.locale };
em && em.trigger('i18n:locale', evObj);
config.locale = locale;
return this;
},
/**
* Get current locale
* @returns {String} Current locale value
*/
getLocale() {
return this.config.locale;
},
/**
* Get all messages
* @param {String} [lang] Specify the language of messages to return
* @param {Object} [opts] Options
* @param {Boolean} [opts.debug] Show warnings in case of missing language
* @returns {Object}
* @example
* i18n.getMessages();
* // -> { en: { hello: '...' }, ... }
* i18n.getMessages('en');
* // -> { hello: '...' }
*/
getMessages(lang, opts = {}) {
const { messages } = this.config;
lang &&
!messages[lang] &&
this._debug(`'${lang}' i18n lang not found`, opts);
return lang ? messages[lang] : messages;
},
/**
* Set new set of messages
* @param {Object} msg Set of messages
* @returns {this}
* @example
* i18n.getMessages();
* // -> { en: { msg1: 'Msg 1', msg2: 'Msg 2', } }
* i18n.setMessages({ en: { msg2: 'Msg 2 up', msg3: 'Msg 3', } });
* // Set replaced
* i18n.getMessages();
* // -> { en: { msg2: 'Msg 2 up', msg3: 'Msg 3', } }
*/
setMessages(msg) {
const { em, config } = this;
config.messages = msg;
em && em.trigger('i18n:update', msg);
return this;
},
/**
* Update messages
* @param {Object} msg Set of messages to add
* @returns {this}
* @example
* i18n.getMessages();
* // -> { en: { msg1: 'Msg 1', msg2: 'Msg 2', } }
* i18n.addMessages({ en: { msg2: 'Msg 2 up', msg3: 'Msg 3', } });
* // Set updated
* i18n.getMessages();
* // -> { en: { msg1: 'Msg 1', msg2: 'Msg 2 up', msg3: 'Msg 3', } }
*/
addMessages(msg) {
const { em } = this;
const { messages } = this.config;
em && em.trigger('i18n:add', msg);
this.setMessages(deepAssign(messages, msg));
return this;
},
/**
* Translate the locale message
* @param {String} key Label to translate
* @param {Object} [opts] Options for the translation
* @param {Object} [opts.params] Params for the translation
* @param {Boolean} [opts.debug] Show warnings in case of missing resources
* @returns {String}
* @example
* obj.setMessages({
* en: { msg: 'Msg', msg2: 'Msg {test}'},
* it: { msg2: 'Msg {test} it'},
* });
* obj.t('msg');
* // -> outputs `Msg`
* obj.t('msg2', { params: { test: 'hello' } }); // use params
* // -> outputs `Msg hello`
* obj.t('msg2', { l: 'it', params: { test: 'hello' } }); // custom local
* // -> outputs `Msg hello it`
*/
t(key, opts = {}) {
const { config } = this;
const param = opts.params || {};
const locale = opts.l || this.getLocale();
const localeFlb = opts.lFlb || config.localeFallback;
let result = this._getMsg(key, locale, opts);
// Try with fallback
if (!result) result = this._getMsg(key, localeFlb, opts);
!result &&
this._debug(`'${key}' i18n key not found in '${locale}' lang`, opts);
result =
result && isString(result) ? this._addParams(result, param) : result;
return result;
},
_localLang() {
const nav = window.navigator || {};
const lang = nav.language || nav.userLanguage;
return lang ? lang.split('-')[0] : 'en';
},
_addParams(str, params) {
const reg = new RegExp(`\{([\\w\\d-]*)\}`, 'g');
return str.replace(reg, (m, val) => params[val] || '').trim();
},
_getMsg(key, locale, opts = {}) {
const msgSet = this.getMessages(locale, opts);
// Lang set is missing
if (!msgSet) return;
let result = msgSet[key];
// Check for nested getter
if (!result && key.indexOf('.') > 0) {
result = key.split('.').reduce((lang, key) => {
if (isUndefined(lang)) return;
return lang[key];
}, msgSet);
}
return result;
},
_debug(str, opts = {}) {
const { em, config } = this;
(opts.debug || config.debug) && em && em.logWarning(str);
}
};
};

117
src/i18n/locale/en.js

@ -0,0 +1,117 @@
const traitInputAttr = { placeholder: 'eg. Text here' };
export default {
assetManager: {
addButton: 'Add image',
inputPlh: 'http://path/to/the/image.jpg',
modalTitle: 'Select Image',
uploadTitle: 'Drop files here or click to upload'
},
// Here just as a reference, GrapesJS core doesn't contain any block,
// so this should be omitted from other local files
blockManager: {
labels: {
// 'block-id': 'Block Label',
},
categories: {
// 'category-id': 'Category Label',
}
},
domComponents: {
names: {
'': 'Box',
wrapper: 'Body',
text: 'Text',
comment: 'Comment',
image: 'Image',
video: 'Video',
label: 'Label',
link: 'Link',
map: 'Map',
tfoot: 'Table foot',
tbody: 'Table body',
thead: 'Table head',
table: 'Table',
row: 'Table row',
cell: 'Table cell'
}
},
deviceManager: {
device: 'Device',
devices: {
desktop: 'Desktop',
tablet: 'Tablet',
mobileLandscape: 'Mobile Landscape',
mobilePortrait: 'Mobile Portrait'
}
},
panels: {
buttons: {
titles: {
preview: 'Preview',
fullscreen: 'Fullscreen',
'sw-visibility': 'View components',
'export-template': 'View code',
'open-sm': 'Open Style Manager',
'open-tm': 'Settings',
'open-layers': 'Open Layer Manager',
'open-blocks': 'Open Blocks'
}
}
},
selectorManager: {
label: 'Classes',
selected: 'Selected',
emptyState: '- State -',
states: {
hover: 'Hover',
active: 'Click',
'nth-of-type(2n)': 'Even/Odd'
}
},
styleManager: {
empty: 'Select an element before using Style Manager',
layer: 'Layer',
fileButton: 'Images',
sectors: {
general: 'General',
layout: 'Layout',
typography: 'Typography',
decorations: 'Decorations',
extra: 'Extra',
flex: 'Flex',
dimension: 'Dimension'
},
// The core library generates the name by their `property` name
properties: {
// float: 'Float',
}
},
traitManager: {
empty: 'Select an element before using Trait Manager',
label: 'Component settings',
traits: {
// The core library generates the name by their `name` property
labels: {
// id: 'Id',
// alt: 'Alt',
// title: 'Title',
// href: 'Href',
},
// In a simple trait, like text input, these are used on input attributes
attributes: {
id: traitInputAttr,
alt: traitInputAttr,
title: traitInputAttr,
href: { placeholder: 'eg. https://google.com' }
},
// In a trait like select, these are used to translate option names
options: {
target: {
'': 'This window',
_blank: 'New window'
}
}
}
}
};

103
src/i18n/locale/it.js

@ -0,0 +1,103 @@
const traitInputAttr = { placeholder: 'es. Testo' };
export default {
assetManager: {
addButton: 'Aggiungi immagine',
inputPlh: 'http://percorso/immagine.jpg',
modalTitle: 'Seleziona immagine',
uploadTitle: 'Trascina qui i tuoi file o clicca per caricarli'
},
domComponents: {
names: {
'': 'Elemento',
wrapper: 'Contenitore',
text: 'Testo',
comment: 'Commento',
image: 'Immagine',
video: 'Video',
label: 'Label',
link: 'Link',
map: 'Mappa',
tfoot: 'Tabella piede',
tbody: 'Tabella corpo',
thead: 'Tabella testa',
table: 'Tabella',
row: 'Tabella riga',
cell: 'Tabella colonna'
}
},
deviceManager: {
device: 'Dispositivo',
devices: {
desktop: 'Desktop',
tablet: 'Tablet',
mobileLandscape: 'Mobile panoramica',
mobilePortrait: 'Mobile'
}
},
panels: {
buttons: {
titles: {
preview: 'Anteprima',
fullscreen: 'Schermo intero',
'sw-visibility': 'Mostra componenti',
'export-template': 'Mostra codice',
'open-sm': 'Mostra Style Manager',
'open-tm': 'Configurazioni',
'open-layers': 'Mostra Livelli',
'open-blocks': 'Mostra Blocchi'
}
}
},
selectorManager: {
label: 'Classi',
selected: 'Selezionato',
emptyState: '- Stati -',
states: {
hover: 'Hover',
active: 'Click',
'nth-of-type(2n)': 'Pari/Dispari'
}
},
styleManager: {
empty: 'Seleziona un elemento prima di usare il Style Manager',
layer: 'Livello',
fileButton: 'Immagini',
sectors: {
general: 'Generale',
layout: 'Layout',
typography: 'Tipografia',
decorations: 'Decorazioni',
extra: 'Extra',
flex: 'Flex',
dimension: 'Dimensioni'
},
// The core library generates the name by their `property` name
properties: {
// float: 'Float',
}
},
traitManager: {
empty: 'Seleziona un elemento prima di usare il Trait Manager',
label: 'Configurazione componente',
traits: {
labels: {
id: 'Id',
alt: 'Alt',
title: 'Titolo'
},
attributes: {
id: traitInputAttr,
alt: traitInputAttr,
title: traitInputAttr,
href: { placeholder: 'es. https://google.com' }
},
options: {
target: {
'': 'Questa finestra',
_blank: 'Nuova finestra'
}
}
}
}
};

141
src/i18n/locale/tr.js

@ -0,0 +1,141 @@
export default {
assetManager: {
addButton: 'Görsel Ekle',
modalTitle: 'Görsel Seçin',
uploadTitle: 'Dosya yüklemek için buraya sürükleyin veya tıklayın'
},
deviceManager: {
device: 'Cihaz',
devices: {
desktop: 'Masaüstü',
tablet: 'Tablet',
mobileLandscape: 'Mobil Yatay',
mobilePortrait: 'Mobil Dikey'
}
},
panels: {
buttons: {
titles: {
preview: 'Önizleme',
fullscreen: 'Tam Ekran',
'sw-visibility': 'Bileşenleri Göster',
'export-template': 'Kodu Göster',
'open-sm': 'Stil Düzenleyiciyi Aç',
'open-tm': 'Ayarlar',
'open-layers': 'Katmanlar',
'open-blocks': 'Bloklar'
}
}
},
selectorManager: {
selected: 'Seçili',
emptyState: '- DURUM -',
label: 'Sınıflar'
},
styleManager: {
empty: 'Stilini düzenlemek istediğiniz öğeyi seçiniz',
layer: 'Katman',
sectors: {
general: 'Genel',
layout: 'Düzen',
typography: 'Tipografi',
decorations: 'Dekorasyon',
extra: 'Ekstra',
flex: 'Flex',
dimension: 'Boyut'
},
properties: {
float: 'Kaydır',
display: 'Görünüm',
position: 'Pozisyon',
top: 'Üst',
right: 'Sağ',
left: 'Sol',
bottom: 'Alt',
width: 'Genişlik',
height: 'Yükseklik',
'max-width': 'Maks. Genişlik',
'max-height': 'Maks. Yükseklik',
margin: 'Margin',
'margin-top': 'Margin Üst',
'margin-right': 'Margin Sağ',
'margin-left': 'Margin Sol',
'margin-bottom': 'Margin Alt',
padding: 'Padding',
'padding-top': 'Padding Üst',
'padding-left': 'Padding Sol',
'padding-right': 'Padding Sağ',
'padding-bottom': 'Padding Alt',
'font-family': 'Font Tipi',
'font-size': 'Font Boyutu',
'font-weight': 'Font Kalınlığı',
'letter-spacing': 'Harf Boşluğu',
color: 'Renk',
'line-height': 'Satır Boşluğu',
'text-align': 'Yazı Hizalaması',
'text-shadow': 'Yazı Gölgesi',
'text-shadow-h': 'Yazı Gölgesi - Yatay',
'text-shadow-v': 'Yazı Gölgesi - Dikey',
'text-shadow-blur': 'Yazı Gölgesi Bulanıklığı',
'text-shadow-color': 'Yazı Gölgesi Rengi',
'border-top-left': 'Kenar Üst Sol',
'border-top-right': 'Kenar Üst Sağ',
'border-bottom-left': 'Kenar Alt Sol',
'border-bottom-right': 'Kenar Alt Sağ',
'border-radius-top-left': 'Köşe Yumuşuması Üst Sol',
'border-radius-top-right': 'Köşe Yumuşuması Üst Sağ',
'border-radius-bottom-left': 'Köşe Yumuşuması Alt Sol',
'border-radius-bottom-right': 'Köşe Yumuşuması Alt Sağ',
'border-radius': 'Köşe Yumuşaması',
border: 'Kenar',
'border-width': 'Kenar Kalınlığı',
'border-style': 'Kenar Stili',
'border-color': 'Kenar Rengi',
'box-shadow': 'Kutu Gölgesi',
'box-shadow-h': 'Kutu Gölgesi - Yatay',
'box-shadow-v': 'Kutu Gölgesi - Dikey',
'box-shadow-blur': 'Kutu Gölgesi Bulanıklığı',
'box-shadow-spread': 'Kutu Gölgesi Dağılımı',
'box-shadow-color': 'Kutu Gölgesi Rengi',
'box-shadow-type': 'Kutu Gölgesi Tipi',
background: 'Arkaplan',
'background-image': 'Arkaplan Resmi',
'background-repeat': 'Arkaplan Tekrarı',
'background-position': 'Arkaplan Pozisyonu',
'background-attachment': 'Arkaplan Eklentisi',
'background-size': 'Arkaplan Boyutu',
transition: 'Geçiş',
'transition-property': 'Geçiş Özelliği',
'transition-duration': 'Geçiş Süresi',
'transition-timing-function': 'Geçiş Zamanlaması Metodu',
perspective: 'Perspektif',
transform: 'Boyutlama',
'transform-rotate-x': 'Yatay Yönlendirme',
'transform-rotate-y': 'Dikey Yönlendirme',
'transform-rotate-z': 'Hacimsel Yönlendirme',
'transform-scale-x': 'Dikey Oran',
'transform-scale-y': 'Yatay Oran',
'transform-scale-z': 'Hacimsel Oran',
'flex-direction': 'Flex Yönü',
'flex-wrap': 'Flex Kesme',
'justify-content': 'İçeriği Sığdır',
'align-items': 'Öğeleri Hizala',
'align-content': 'İçeriği Hizala',
order: 'Sıra',
'flex-basis': 'Flex Bazı',
'flex-grow': 'Flex Büyüme',
'flex-shrink': 'Flex Küçülme',
'align-self': 'Kendini Hizala',
'background-color': 'Arkaplan Rengi'
}
},
traitManager: {
empty: 'Özelliklerini düzenlemek istediğiniz öğeyi seçiniz',
label: 'Bileşen Özellikleri',
traits: {
labels: {},
attributes: {},
options: {}
}
}
};

5
src/keymaps/messages.js

@ -0,0 +1,5 @@
export default {
en: {
hello: 'Hello'
}
};

16
src/panels/config/config.js

@ -1,11 +1,11 @@
var swv = 'sw-visibility';
var expt = 'export-template';
var osm = 'open-sm';
var otm = 'open-tm';
var ola = 'open-layers';
var obl = 'open-blocks';
var ful = 'fullscreen';
var prv = 'preview';
const swv = 'sw-visibility';
const expt = 'export-template';
const osm = 'open-sm';
const otm = 'open-tm';
const ola = 'open-layers';
const obl = 'open-blocks';
const ful = 'fullscreen';
const prv = 'preview';
export default {
stylePrefix: 'pn-',

7
src/panels/view/ButtonView.js

@ -55,7 +55,12 @@ export default Backbone.View.extend({
* @return void
* */
updateAttributes() {
this.$el.attr(this.model.get('attributes'));
const { em, model, $el } = this;
const attr = model.get('attributes') || {};
const title = em && em.t && em.t(`panels.buttons.titles.${model.id}`);
$el.attr(attr);
title && $el.attr({ title });
this.updateClassName();
},

14
src/selector_manager/config/config.js

@ -9,20 +9,8 @@ export default {
// Default selectors
selectors: [],
// Label for selectors
label: 'Classes',
// Label for states
statesLabel: '- State -',
selectedLabel: 'Selected',
// States
states: [
{ name: 'hover', label: 'Hover' },
{ name: 'active', label: 'Click' },
{ name: 'nth-of-type(2n)', label: 'Even/Odd' }
],
states: [{ name: 'hover' }, { name: 'active' }, { name: 'nth-of-type(2n)' }],
// Custom selector name escaping strategy, eg.
// name => name.replace(' ', '_')

6
src/selector_manager/view/ClassTagView.js

@ -4,9 +4,9 @@ const inputProp = 'contentEditable';
export default Backbone.View.extend({
template() {
const pfx = this.pfx;
const ppfx = this.ppfx;
const label = this.model.get('label') || '';
const { pfx, model } = this;
const label = model.get('label') || '';
return `
<span id="${pfx}checkbox" class="fa" data-tag-status></span>
<span id="${pfx}tag-label" data-tag-name>${label}</span>

40
src/selector_manager/view/ClassTagsView.js

@ -90,16 +90,20 @@ export default Backbone.View.extend({
* @private
*/
getStateOptions() {
var strInput = '';
for (var i = 0; i < this.states.length; i++) {
strInput +=
'<option value="' +
this.states[i].name +
'">' +
this.states[i].label +
'</option>';
}
return strInput;
const { states, em } = this;
let result = [];
states.forEach(state =>
result.push(
`<option value="${state.name}">${em.t(
`selectorManager.states.${state.name}`
) ||
state.label ||
state.name}</option>`
)
);
return result.join('');
},
/**
@ -304,21 +308,19 @@ export default Backbone.View.extend({
},
render() {
const ppfx = this.ppfx;
const config = this.config;
const $el = this.$el;
const { em, pfx, ppfx, $el } = this;
$el.html(
this.template({
selectedLabel: config.selectedLabel,
statesLabel: config.statesLabel,
label: config.label,
pfx: this.pfx,
ppfx: this.ppfx
selectedLabel: em.t('selectorManager.selected'),
statesLabel: em.t('selectorManager.emptyState'),
label: em.t('selectorManager.label'),
pfx,
ppfx
})
);
this.$input = $el.find('input#' + this.newInputId);
this.$addBtn = $el.find('#' + this.addBtnId);
this.$classes = $el.find('#' + this.pfx + 'tags-c');
this.$classes = $el.find('#' + pfx + 'tags-c');
this.$states = $el.find('#' + this.stateInputId);
this.$statesC = $el.find('#' + this.stateInputC);
this.$states.append(this.getStateOptions());

6
src/style_manager/config/config.js

@ -7,12 +7,6 @@ export default {
// With the empty value, nothing will be rendered
appendTo: '',
// Text to show in case no element selected
textNoElement: 'Select an element before using Style Manager',
// Text for layers
textLayer: 'Layer',
// Hide the property in case it's not stylable for the
// selected component (each component has 'stylable' property)
hideNotStylable: true,

4
src/style_manager/model/PropertyInteger.js

@ -21,7 +21,8 @@ export default Property.extend({
max: ''
},
init() {
initialize(props = {}, opts = {}) {
Property.callParentInit(Property, this, props, opts);
const unit = this.get('unit');
const units = this.get('units');
this.input = new InputNumber({ model: this });
@ -29,6 +30,7 @@ export default Property.extend({
if (units.length && !unit) {
this.set('unit', units[0]);
}
Property.callInit(this, props, opts);
},
clearValue(opts = {}) {

5
src/style_manager/view/LayerView.js

@ -10,8 +10,8 @@ export default Backbone.View.extend({
},
template(model) {
const { pfx, ppfx, config } = this;
const label = `${config.textLayer} ${model.get('index')}`;
const { pfx, ppfx, em } = this;
const label = `${em && em.t('styleManager.layer')} ${model.get('index')}`;
return `
<div id="${pfx}move" class="${ppfx}no-touch-actions" data-move-layer>
@ -33,6 +33,7 @@ export default Backbone.View.extend({
let model = this.model;
this.stackModel = o.stackModel || {};
this.config = o.config || {};
this.em = this.config.em;
this.pfx = this.config.stylePrefix || '';
this.ppfx = this.config.pStylePrefix || '';
this.sorter = o.sorter || null;

7
src/style_manager/view/PropertyFileView.js

@ -6,15 +6,14 @@ const $ = Backbone.$;
export default PropertyView.extend({
templateInput() {
const pfx = this.pfx;
const ppfx = this.ppfx;
const assetsLabel = this.config.assetsLabel || 'Images';
const { pfx, em } = this;
return `
<div class="${pfx}field ${pfx}file">
<div id='${pfx}input-holder'>
<div class="${pfx}btn-c">
<button class="${pfx}btn" id="${pfx}images" type="button">
${assetsLabel}
${em.t('styleManager.fileButton')}
</button>
</div>
<div style="clear:both;"></div>

10
src/style_manager/view/PropertyView.js

@ -19,14 +19,14 @@ export default Backbone.View.extend({
},
templateLabel(model) {
const { pfx } = this;
const icon = model.get('icon') || '';
const info = model.get('info') || '';
const parent = model.parent;
const { pfx, em } = this;
const { parent } = model;
const { icon = '', info = '', id, name } = model.attributes;
const label = (em && em.t(`styleManager.properties.${id}`)) || name;
return `
<span class="${pfx}icon ${icon}" title="${info}">
${model.get('name')}
${label}
</span>
${!parent ? `<b class="${pfx}clear" ${clearProp}>&Cross;</b>` : ''}
`;

17
src/style_manager/view/SectorView.js

@ -15,6 +15,7 @@ export default Backbone.View.extend({
initialize(o) {
this.config = o.config || {};
this.em = this.config.em;
this.pfx = this.config.stylePrefix || '';
this.target = o.target || {};
this.propTarget = o.propTarget || {};
@ -78,17 +79,13 @@ export default Backbone.View.extend({
},
render() {
const { pfx, model } = this;
const { id } = model.attributes;
this.$el.html(
this.template({
pfx,
label: model.get('name')
})
);
this.$caret = this.$el.find(`#${pfx}caret`);
const { pfx, model, em, $el } = this;
const { id, name } = model.attributes;
const label = (em && em.t(`styleManager.sectors.${id}`)) || name;
$el.html(this.template({ pfx, label }));
this.$caret = $el.find(`#${pfx}caret`);
this.renderProperties();
this.$el.attr('class', `${pfx}sector ${pfx}sector__${id} no-select`);
$el.attr('class', `${pfx}sector ${pfx}sector__${id} no-select`);
this.updateOpen();
return this;
},

16
src/trait_manager/config/config.js

@ -5,20 +5,6 @@ export default {
// With the empty value, nothing will be rendered
appendTo: '',
labelContainer: 'Component settings',
// Placeholder label for text input types
labelPlhText: 'eg. Text here',
// Placeholder label for href input
labelPlhHref: 'eg. https://google.com',
// Default options for the target input
optionsTarget: [
{ value: '', name: 'This window' },
{ value: '_blank', name: 'New window' }
],
// Text to show in case no element selected
textNoElement: 'Select an element before using Trait Manager'
optionsTarget: [{ value: '' }, { value: '_blank' }]
};

12
src/trait_manager/model/TraitFactory.js

@ -21,18 +21,6 @@ export default (config = {}) => ({
break;
}
// Define placeholder
switch (prop) {
case 'title':
case 'alt':
case 'id':
obj.placeholder = config.labelPlhText;
break;
case 'href':
obj.placeholder = config.labelPlhHref;
break;
}
// Define options
switch (prop) {
case 'target':

8
src/trait_manager/view/TraitSelectView.js

@ -26,7 +26,8 @@ export default TraitView.extend({
*/
getInputEl() {
if (!this.$input) {
const { model } = this;
const { model, em } = this;
const propName = model.get('name');
const opts = model.get('options') || [];
let input = '<select>';
@ -46,8 +47,9 @@ export default TraitView.extend({
style = el.style ? el.style.replace(/"/g, '&quot;') : '';
attrs += style ? ` style="${style}"` : '';
}
input += `<option value="${value}"${attrs}>${name}</option>`;
const resultName =
em.t(`traitManager.traits.options.${propName}.${value}`) || name;
input += `<option value="${value}"${attrs}>${resultName}</option>`;
});
input += '</select>';

12
src/trait_manager/view/TraitView.js

@ -135,8 +135,12 @@ export default Backbone.View.extend({
* @private
*/
getLabel() {
const { em } = this;
const { label, name } = this.model.attributes;
return capitalize(label || name).replace(/-/g, ' ');
return (
em.t(`traitManager.traits.labels.${name}`) ||
capitalize(label || name).replace(/-/g, ' ')
);
},
/**
@ -153,13 +157,17 @@ export default Backbone.View.extend({
*/
getInputEl() {
if (!this.$input) {
const md = this.model;
const { em, model } = this;
const md = model;
const { name } = model.attributes;
const plh = md.get('placeholder') || md.get('default') || '';
const type = md.get('type') || 'text';
const min = md.get('min');
const max = md.get('max');
const value = this.getModelValue();
const input = $(`<input type="${type}" placeholder="${plh}">`);
const i18nAttr = em.t(`traitManager.traits.attributes.${name}`) || {};
input.attr(i18nAttr);
if (!isUndefined(value)) {
md.set({ value }, { silent: true });

10
test/specs/asset_manager/view/FileUploader.js

@ -40,16 +40,6 @@ describe('File Uploader', () => {
});
describe('Interprets configurations correctly', () => {
test('Has correct title', () => {
var view = new FileUploader({
config: {
uploadText: 'Test'
}
});
view.render();
expect(view.$el.find('#title').html()).toEqual('Test');
});
test('Could be disabled', () => {
var view = new FileUploader({
config: {

204
test/specs/i18n/index.js

@ -0,0 +1,204 @@
import I18n from 'i18n';
import Editor from 'editor/index';
describe('I18n', () => {
describe('Main', () => {
let obj;
let editor = Editor().init();
let em = editor.getModel();
beforeEach(() => {
obj = I18n();
obj.init({ em });
});
test('Object exists', () => {
expect(obj).toBeTruthy();
});
test('getConfig method', () => {
expect(obj.getConfig()).toBeTruthy();
});
test('Default local', () => {
expect(obj.getLocale()).toBeTruthy();
});
test('Init with config', () => {
const locale = 'it';
const localeFallback = 'it';
const msg = 'Hello!!!';
obj.init({
em,
locale,
localeFallback,
detectLocale: 0,
messages: {
en: { msg }
}
});
expect(obj.getLocale()).toBe(locale);
expect(obj.getConfig().localeFallback).toBe(localeFallback);
expect(obj.getLocale()).toBe(locale);
});
test('English always imported', () => {
obj.init({
messages: { it: {} }
});
expect(Object.keys(obj.getMessages())).toEqual(['en', 'it']);
});
test('setLocale and getLocale methods', () => {
const localeBefore = obj.getLocale();
const localeNew = `${localeBefore}2`;
obj.setLocale(localeNew);
expect(obj.getLocale()).toBe(localeNew);
});
test('Default messages', () => {
expect(obj.getMessages()).toBeTruthy();
});
test('setMessages method', () => {
const set1 = { en: { msg1: 'Msg 1' } };
obj.setMessages(set1);
expect(obj.getMessages()).toEqual(set1);
const set2 = { en: { msg2: 'Msg 2' } };
obj.setMessages(set2);
expect(obj.getMessages()).toEqual(set2);
});
test('addMessages method', () => {
const set1 = { en: { msg1: 'Msg 1', msg2: 'Msg 2' } };
obj.setMessages(set1);
const set2 = {
en: { msg2: 'Msg 2 up', msg3: 'Msg 3' },
it: { msg1: 'Msg 1' }
};
obj.addMessages(set2);
expect(obj.getMessages()).toEqual({
en: { msg1: 'Msg 1', msg2: 'Msg 2 up', msg3: 'Msg 3' },
it: { msg1: 'Msg 1' }
});
});
test('addMessages with deep extend possibility', () => {
obj.setMessages({
en: {
msg1: 'Msg 1',
msg2: 'Msg 2',
msg3: {
msg31: 'Msg 31',
msg32: { msg321: 'Msg 321' }
}
}
});
obj.addMessages({
en: {
msg2: { msg21: 'Msg 21' },
msg3: {
msg32: { msg322: 'Msg 322' },
msg33: 'Msg 33'
},
msg4: 'Msg 4'
}
});
expect(obj.getMessages()).toEqual({
en: {
msg1: 'Msg 1',
msg2: { msg21: 'Msg 21' },
msg3: {
msg31: 'Msg 31',
msg32: {
msg321: 'Msg 321',
msg322: 'Msg 322'
},
msg33: 'Msg 33'
},
msg4: 'Msg 4'
}
});
});
test('Translate method with global locale', () => {
const msg1 = 'Msg 1';
obj.setLocale('en');
obj.setMessages({
en: { msg1 },
it: { msg1: `${msg1} it` }
});
expect(obj.t('msg2')).toBe(undefined);
expect(obj.t('msg1')).toBe(msg1);
});
test('Translate method with object structure', () => {
const msg1 = 'Msg level 1';
const msg2 = 'Msg level 2';
obj.setLocale('en');
obj.setMessages({
en: {
key1: {
msg1,
key2: {
msg2
}
}
}
});
expect(obj.t('key1.msg1')).toBe(msg1);
expect(obj.t('key1.key2.msg2')).toBe(msg2);
expect(obj.t('key1.key2.msg3')).toBe(undefined);
expect(obj.t('key1.key3.msg2')).toBe(undefined);
});
test('Translate method with custom locale', () => {
const msg1 = 'Msg 1';
const msg1Alt = `${msg1} it`;
obj.setLocale('en');
obj.setMessages({
en: { msg1 },
it: { msg1: msg1Alt }
});
expect(obj.t('msg1', { l: 'it' })).toBe(msg1Alt);
});
test('Translate method with fallback locale', () => {
const msg1 = 'Msg en';
obj.setLocale('it');
obj.setMessages({
en: { msg1 },
it: {}
});
expect(obj.t('msg1')).toBe(msg1);
});
test('Translate method with a param', () => {
const msg1 = 'Msg 1 {test}';
const msg1Alt = `${msg1} it`;
obj.setLocale('en');
obj.setMessages({
en: { msg1 },
it: { msg1: msg1Alt }
});
expect(obj.t('msg1', { params: { test: 'Hello' } })).toBe('Msg 1 Hello');
expect(obj.t('msg1', { l: 'it', params: { test: 'Hello' } })).toBe(
'Msg 1 Hello it'
);
});
test('i18n events', () => {
const handlerAdd = jest.fn();
const handlerUpdate = jest.fn();
const handlerLocale = jest.fn();
em.on('i18n:add', handlerAdd);
em.on('i18n:update', handlerUpdate);
em.on('i18n:locale', handlerLocale);
obj.addMessages({ en: { msg1: 'Msg 1', msg2: 'Msg 2' } });
obj.setLocale('it');
expect(handlerAdd).toBeCalledTimes(1);
expect(handlerUpdate).toBeCalledTimes(1);
expect(handlerLocale).toBeCalledTimes(1);
});
});
});
Loading…
Cancel
Save