TranslationMode progress

This commit is contained in:
Andrea Vos 2022-07-30 00:57:51 +02:00
parent 4ff88bbcf6
commit d285e04272
10 changed files with 161 additions and 40 deletions

View File

@ -9,11 +9,11 @@
</p>
</div>
<div class="modal-body" v-if="message">
<p class="py-5 text-center" v-html="message"></p>
<p :class="[margin ? 'py-5 text-center' : '']" v-html="message"></p>
</div>
<div class="modal-body" v-if="value !== undefined">
<ListInput v-if="Array.isArray(value)" v-model="value" v-slot="s">
<textarea v-model="s.val" class="form-control" rows="5"></textarea>
<textarea v-model="s.val" class="form-control" rows="5" @keyup="s.update(s.val)" @update="s.update(s.val)"></textarea>
</ListInput>
<textarea v-else v-model="value" class="form-control" rows="5"></textarea>
</div>
@ -53,6 +53,7 @@
icon: undefined,
header: undefined,
message: undefined,
margin: true,
color: null,
value: undefined,
size: undefined,
@ -88,7 +89,8 @@
this.icon = message.icon || (choice ? 'map-marker-question' : null);
this.header = message.header;
this.message = message.message || (choice ? this.$t('confirm.header') : null);
this.size = size;
this.margin = message.margin ?? true;
this.size = message.size ?? size;
this.color = color;
this.value = value;
this.shown = true;
@ -118,6 +120,7 @@
this.icon = undefined;
this.header = undefined;
this.message = undefined;
this.margin = true;
this.color = null;
this.value = undefined;
this.size = undefined;

View File

@ -3,7 +3,7 @@
</template>
<script>
import t from '../src/translator';
import translator from '../src/translator';
import {mapState} from "vuex";
export default {
@ -23,12 +23,12 @@
'translationChanges',
]),
modified() {
return this.translationChanges.hasOwnProperty(this.key);
return this.translationMode && this.translationChanges.hasOwnProperty(this.key);
},
txt() {
return this.modified
? t(this.translationChanges[this.key], this.params || {}, !this.silent, false)
: t(this.key, this.params || {}, !this.silent);
? translator.applyParams(this.translationChanges[this.key], this.params || {})
: translator.translate(this.key, this.params || {}, !this.silent);
}
},
methods: {
@ -38,14 +38,30 @@
e.preventDefault();
e.stopPropagation();
const base = translator.get(this.key, false, true);
const newValue = await this.$editor(
this.modified ? this.translationChanges[this.key] : t(this.key),
{ icon: 'language', header: this.key },
this.modified
? this.translationChanges[this.key]
: translator.get(this.key),
{
icon: 'language',
header: this.key,
message: base
? ('<div class="small alert alert-info">'
+ (Array.isArray(base)
? `<ul>${base.map(el => '<li>' + el + '</el>')}</ul>`
: base)
+ '</div>')
: undefined,
margin: false,
},
'info'
);
if (newValue !== undefined) {
this.$store.commit('translate', {key: this.key, newValue});
this.$cookies.set('translations', this.$store.state.translationChanges);
}
},
},

View File

