Improve i18n editor.
- Add a one-click `download JSON` button. - Add a `New language` button. Add secondary editor InLang (remote service) to docs. Closes #1266, #1267.
This commit is contained in:
parent
71a9138b23
commit
7db3d7da0f
4 changed files with 98 additions and 45 deletions
|
@ -17,9 +17,17 @@ To customize an existing language or to load a new language, put one or more `.j
|
||||||
|
|
||||||
## Contributing a new language
|
## Contributing a new language
|
||||||
|
|
||||||
|
### Using the basic editor
|
||||||
|
|
||||||
- Visit [https://listmonk.app/i18n](https://listmonk.app/i18n)
|
- Visit [https://listmonk.app/i18n](https://listmonk.app/i18n)
|
||||||
- To make changes to an existing language, use the "Load language" option on the top right.
|
- Click on `Createa new language`, or to make changes to an existing language, use `Load language`.
|
||||||
- To create a new language, use the "Load language" option on the top right and select "Default".
|
- Translate the text in the text fields on the UI.
|
||||||
- Translate the text in the input fields on the UI.
|
- Once done, use the `Download raw JSON` to download the language file.
|
||||||
- Once done, use the `Switch to raw JSON` and copy the JSON data and save it to a file named `xx.json`, where `xx` is the two letter code of the language.
|
- Send a pull request to add the file to the [i18n directory on the GitHub repo](https://github.com/knadh/listmonk/tree/master/i18n).
|
||||||
- Send a pull request to add the file to the [i18n directory on the GitHub repo](https://github.com/knadh/listmonk/tree/master/i18n). If you are not familiar with pull requests, share the file by creating a new "issue" (comment) on the [GitHub issues page](https://github.com/knadh/listmonk/issues)
|
|
||||||
|
### Using InLang (external service)
|
||||||
|
|
||||||
|
- Visit [https://inlang.com/editor/github.com/knadh/listmonk](https://inlang.com/editor/github.com/knadh/listmonk)
|
||||||
|
- To make changes and push them, you need to log in to GitHub using OAuth and fork the project from the UI.
|
||||||
|
- Translate the text in the input fields on the UI. You can use the filters to see only the necessary translations.
|
||||||
|
- Once you're done, push the changes from the UI and click on "Open a pull request." This will take you to GitHub, where you can write a PR message.
|
||||||
|
|
|
@ -13,7 +13,11 @@
|
||||||
<h1 class="title">{{ values["_.name"] }}</h1>
|
<h1 class="title">{{ values["_.name"] }}</h1>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<div class="import block">
|
<div class="import block">
|
||||||
<a v-if="!isRawVisible" href="#" @click.prevent="onToggleRaw">Switch to raw JSON</a>
|
<a href="#" @click.prevent="onToggleRaw">
|
||||||
|
<template v-if="!isRawVisible">Switch to raw JSON</template>
|
||||||
|
<template v-else>Switch to editor</template>
|
||||||
|
</a>
|
||||||
|
<a href="#" @click.prevent="onDownloadJSON">Download raw JSON</a>
|
||||||
<a v-else href="#" @click.prevent="onToggleRaw">Switch to editor</a>
|
<a v-else href="#" @click.prevent="onToggleRaw">Switch to editor</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -33,7 +37,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="selector block">
|
<div class="selector block">
|
||||||
Load language
|
Load existing language
|
||||||
<select v-model="loadLang" @change="onLoadLanguage">
|
<select v-model="loadLang" @change="onLoadLanguage">
|
||||||
<option value="en">Default (en)</option>
|
<option value="en">Default (en)</option>
|
||||||
<option value="ca"> Català (ca) </option>
|
<option value="ca"> Català (ca) </option>
|
||||||
|
@ -60,6 +64,9 @@
|
||||||
<option value="zh-CN"> 简体中文 (zh-CN) </option>
|
<option value="zh-CN"> 简体中文 (zh-CN) </option>
|
||||||
<option value="zh-TW"> 繁體中文(zh-TW) </option>
|
<option value="zh-TW"> 繁體中文(zh-TW) </option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
|
||||||
|
<a href="#" @click.prevent="onNewLang">+ Create new language</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
@ -23,41 +23,41 @@ var app = new Vue({
|
||||||
|
|
||||||
loadBaseLang(url) {
|
loadBaseLang(url) {
|
||||||
return fetch(url).then(response => response.json()).then(data => {
|
return fetch(url).then(response => response.json()).then(data => {
|
||||||
// Retain the base values.
|
// Retain the base values.
|
||||||
Object.assign(this.base, data);
|
Object.assign(this.base, data);
|
||||||
|
|
||||||
// Get the sorted keys from the language map.
|
// Get the sorted keys from the language map.
|
||||||
const keys = [];
|
const keys = [];
|
||||||
const visibleKeys = {};
|
const visibleKeys = {};
|
||||||
let head = null;
|
let head = null;
|
||||||
Object.entries(this.base).sort((a, b) => a[0].localeCompare(b[0])).forEach((v) => {
|
Object.entries(this.base).sort((a, b) => a[0].localeCompare(b[0])).forEach((v) => {
|
||||||
const h = v[0].split('.')[0];
|
const h = v[0].split('.')[0];
|
||||||
keys.push({
|
keys.push({
|
||||||
"key": v[0],
|
"key": v[0],
|
||||||
"head": (head !== h ? h : null) // eg: campaigns on `campaigns.something.else`
|
"head": (head !== h ? h : null) // eg: campaigns on `campaigns.something.else`
|
||||||
});
|
|
||||||
|
|
||||||
visibleKeys[v[0]] = true;
|
|
||||||
head = h;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.keys = keys;
|
visibleKeys[v[0]] = true;
|
||||||
this.visibleKeys = visibleKeys;
|
head = h;
|
||||||
this.values = { ...this.base };
|
});
|
||||||
|
|
||||||
// Is there cached localStorage data?
|
this.keys = keys;
|
||||||
if(localStorage.data) {
|
this.visibleKeys = visibleKeys;
|
||||||
try {
|
this.values = { ...this.base };
|
||||||
this.loadData(JSON.parse(localStorage.data));
|
|
||||||
} catch(e) {
|
// Is there cached localStorage data?
|
||||||
console.log("Bad JSON in localStorage: " + e.toString());
|
if (localStorage.data) {
|
||||||
}
|
try {
|
||||||
return;
|
this.populateData(JSON.parse(localStorage.data));
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Bad JSON in localStorage: " + e.toString());
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
loadData(data) {
|
populateData(data) {
|
||||||
// Filter out all keys from data except for the base ones
|
// Filter out all keys from data except for the base ones
|
||||||
// in the base language.
|
// in the base language.
|
||||||
const vals = this.keys.reduce((a, key) => {
|
const vals = this.keys.reduce((a, key) => {
|
||||||
|
@ -69,6 +69,15 @@ var app = new Vue({
|
||||||
this.saveData();
|
this.saveData();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
loadLanguage(lang) {
|
||||||
|
return fetch(BASEURL + lang + ".json").then(response => response.json()).then(data => {
|
||||||
|
this.populateData(data);
|
||||||
|
}).catch((e) => {
|
||||||
|
console.log(e);
|
||||||
|
alert("error fetching file: " + e.toString());
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
saveData() {
|
saveData() {
|
||||||
localStorage.data = JSON.stringify(this.values);
|
localStorage.data = JSON.stringify(this.values);
|
||||||
},
|
},
|
||||||
|
@ -87,7 +96,7 @@ var app = new Vue({
|
||||||
this.rawData = JSON.stringify(this.values, Object.keys(this.values).sort(), 4);
|
this.rawData = JSON.stringify(this.values, Object.keys(this.values).sort(), 4);
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
this.loadData(JSON.parse(this.rawData));
|
this.populateData(JSON.parse(this.rawData));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert("error parsing JSON: " + e.toString());
|
alert("error parsing JSON: " + e.toString());
|
||||||
return false;
|
return false;
|
||||||
|
@ -98,16 +107,37 @@ var app = new Vue({
|
||||||
},
|
},
|
||||||
|
|
||||||
onLoadLanguage() {
|
onLoadLanguage() {
|
||||||
if(!confirm("Loading this language will overwrite your local changes. Continue?")) {
|
if (!confirm("Loading this language will overwrite your local changes. Continue?")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch(BASEURL + this.loadLang + ".json").then(response => response.json()).then(data => {
|
this.loadLanguage(this.loadLang);
|
||||||
this.loadData(data);
|
},
|
||||||
}).catch((e) => {
|
|
||||||
console.log(e);
|
onNewLang() {
|
||||||
alert("error fetching file: " + e.toString());
|
if (!confirm("Creating a new language will overwrite your local changes. Continue?")) {
|
||||||
});
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = { ...this.base };
|
||||||
|
data["_.code"] = "iso-code-here"
|
||||||
|
data["_.name"] = "New language"
|
||||||
|
this.populateData(data);
|
||||||
|
},
|
||||||
|
|
||||||
|
onDownloadJSON() {
|
||||||
|
// Create a Blob using the content, mimeType, and optional encoding
|
||||||
|
const blob = new Blob([JSON.stringify(this.values, Object.keys(this.values).sort(), 4)], { type: "" });
|
||||||
|
|
||||||
|
// Create an anchor element with a download attribute
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.download = `${this.values["_.code"]}.json`;
|
||||||
|
link.href = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
// Append the link to the DOM, click it to start the download, and remove it
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -128,10 +158,10 @@ var app = new Vue({
|
||||||
if (v === "pending") {
|
if (v === "pending") {
|
||||||
visible = !this.isDone(k.key);
|
visible = !this.isDone(k.key);
|
||||||
} else if (v === "complete") {
|
} else if (v === "complete") {
|
||||||
visible = this.isDone(k.key);
|
visible = this.isDone(k.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(visible) {
|
if (visible) {
|
||||||
visibleKeys[k.key] = true;
|
visibleKeys[k.key] = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -145,7 +175,7 @@ var app = new Vue({
|
||||||
let n = 0;
|
let n = 0;
|
||||||
|
|
||||||
this.keys.forEach(k => {
|
this.keys.forEach(k => {
|
||||||
if(this.values[k.key] !== this.base[k.key]) {
|
if (this.values[k.key] !== this.base[k.key]) {
|
||||||
n++;
|
n++;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -12,6 +12,10 @@ h1, h2, h3, h4, h5 {
|
||||||
margin: 0 0 15px 0;
|
margin: 0 0 15px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #0055d4;
|
||||||
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
padding: 30px;
|
padding: 30px;
|
||||||
}
|
}
|
||||||
|
@ -20,6 +24,10 @@ h1, h2, h3, h4, h5 {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
.header a {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
.header .controls {
|
.header .controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue