mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-08 23:14:43 -04:00
TranslationMode progress
This commit is contained in:
parent
4ff88bbcf6
commit
d285e04272
@ -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;
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -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: {
|
||||||
|
18
migrations/056-translations.sql
Normal file
18
migrations/056-translations.sql
Normal 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;
|
@ -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`
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
28
server/routes/translations.js
Normal file
28
server/routes/translations.js
Normal 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;
|
@ -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',
|
||||||
|
@ -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);
|
||||||
|
@ -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 = {};
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user