mirror of https://github.com/artf/grapesjs.git
committed by
GitHub
55 changed files with 3947 additions and 1261 deletions
File diff suppressed because it is too large
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
@ -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 |
|||
@ -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)) |
|||
|
|||
@ -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> |
|||
File diff suppressed because it is too large
@ -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); |
|||
@ -1,5 +1,3 @@ |
|||
export default { |
|||
devices: [], |
|||
|
|||
deviceLabel: 'Device' |
|||
devices: [] |
|||
}; |
|||
|
|||
@ -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 |
|||
} |
|||
}; |
|||
@ -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); |
|||
} |
|||
}; |
|||
}; |
|||
@ -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' |
|||
} |
|||
} |
|||
} |
|||
} |
|||
}; |
|||
@ -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' |
|||
} |
|||
} |
|||
} |
|||
} |
|||
}; |
|||
@ -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: {} |
|||
} |
|||
} |
|||
}; |
|||
@ -0,0 +1,5 @@ |
|||
export default { |
|||
en: { |
|||
hello: 'Hello' |
|||
} |
|||
}; |
|||
@ -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…
Reference in new issue