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

View File

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

View File

@ -1,19 +1,24 @@
<template> <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"> <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> <small>Changes: {{ changesCount }}</small>
</div> </button>
<div v-if="changesCount" @click.prevent="commitChanges"> <div v-if="changesCount" @click.prevent="commitChanges">
<SquareButton link="#" colour="success" aria-label="Commit changes"> <SquareButton link="#" colour="success" aria-label="Commit changes">
<Icon v="check-circle"/> <Icon v="check-circle"/>
</SquareButton> </SquareButton>
</div> </div>
<div @click.prevent="revertChanges"> <div v-if="changesCount" @click.prevent="revertChanges">
<SquareButton link="#" colour="danger" aria-label="Revert changes"> <SquareButton link="#" colour="danger" aria-label="Revert changes">
<Icon v="times-circle"/> <Icon v="times-circle"/>
</SquareButton> </SquareButton>
</div> </div>
<div @click.prevent="pause">
<SquareButton link="#" colour="info" aria-label="Pause translation mode">
<Icon v="pause-circle"/>
</SquareButton>
</div>
</template> </template>
<div v-else @click.prevent="startTranslating"> <div v-else @click.prevent="startTranslating">
<SquareButton link="#" colour="info" aria-label="Translation Mode"> <SquareButton link="#" colour="info" aria-label="Translation Mode">
@ -25,6 +30,7 @@
<script> <script>
import {mapState} from "vuex"; import {mapState} from "vuex";
import Suml from 'suml';
export default { export default {
data() { data() {
@ -37,13 +43,34 @@
}, },
async commitChanges() { async commitChanges() {
await this.$confirm(`Do you want to commit ${this.changesCount} changes?`, 'success'); 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.$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() { async revertChanges() {
if (this.changesCount) { if (this.changesCount) {
await this.$confirm(`Do you want to revert ${this.changesCount} changes?`, 'danger'); await this.$confirm(`Do you want to revert ${this.changesCount} changes?`, 'danger');
} }
this.$store.commit('translationAbort'); 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: { 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 Vue from 'vue'
import t from '../src/translator'; import translator from '../src/translator';
import config from '../data/config.suml'; import config from '../data/config.suml';
import {buildDict} from "../src/helpers"; import {buildDict} from "../src/helpers";
import {DateTime} from "luxon"; import {DateTime} from "luxon";
@ -10,11 +10,11 @@ export default ({ app, store }) => {
Vue.prototype.$base = process.env.BASE_URL; Vue.prototype.$base = process.env.BASE_URL;
Vue.prototype.$t = t; Vue.prototype.$t = (key, params = {}, warn = true) => translator.translate(key, params, warn);
Vue.prototype.$te = key => t(key, {}, false) !== undefined; Vue.prototype.$te = (key) => translator.has(key);
Vue.prototype.$translateForPronoun = (str, pronoun) => Vue.prototype.$translateForPronoun = (str, pronoun) =>
pronoun.format( 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; Vue.prototype.config = config;
@ -31,6 +31,7 @@ export default ({ app, store }) => {
}); });
store.commit('setSpelling', app.$cookies.get('spelling')); 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` 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)); router.use(grant.express()(require('./social').config));
app.use(require('./routes/home').default); app.use(require('./routes/home').default);
app.use(require('./routes/banner').default); app.use(require('./routes/banner').default);
app.use(require('./routes/user').default); app.use(require('./routes/user').default);
app.use(require('./routes/profile').default); app.use(require('./routes/profile').default);
app.use(require('./routes/admin').default); app.use(require('./routes/admin').default);
app.use(require('./routes/mfa').default); app.use(require('./routes/mfa').default);
app.use(require('./routes/pronouns').default); app.use(require('./routes/pronouns').default);
app.use(require('./routes/sources').default); app.use(require('./routes/sources').default);
app.use(require('./routes/nouns').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/pronounce').default);
app.use(require('./routes/census').default); app.use(require('./routes/census').default);
app.use(require('./routes/names').default); app.use(require('./routes/names').default);
app.use(require('./routes/images').default); app.use(require('./routes/images').default);
app.use(require('./routes/blog').default); app.use(require('./routes/blog').default);
app.use(require('./routes/calendar').default); app.use(require('./routes/calendar').default);
app.use(require('./routes/translations').default);
app.use(function (err, req, res, next) { app.use(function (err, req, res, next) {
console.error(err.stack); 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 = { export const contact = {
all: { all: {
@ -75,8 +75,8 @@ const supportLinks = {
bank: { bank: {
icon: 'money-check-alt', icon: 'money-check-alt',
url: 'https://bunq.me/PronounsPage', url: 'https://bunq.me/PronounsPage',
headline: t('support.bankAccount'), headline: translator.translate('support.bankAccount'),
// tooltip: t('support.bankAccountOwner'), // tooltip: translator.translate('support.bankAccountOwner'),
}, },
kofi: { kofi: {
icon: 'coffee', icon: 'coffee',

View File

@ -1,8 +1,21 @@
import translations from '../data/translations.suml'; import translations from '../data/translations.suml';
import baseTranslations from '../locale/_base/translations.suml';
export default (key, params = {}, warn = true, translate = true) => { class Translator {
let value = translations; constructor(translations, baseTranslations) {
if (translate) { 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('.')) { for (let part of key.split('.')) {
value = value[part]; value = value[part];
if (value === undefined) { if (value === undefined) {
@ -12,17 +25,23 @@ export default (key, params = {}, warn = true, translate = true) => {
return undefined; return undefined;
} }
} }
} else { return value;
value = key;
} }
for (let k in params) { has(key) {
if (params.hasOwnProperty(k)) { return this.get(key, false) !== undefined;
value = Array.isArray(value) }
? value.map(v => v.replace(new RegExp('%' + k + '%', 'g'), params[k]))
: value.replace(new RegExp('%' + k + '%', 'g'), params[k]); 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 jwt from 'jsonwebtoken';
import t from '../src/translator'; import translator from '../src/translator';
import {buildDict} from "../src/helpers"; import {buildDict} from "../src/helpers";
export const state = () => ({ export const state = () => ({
@ -57,17 +57,20 @@ export const mutations = {
}, },
translationInit(state) { translationInit(state) {
state.translationMode = true; state.translationMode = true;
state.translationChanges = {};
}, },
translationCommit(state) { translationCommit(state) {
alert('not implemented!') state.translationMode = false;
state.translationChanges = {};
}, },
translationAbort(state) { translationAbort(state) {
state.translationMode = false; state.translationMode = false;
state.translationChanges = {}; state.translationChanges = {};
}, },
translationPause(state) {
state.translationMode = false;
},
translate(state, {key, newValue}) { translate(state, {key, newValue}) {
if (newValue !== t(key)) { if (newValue !== translator.get(key)) {
const translationChanges = {...state.translationChanges}; const translationChanges = {...state.translationChanges};
translationChanges[key] = newValue; translationChanges[key] = newValue;
state.translationChanges = translationChanges; state.translationChanges = translationChanges;
@ -82,4 +85,13 @@ export const mutations = {
}, state.translationChanges); }, state.translationChanges);
} }
}, },
restoreTranslations(state, translations) {
if (translations) {
state.translationMode = true;
state.translationChanges = translations;
} else {
state.translationMode = false;
state.translationChanges = {};
}
},
} }