Added new function to add cumstom translation strings on Admin panel

This commit is contained in:
Yevhenii Pertiaka 2023-08-17 21:54:14 +09:00
parent 4153fe7d0d
commit b1525d4221
10 changed files with 599 additions and 2 deletions

View file

@ -20,6 +20,10 @@ template(name="settingHeaderBar")
i.fa(class="fa-paperclip")
span {{_ 'attachments'}}
a.setting-header-btn.informations(href="{{pathFor 'translation'}}")
i.fa(class="fa-font")
span {{_ 'translation'}}
a.setting-header-btn.informations(href="{{pathFor 'information'}}")
i.fa(class="fa-info-circle")
span {{_ 'info'}}

View file

@ -0,0 +1,67 @@
.main-body {
overflow: scroll;
}
table {
color: #000;
}
table td,
table th {
border: 1px solid #d2d0d0;
text-align: left;
padding: 8px;
}
table tr:nth-child(even) {
background-color: #ddd;
}
.ext-box {
display: flex;
flex-direction: row;
height: 34px;
}
.ext-box .ext-box-left {
display: flex;
width: 100%;
gap: 10px;
}
.ext-box span {
vertical-align: center;
line-height: 34px;
}
.ext-box input,
.ext-box button {
padding: 0;
}
.ext-box button {
min-width: 90px;
}
.content-wrapper {
margin-top: 10px;
}
.buttonsContainer {
display: flex;
}
.buttonsContainer input {
margin: 0;
}
.buttonsContainer div {
margin: auto;
}
.more-settings-translation {
margin-left: 10px;
}
#cancelBtn {
margin-left: 5% !important;
background: #ffa500;
color: #fff;
}
#deleteAction {
margin-left: 5% !important;
}
p.js-translation-language {
font-weight: bold;
color: #000;
}
p.js-translation-text {
font-weight: bold;
color: #000;
}

View file