@ -1,19 +1,24 @@
<template>
<div v-if="$isGranted()" class="scroll-btn d-print-none d-flex align-items-center">
<div v-if="$isGranted('translations')" class="scroll-btn d-print-none d-flex align-items-center">
<template v-if="translationMode">
<div class="bg-info rounded m-1 px-3 py-1 d-flex justify-content-center align-items-center">
<button class="btn btn-info btn-sm m-1 px-3 py-1 d-flex justify-content-center align-items-center" @click="showChanges">
<small>Changes: {{ changesCount }}</small>
</div>
</button>
<div v-if="changesCount" @click.prevent="commitChanges">
<SquareButton link="#" colour="success" aria-label="Commit changes">
<Icon v="check-circle"/>
</SquareButton>
</div>
<div @click.prevent="revertChanges">
<div v-if="changesCount" @click.prevent="revertChanges">
<SquareButton link="#" colour="danger" aria-label="Revert changes">
<Icon v="times-circle"/>
</SquareButton>
</div>
<div @click.prevent="pause">
<SquareButton link="#" colour="info" aria-label="Pause translation mode">
<Icon v="pause-circle"/>
</SquareButton>
</div>
</template>
<div v-else @click.prevent="startTranslating">
<SquareButton link="#" colour="info" aria-label="Translation Mode">
@ -25,6 +30,7 @@
<script>
import {mapState} from "vuex";
import Suml from 'suml';
export default {
data() {
@ -37,13 +43,34 @@
},
async commitChanges() {
await this.$confirm(`Do you want to commit ${this.changesCount} changes?`, 'success');
const response = await this.$post(`/translations/propose`, {
changes: this.translationChanges,
});
this.$store.commit('translationCommit');
this.$cookies.remove('translations');
setTimeout(
() => this.$alert({header: 'Your translation proposals were saved', message: 'Thank you for contributing!'}, 'success'),
500,
)
},
async revertChanges() {
if (this.changesCount) {
await this.$confirm(`Do you want to revert ${this.changesCount} changes?`, 'danger');
}
this.$store.commit('translationAbort');
this.$cookies.remove('translations');
},
async showChanges() {
await this.$alert({
header: 'Changes overview',
message: '<pre>' + new Suml().dump(this.translationChanges) + '</pre>',
margin: false,
size: 'lg',
}, 'info');
},
async pause() {
this.$store.commit('translationPause');
},
},
computed: {

View File

@ -0,0 +1,18 @@
-- Up
CREATE TABLE translations (
id TEXT NOT NULL PRIMARY KEY,
locale TEXT NOT NULL,
tKey TEXT NOT NULL,
tValue TEXT NOT NULL,
status INTEGER NOT NULL,
author_id TEXT NULL REFERENCES users(id)
);
CREATE INDEX "translations_locale" ON "translations" ("locale");
CREATE INDEX "translations_key" ON "translations" ("key");
CREATE INDEX "translations_status" ON "translations" ("status");
-- Down
DROP TABLE translations;

View File

@ -1,5 +1,5 @@
import Vue from 'vue'
import t from '../src/translator';
import translator from '../src/translator';
import config from '../data/config.suml';
import {buildDict} from "../src/helpers";
import {DateTime} from "luxon";
@ -10,11 +10,11 @@ export default ({ app, store }) => {
Vue.prototype.$base = process.env.BASE_URL;
Vue.prototype.$t = t;
Vue.prototype.$te = key => t(key, {}, false) !== undefined;
Vue.prototype.$t = (key, params = {}, warn = true) => translator.translate(key, params, warn);
Vue.prototype.$te = (key) => translator.has(key);
Vue.prototype.$translateForPronoun = (str, pronoun) =>
pronoun.format(
t(`flags.${str.replace(/ /g, '_').replace(/'/g, `*`)}`, {}, false) || str
translator.translate(`flags.${str.replace(/ /g, '_').replace(/'/g, `*`)}`, {}, false) || str
);
Vue.prototype.config = config;
@ -31,6 +31,7 @@ export default ({ app, store }) => {
});
store.commit('setSpelling', app.$cookies.get('spelling'));
store.commit('restoreTranslations', app.$cookies.get('translations'))
Vue.prototype.buildImageUrl = (imageId, size) => `${process.env.CLOUDFRONT}/images/${imageId}-${size}.png`

View File

@ -93,14 +93,11 @@ app.use(require('./routes/grantOverrides').default);
router.use(grant.express()(require('./social').config));
app.use(require('./routes/home').default);
app.use(require('./routes/banner').default);
app.use(require('./routes/user').default);
app.use(require('./routes/profile').default);
app.use(require('./routes/admin').default);
app.use(require('./routes/mfa').default);
app.use(require('./routes/pronouns').default);
app.use(require('./routes/sources').default);
app.use(require('./routes/nouns').default);
@ -109,10 +106,10 @@ app.use(require('./routes/terms').default);
app.use(require('./routes/pronounce').default);
app.use(require('./routes/census').default);
app.use(require('./routes/names').default);
app.use(require('./routes/images').default);
app.use(require('./routes/blog').default);
app.use(require('./routes/calendar').default);
app.use(require('./routes/translations').default);
app.use(function (err, req, res, next) {
console.error(err.stack);

View File

@ -0,0 +1,28 @@
import { Router } from 'express';
import SQL from 'sql-template-strings';
import {ulid} from "ulid";
import { handleErrorAsync } from "../../src/helpers";
const router = Router();
router.post('/translations/propose', handleErrorAsync(async (req, res) => {
if (!req.isGranted('translations')) {
return res.status(401).json({error: 'Unauthorised'});
}
for (let tKey in req.body.changes) {
if (!req.body.changes.hasOwnProperty(tKey)) { continue; }
// TODO single insert
await req.db.get(SQL`INSERT INTO translations (id, locale, tKey, tValue, status, author_id) VALUES (
${ulid()}, ${global.config.locale},
${tKey}, ${JSON.stringify(req.body.changes[tKey])},
0, ${req.user ? req.user.id : null}
)`)
}
// TODO email
return res.json('OK');
}));
export default router;

View File

@ -1,4 +1,4 @@
import t from './translator';
import translator from './translator';
export const contact = {
all: {
@ -75,8 +75,8 @@ const supportLinks = {
bank: {
icon: 'money-check-alt',
url: 'https://bunq.me/PronounsPage',
headline: t('support.bankAccount'),
// tooltip: t('support.bankAccountOwner'),
headline: translator.translate('support.bankAccount'),
// tooltip: translator.translate('support.bankAccountOwner'),
},
kofi: {
icon: 'coffee',

View File

@ -1,8 +1,21 @@
import translations from '../data/translations.suml';
import baseTranslations from '../locale/_base/translations.suml';
export default (key, params = {}, warn = true, translate = true) => {
let value = translations;
if (translate) {
class Translator {
constructor(translations, baseTranslations) {
this.translations = translations;
this.baseTranslations = baseTranslations;
}
translate(key, params = {}, warn = true) {
return this.applyParams(
this.get(key, warn),
params,
);
}
get(key, warn = true, base = false) {
let value = base ? this.baseTranslations : this.translations;
for (let part of key.split('.')) {
value = value[part];
if (value === undefined) {
@ -12,17 +25,23 @@ export default (key, params = {}, warn = true, translate = true) => {
return undefined;
}
}
} else {
value = key;
return value;
}
for (let k in params) {
if (params.hasOwnProperty(k)) {
value = Array.isArray(value)
? value.map(v => v.replace(new RegExp('%' + k + '%', 'g'), params[k]))
: value.replace(new RegExp('%' + k + '%', 'g'), params[k]);
has(key) {
return this.get(key, false) !== undefined;
}
applyParams (value, params = {}) {
for (let k in params) {
if (params.hasOwnProperty(k)) {
value = Array.isArray(value)
? value.map(v => v.replace(new RegExp('%' + k + '%', 'g'), params[k]))
: value.replace(new RegExp('%' + k + '%', 'g'), params[k]);
}
}
return value;
}
return value;
}
export default new Translator(translations, baseTranslations);

View File

@ -1,5 +1,5 @@
import jwt from 'jsonwebtoken';
import t from '../src/translator';
import translator from '../src/translator';
import {buildDict} from "../src/helpers";
export const state = () => ({
@ -57,17 +57,20 @@ export const mutations = {
},
translationInit(state) {
state.translationMode = true;
state.translationChanges = {};
},
translationCommit(state) {
alert('not implemented!')
state.translationMode = false;
state.translationChanges = {};
},
translationAbort(state) {
state.translationMode = false;
state.translationChanges = {};
},
translationPause(state) {
state.translationMode = false;
},
translate(state, {key, newValue}) {
if (newValue !== t(key)) {
if (newValue !== translator.get(key)) {
const translationChanges = {...state.translationChanges};
translationChanges[key] = newValue;
state.translationChanges = translationChanges;
@ -82,4 +85,13 @@ export const mutations = {
}, state.translationChanges);
}
},
restoreTranslations(state, translations) {
if (translations) {
state.translationMode = true;
state.translationChanges = translations;
} else {
state.translationMode = false;
state.translationChanges = {};
}
},
}