@ -0,0 +1,105 @@
template(name="translation")
.setting-content
unless currentUser.isAdmin
| {{_ 'error-notAuthorized'}}
else
.content-title.ext-box
.ext-box-left
if loading.get
+spinner
else if translationSetting.get
span
i.fa.fa-font
unless isMiniScreen
| {{_ 'translation'}}
input#searchTranslationInput(placeholder="{{_ 'search'}}")
button#searchTranslationButton
i.fa.fa-search
| {{_ 'search'}}
.ext-box-right
span {{#unless isMiniScreen}}{{_ 'translation-number'}}{{/unless}} #{translationNumber}
.content-body
.side-menu
ul
li.active
a.js-translation-menu(data-id="translation-setting")
i.fa.fa-font
| {{_ 'translation'}}
.main-body
if loading.get
+spinner
else if translationSetting.get
+translationGeneral
template(name="translationGeneral")
table
tbody
tr
th {{_ 'language'}}
th {{_ 'text'}}
th {{_ 'translation-text'}}
th
+newTranslationRow
each translation in translationList
+translationRow(translationId=translation._id)
template(name="newTranslationRow")
a.new-translation
i.fa.fa-plus-square
| {{_ 'new'}}
template(name="translationRow")
tr
td {{translationData.language}}
td {{translationData.text}}
td {{translationData.translationText}}
td
a.edit-translation
i.fa.fa-edit
| {{_ 'edit'}}
a.more-settings-translation
i.fa.fa-ellipsis-h
template(name="editTranslationPopup")
form
label
| {{_ 'language'}}
input.js-translation-language(type="text" value=translation.language required readonly)
label
| {{_ 'text'}}
input.js-translation-text(type="text" value=translation.text required readonly)
label
| {{_ 'translation-text'}}
input.js-translation-translation-text(type="text" value=translation.translationText)
hr
div.buttonsContainer
input.primary.wide(type="submit" value="{{_ 'save'}}")
template(name="newTranslationPopup")
form
label
| {{_ 'language'}}
input.js-translation-language(type="text" value="en" required)
label
| {{_ 'text'}}
span.error.hide.text-taken
| {{_ 'error-text-taken'}}
input.js-translation-text(type="text" value="" required)
label
| {{_ 'translation-text'}}
input.js-translation-translation-text(type="text" value="")
hr
div.buttonsContainer
input.primary.wide(type="submit" value="{{_ 'save'}}")
template(name="settingsTranslationPopup")
ul.pop-over-list
li
form
label
| {{_ 'delete-translation-confirm-popup'}}
br
label.hide.orgId(type="text" value=org._id)
div.buttonsContainer
input#deleteButton.card-details-red.right.wide(type="button" value="{{_ 'delete'}}")

View file

@ -0,0 +1,214 @@
import { ReactiveCache } from '/imports/reactiveCache';
const translationsPerPage = 25;
BlazeComponent.extendComponent({
mixins() {
return [Mixins.InfiniteScrolling];
},
onCreated() {
this.error = new ReactiveVar('');
this.loading = new ReactiveVar(false);
this.translationSetting = new ReactiveVar(true);
this.findTranslationsOptions = new ReactiveVar({});
this.numberTranslations = new ReactiveVar(0);
this.page = new ReactiveVar(1);
this.loadNextPageLocked = false;
this.callFirstWith(null, 'resetNextPeak');
this.autorun(() => {
const limitTranslations = this.page.get() * translationsPerPage;
this.subscribe('translation', this.findTranslationsOptions.get(), 0, () => {
this.loadNextPageLocked = false;
const nextPeakBefore = this.callFirstWith(null, 'getNextPeak');
this.calculateNextPeak();
const nextPeakAfter = this.callFirstWith(null, 'getNextPeak');
if (nextPeakBefore === nextPeakAfter) {
this.callFirstWith(null, 'resetNextPeak');
}
});
});
},
events() {
return [
{
'click #searchTranslationButton'() {
this.filterTranslation();
},
'keydown #searchTranslationInput'(event) {
if (event.keyCode === 13 && !event.shiftKey) {
this.filterTranslation();
}
},
'click #newTranslationButton'() {
Popup.open('newTranslation');
},
'click a.js-translation-menu': this.switchMenu,
},
];
},
filterTranslation() {
const value = $('#searchTranslationInput').first().val();
if (value === '') {
this.findTranslationsOptions.set({});
} else {
const regex = new RegExp(value, 'i');
this.findTranslationsOptions.set({
$or: [
{ language: regex },
{ text: regex },
{ translationText: regex },
],
});
}
},
loadNextPage() {
if (this.loadNextPageLocked === false) {
this.page.set(this.page.get() + 1);
this.loadNextPageLocked = true;
}
},
calculateNextPeak() {
const element = this.find('.main-body');
if (element) {
const altitude = element.scrollHeight;
this.callFirstWith(this, 'setNextPeak', altitude);
}
},
reachNextPeak() {
this.loadNextPage();
},
setError(error) {
this.error.set(error);
},
setLoading(w) {
this.loading.set(w);
},
translationList() {
const translations = ReactiveCache.getTranslations(this.findTranslationsOptions.get(), {
sort: { modifiedAt: 1 },
fields: { _id: true },
});
this.numberTranslations.set(translations.length);
return translations;
},
translationNumber() {
return this.numberTranslations.get();
},
switchMenu(event) {
const target = $(event.target);
if (!target.hasClass('active')) {
$('.side-menu li.active').removeClass('active');
target.parent().addClass('active');
const targetID = target.data('id');
this.translationSetting.set('translation-setting' === targetID);
}
},
}).register('translation');
Template.translationRow.helpers({
translationData() {
return ReactiveCache.getTranslation(this.translationId);
},
});
Template.editTranslationPopup.helpers({
translation() {
return ReactiveCache.getTranslation(this.translationId);
},
errorMessage() {
return Template.instance().errorMessage.get();
},
});
Template.newTranslationPopup.onCreated(function () {
this.errorMessage = new ReactiveVar('');
});
Template.newTranslationPopup.helpers({
translation() {
return ReactiveCache.getTranslation(this.translationId);
},
errorMessage() {
return Template.instance().errorMessage.get();
},
});
BlazeComponent.extendComponent({
onCreated() {},
translation() {
return ReactiveCache.getTranslation(this.translationId);
},
events() {
return [
{
'click a.edit-translation': Popup.open('editTranslation'),
'click a.more-settings-translation': Popup.open('settingsTranslation'),
},
];
},
}).register('translationRow');
BlazeComponent.extendComponent({
events() {
return [
{
'click a.new-translation': Popup.open('newTranslation'),
},
];
},
}).register('newTranslationRow');
Template.editTranslationPopup.events({
submit(event, templateInstance) {
event.preventDefault();
const translation = ReactiveCache.getTranslation(this.translationId);
const translationText = templateInstance.find('.js-translation-translation-text').value.trim();
Meteor.call(
'setTranslationText',
translation,
translationText
);
Popup.back();
},
});
Template.newTranslationPopup.events({
submit(event, templateInstance) {
event.preventDefault();
const language = templateInstance.find('.js-translation-language').value.trim();
const text = templateInstance.find('.js-translation-text').value.trim();
const translationText = templateInstance.find('.js-translation-translation-text').value.trim();
Meteor.call(
'setCreateTranslation',
language,
text,
translationText,
function(error) {
const textMessageElement = templateInstance.$('.text-taken');
if (error) {
const errorElement = error.error;
if (errorElement === 'text-already-taken') {
textMessageElement.show();
}
} else {
textMessageElement.hide();
Popup.back();
}
},
);
Popup.back();
},
});
Template.settingsTranslationPopup.events({
'click #deleteButton'(event) {
event.preventDefault();
Translation.remove(this.translationId);
Popup.back();
}
});

View file

@ -381,6 +381,30 @@ FlowRouter.route('/attachments', {
},
});
FlowRouter.route('/translation', {
name: 'translation',
triggersEnter: [
AccountsTemplates.ensureSignedIn,
() => {
Session.set('currentBoard', null);
Session.set('currentList', null);
Session.set('currentCard', null);
Session.set('popupCardId', null);
Session.set('popupCardBoardId', null);
Filter.reset();
Session.set('sortBy', '');
EscapeActions.executeAll();
},
],
action() {
BlazeLayout.render('defaultLayout', {
headerBar: 'settingHeaderBar',
content: 'translation',
});
},
});
FlowRouter.notFound = {
action() {
BlazeLayout.render('defaultLayout', { content: 'notFound' });

View file

@ -1229,5 +1229,10 @@
"allowed-avatar-filetypes": "Allowed avatar filetypes:",
"invalid-file": "If filename is invalid, upload or rename is cancelled.",
"preview-pdf-not-supported": "Your device does not support previewing PDF. Try downloading instead.",
"drag-board": "Drag board"
"drag-board": "Drag board",
"translation-number": "The number of custom strings is: ",
"delete-translation-confirm-popup": "Are you sure you want to delete this custom string? There is no undo.",
"translation": "Translation",
"text": "Text",
"translation-text": "Translation text"
}

View file

@ -1,5 +1,6 @@
import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
import Translation from '/models/translation';
import i18next from 'i18next';
import sprintf from 'i18next-sprintf-postprocessor';
import languages from './languages';
@ -45,7 +46,18 @@ export const TAPi18n = {
},
async loadLanguage(language) {
if (language in languages && 'load' in languages[language]) {
const data = await languages[language].load();
let data = await languages[language].load();
let custom_translations = [];
if (Meteor.isServer) {
custom_translations = Translation.find({language: language}, {fields: { text: true, translationText: true }}).fetch();
} else if (Meteor.isClient) {
await Meteor.subscribe('translation', {language: language}, 0);
custom_translations = ReactiveCache.getTranslations({language: language}, {fields: { text: true, translationText: true }});
}
if (custom_translations && custom_translations.length > 0) {
data = custom_translations.reduce((acc, cur) => (acc[cur.text]=cur.translationText, acc), data);
}
this.i18n.addResourceBundle(language, DEFAULT_NAMESPACE, data);
} else {
throw new Error(`Language ${language} is not supported`);

View file

@ -1261,6 +1261,17 @@ ReactiveCache = {
}
return ret;
},
getTranslations(selector, options, getQuery) {
let ret = Translation.find(selector, options);
if (getQuery !== true) {
ret = ret.fetch();
}
return ret;
},
getTranslation(idOrFirstObjectSelector, options) {
const ret = Translation.findOne(idOrFirstObjectSelector, options);
return ret;
}
}
// Client side little MiniMongo DB "Index"

128
models/translation.js Normal file
View file

@ -0,0 +1,128 @@
Translation = new Mongo.Collection('translation');
/**
* A Organization User in wekan
*/
Translation.attachSchema(
new SimpleSchema({
language: {
/**
* the language
*/
type: String,
max: 5,
},
text: {
/**
* the text
*/
type: String,
},
translationText: {
/**
* the translation text
*/
type: String,
},
createdAt: {
/**
* creation date of the organization user
*/
type: Date,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert) {
return new Date();
} else if (this.isUpsert) {
return { $setOnInsert: new Date() };
} else {
this.unset();
}
},
},
modifiedAt: {
type: Date,
denyUpdate: false,
// eslint-disable-next-line consistent-return
autoValue() {
if (this.isInsert || this.isUpsert || this.isUpdate) {
return new Date();
} else {
this.unset();
}
},
},
}),
);
if (Meteor.isServer) {
Translation.allow({
insert(userId, doc) {
const user = ReactiveCache.getUser(userId) || ReactiveCache.getCurrentUser();
if (user?.isAdmin)
return true;
if (!user) {
return false;
}
return doc._id === userId;
},
update(userId, doc) {
const user = ReactiveCache.getUser(userId) || ReactiveCache.getCurrentUser();
if (user?.isAdmin)
return true;
if (!user) {
return false;
}
return doc._id === userId;
},
remove(userId, doc) {
const user = ReactiveCache.getUser(userId) || ReactiveCache.getCurrentUser();
if (user?.isAdmin)
return true;
if (!user) {
return false;
}
return doc._id === userId;
},
fetch: [],
});
Meteor.methods({
setCreateTranslation(
language,
text,
translationText,
) {
check(language, String);
check(text, String);
check(translationText, String);
const nTexts = ReactiveCache.getTranslations({ language, text }).length;
if (nTexts > 0) {
throw new Meteor.Error('text-already-taken');
} else {
Translation.insert({
language,
text,
translationText,
});
}
},
setTranslationText(translation, translationText) {
check(translation, Object);
check(translationText, String);
Translation.update(translation, {
$set: { translationText: translationText },
});
},
});
}
if (Meteor.isServer) {
// Index for Organization User.
Meteor.startup(() => {
Translation._collection.createIndex({ modifiedAt: -1 });
});
}
export default Translation;

View file

@ -0,0 +1,27 @@
import { ReactiveCache } from '/imports/reactiveCache';
Meteor.publish('translation', function(query, limit) {
check(query, Match.OneOf(Object, null));
check(limit, Number);
let ret = [];
const user = ReactiveCache.getCurrentUser();
if (user && user.isAdmin) {
ret = ReactiveCache.getTranslations(query,
{
limit,
sort: { modifiedAt: -1 },
fields: {
language: 1,
text: 1,
translationText: 1,
createdAt: 1,
}
},
true,
);
}
return ret;
});