TranslationMode - admin

This commit is contained in:
Andrea Vos 2022-07-30 12:12:47 +02:00
parent 852f0f812e
commit 9fd67a5bb0
19 changed files with 208 additions and 48 deletions

View File

@ -4,7 +4,7 @@
<Header/>
<main class="container">
<Nuxt/>
<!--<TranslationMode/>-->
<TranslationMode/>
<ScrollButton/>
</main>
</div>

View File

@ -120,7 +120,7 @@ sources:
key: 'Key'
keyInfo: 'Identifier for linking sources between language versions and linking with the dictionary'
images: 'Images'
spoiler: 'Spoiler' # TODO
spoiler: 'Spoiler'
otherVersions: 'In other languages'
referenced: 'Examples of use'
@ -435,7 +435,7 @@ contact:
language: >
We're an international team people who created a specific language version
might not be the same people who will read your message.
So, if possible, we'd appreciate, if you contacted us in English.
So, if possible, we'd appreciate, if you contacted us in <strong>English or Polish</strong>.
team:
name: 'The “Neutral Language Council” collective'
nameShort: 'Collective'
@ -575,7 +575,7 @@ profile:
flagsInfo: 'Drag & drop your pride flags into this frame.'
flagsCustom: 'Upload custom flags'
flagsCustomWarning: 'This flag has been uploaded by a user. The team of pronouns.page is not responsible for it.'
flagsAsterisk: 'This is not a queer identity, but we include it for people who are queer in other ways (eg. straight trans people).' # TODO
flagsAsterisk: 'This is not a queer identity, but we include it for people who are queer in other ways (eg. straight trans people).'
links: 'Links'
linksRecommended: 'We recommend linking to'
verifiedLinks:

View File

@ -402,7 +402,6 @@ support:
description: >
Wenn du dich an den Kosten für Server, Domains, Aufkleber etc. beteiligen oder den Autor*innen einfach ein Bier ausgeben willst,
kannst du den untenstehenden Link nutzen:
bankAccount: 'Bank transfer' # TODO
user:
header: 'Account'
@ -579,8 +578,6 @@ confirm:
yes: 'Ja, ich bin sicher'
no: 'Nein, abbrechen'
ok: 'OK'
save: 'Save' # TODO
dismiss: 'Dismiss' # TODO
terms:
header: 'Nutzungsbedingungen'
@ -1001,8 +998,6 @@ calendar:
trans_flag_day: 'Trans Flaggen Tag'
alan_turing_day: 'Alan Turing Tag'
nonbinary_kids_day: 'Tag nicht-binärer Kinder'
masc_lesbian_week: 'Masculine Lesbian Visibility and Awareness Week' # TODO
masc_lesbian_day: 'Masculine Lesbian Visibility and Awareness Day' # TODO
banner: 'Heute feiern wir'
celebrating_custom: 'wird gefeiert'
celebrating_day: 'wird gefeiert am:'

View File

@ -491,7 +491,7 @@ contact:
language: >
We're an international team people who created a specific language version
might not be the same people who will read your message.
So, if possible, we'd appreciate, if you contacted us in English.
So, if possible, we'd appreciate, if you contacted us in <strong>English or Polish</strong>.
team:
name: 'The “Neutral Language Council” collective'
nameShort: 'Collective'

View File

@ -0,0 +1,19 @@
export default [
'contact.language',
'support.bankAccount',
'confirm.save',
'confirm.dismiss',
'calendar.events.mspec_lesbian_day',
'calendar.events.mspec_lesbian_week',
'calendar.events.mspec_gay_day',
'calendar.events.mspec_gay_week',
'calendar.events.masc_lesbian_day',
'calendar.events.masc_lesbian_week',
'user.login.domainPlaceholder',
'user.login.deprecated',
'user.login.depreciationNotice',
'profile.flagsAsterisk',
'profile.verifiedLinks.header',
'profile.verifiedLinks.info',
'sources.submit.spoiler',
];

View File

@ -409,7 +409,6 @@ support:
description: >
Si vous voulez donnez un coup de main pour le serveur, le domaine, les stickers, etc. ou simplement payer une bière à l'auteur,
vous pouvez utiliser les liens ci-dessous :
bankAccount: 'Bank transfer' # TODO
user:
header: 'Compte'
@ -586,8 +585,6 @@ confirm:
yes: 'Oui, je suis sûr·e'
no: 'Non, annuler'
ok: 'OK'
save: 'Save' # TODO
dismiss: 'Dismiss' # TODO
terms:
header: 'Conditions d''utilisation'
@ -1007,10 +1004,6 @@ calendar:
black_hiv_awareness_day: 'Journée de sensibilisation au VIH/sida des communautés noires'
hiv_aging_awareness_day: 'Journée de la sensibilisation du VIH/sida et au vieillissement'
trans_hiv_testing_day: 'Journée de dépistage au VIH des transgenres'
mspec_lesbian_day: 'Mspec Lesbian Visbility & Awareness Day' # TODO
mspec_lesbian_week: 'Mspec Lesbian Visbility & Awareness Week' # TODO
mspec_gay_day: 'Mspec Gay Visbility & Awareness Day' # TODO
mspec_gay_week: 'Mspec Gay Visbility & Awareness Week' # TODO
spring_testing_week: 'Semaine Européenne printanière des tests anti-VIH'
autumn_testing_week: 'Semaine Européenne automnale des tests anti-VIH'
sex_worker_day: 'Journée internationale des travailleurs du sexe'
@ -1024,8 +1017,6 @@ calendar:
trans_flag_day: 'Jour du drapeau trans'
alan_turing_day: 'Journée Alan Turing'
nonbinary_kids_day: 'Journée des enfants non-binaires'
masc_lesbian_week: 'Masculine Lesbian Visibility and Awareness Week' # TODO
masc_lesbian_day: 'Masculine Lesbian Visibility and Awareness Day' # TODO
banner: 'Nous célébrons '
celebrating_custom: 'est célébrée'
celebrating_day: 'est fêté le'

View File

@ -119,7 +119,6 @@ sources:
key: 'Key'
keyInfo: 'Identifier for linking sources between language versions and linking with the dictionary'
images: 'Images'
spoiler: 'Spoiler' # TODO
otherVersions: 'In other languages'
referenced: 'Examples of use'
@ -479,7 +478,6 @@ support:
description: >
If you want to chip in for the server, domains, stickers etc., or simply buy the authors a beer,
you can use the links below:
bankAccount: 'Bank transfer' # TODO
user:
header: 'Account'

View File

@ -118,7 +118,6 @@ sources:
key: '키'
keyInfo: '언어 버전 간 소스 연결 및 사전 연결 식별자'
images: '이미지'
spoiler: 'Spoiler' # TODO
otherVersions: '다른 언어'
referenced: '사용 예'

View File

@ -381,7 +381,6 @@ support:
description: >
Si keres donar para el servidor, los dominios, stikers ets., o simplemente merkarles una bira a les autorxs,
puedes uzar los sigientes atadijos:
bankAccount: 'Bank transfer' # TODO
user:
header: 'Kuento'
@ -560,8 +559,6 @@ confirm:
yes: 'Si, lo esto'
no: 'No, anular'
ok: 'OK'
save: 'Save' # TODO
dismiss: 'Dismiss' # TODO
terms:
header: 'Terminos de Servisio'

View File

@ -405,7 +405,6 @@ support:
description: >
Als je wilt bijdragen aan de server, domeinen, stickers, andere materialen, of als je simpelweg een biertje voor de auteurs wil kopen,
kun je de onderstaande links gebruiken:
bankAccount: 'Bank transfer' # TODO
user:
header: 'Account'
@ -581,8 +580,6 @@ confirm:
yes: 'Ja, dat weet ik zeker'
no: 'Nee, annuleer'
ok: 'OK'
save: 'Save' # TODO
dismiss: 'Dismiss' # TODO
terms:
header: 'Gebruiksvoorwaarden'

View File

@ -381,7 +381,6 @@ support:
description: >
Hvis du har lyst til å bidra til serveren, domener, klistremerker osv., eller bare kjøpe forfatterne en kaffe,
kan du bruke linkene nedenfor:
bankAccount: 'Bank transfer' # TODO
user:
header: 'Bruker'
@ -402,12 +401,6 @@ user:
passwordless: 'Denne nettsiden lagrer ingen passord. {https://avris.it/blog/passwords-are-passé=More info.}'
instancePlaceholder: 'Forekomst'
domainPlaceholder: 'Domene'
# TODO
deprecated: 'Deprecated'
depreciationNotice: >
This is not a reliable authentication provider.
<a href="%link%" target="_blank" rel="noopener">Check out this link for more details</a>.
We highly recommend making sure that you have a different login method available.
code:
action: 'Gyldig'
invalid: 'Ugyldig kode.'
@ -470,7 +463,6 @@ profile:
flagsInfo: 'Dra og dropp pride flaggene dine inn i denne rammen.'
flagsCustom: 'Last opp tilpasset flagg'
flagsCustomWarning: 'Dette flagget har blitt lastet opp av en bruker. Teamet som har laget pronouns.page er ikke ansvarlig for det.'
flagsAsterisk: 'This is not a queer identity, but we include it for people who are queer in other ways (eg. straight trans people).' # TODO
links: 'Lenker'
linksRecommended: 'Vi anbefaler å lenke til'
# TODO
@ -561,8 +553,6 @@ confirm:
yes: 'Ja, jeg er sikker'
no: 'Nei, kanseller'
ok: 'OK'
save: 'Save' # TODO
dismiss: 'Dismiss' # TODO
terms:
header: 'Vilkår og betingelser'

View File

@ -534,7 +534,7 @@ links:
Takie słowa padły w trakcie jednego z wywiadów przeprowadzanych podczas badania na rzecz tej pracy.
Zacytowany wyżej fragment przedstawia dokładnie jej cel uświadomienie o potrzebie
budowania bezpiecznej przestrzeni do istnienia dla osób niebinarnych.
Drugi fragment cytatu ujawnia kolejną ważną prośbę o niepatrzenie na inne osoby przez pryzmat płc
Drugi fragment cytatu ujawnia kolejną ważną prośbę o niepatrzenie na inne osoby przez pryzmat płci
i niedzielenie społeczeństwa na męską i żeńską część.
links:
-

View File

@ -1148,7 +1148,7 @@ contact:
language: >
Jesteśmy międzynarodowym zespołem osoby, które stworzyły daną wersję językową,
niekoniecznie będą tymi samymi osobami, które zobaczą Twoją wiadomość.
W miarę możliwości prosimy o kontakt w języku polskim lub angielskim.
W miarę możliwości prosimy o kontakt w języku <strong>polskim lub angielskim.</strong>
team:
name: 'Kolektyw „Rada Języka Neutralnego”'
nameShort: 'Kolektyw'

View File

@ -408,7 +408,6 @@ support:
description: >
Se você quiser de doar para o servidor, os domínios, stickers etc., ou simplemente comprar uma cerveja para ês autores,
podem usar as ligações seguintes:
bankAccount: 'Bank transfer' # TODO
user:
header: 'Conta'
@ -585,8 +584,6 @@ confirm:
yes: 'Sim'
no: 'Não, cancelar'
ok: 'OK'
save: 'Save' # TODO
dismiss: 'Dismiss' # TODO
terms:
header: 'Termos de Serviço'

View File

@ -127,6 +127,90 @@
Impersonate <button class="btn btn-primary btn-sm" @click="impersonate('example@pronouns.page')">@example</button>
</section>
<section v-if="$isGranted('translations') && missingTranslations.length">
<h3>
<Icon v="language"/>
Missing translations ({{missingTranslations.length}})
</h3>
<p>
In order to start translating, enable translation mode with the button in bottom right corner.
Then you can propose translations both her as well as in context anywhere on the site.
</p>
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<th>key</th>
<th>base</th>
<th>translation</th>
</tr>
</thead>
<tbody>
<tr v-for="mt in missingTranslations">
<td>{{mt}}</td>
<td>{{translator.get(mt, false, true)}}</td>
<td><T>{{mt}}</T></td>
</tr>
</tbody>
</table>
</div>
</section>
<section v-if="$isGranted('translations') && translationProposals.length">
<h3>
<Icon v="language"/>
Translation proposals ({{translationProposals.length}})
</h3>
<div class="table-responsive">
<table class="table table-bordered">
<thead>
<tr>
<th>key</th>
<th>base</th>
<th>translation</th>
<th>author</th>
</tr>
</thead>
<tbody>
<tr v-for="tp in translationProposals">
<td>{{tp.tKey}}</td>
<td>{{translator.get(tp.tKey, false, true)}}</td>
<td v-if="Array.isArray(tp.tValue)">
<ul>
<li v-for="el in tp.tValue">{{el}}</li>
</ul>
</td>
<td v-else>
{{tp.tValue}}
</td>
<td>
<nuxt-link :to="`/@${tp.author}`">@{{tp.author}}</nuxt-link>
<br/>
<button class="btn btn-sm btn-danger" @click="rejectTranslationProposal(tp.id)">Reject</button>
</td>
</tr>
</tbody>
</table>
</div>
<details class="border mb-3">
<summary class="bg-light p-3">
<span class="badge bg-success">Approve</span>
</summary>
<div class="p-2">
<p>
We still need to manually move the translations to the relevant SUML file,
but at least it should be easy to copy paste bits from here:
</p>
<hr/>
<pre>{{translationsProposalsSuml}}</pre>
<hr/>
<button class="btn btn-success" @click="markTranslationProposalsDone">Copied, mark as done</button>
</div>
</details>
</section>
<section v-if="$isGranted('users')">
<h3>
<Icon v="siren-on"/>
@ -189,8 +273,10 @@
</template>
<script>
import {head} from "../src/helpers";
import {deepSet, head} from "../src/helpers";
import {socialProviders} from "../src/socialProviders";
import translator from '../src/translator';
import Suml from 'suml';
export default {
data() {
@ -203,6 +289,8 @@
adminsFilter: false,
usersShown: false,
adminNotifications: this.$user().adminNotifications ?? 7,
translator,
missingTranslations: translator.listMissingTranslations(),
}
},
async asyncData({ app, store }) {
@ -216,9 +304,15 @@
abuseReports = await app.$axios.$get(`/admin/reports`);
} catch {}
let translationProposals = [];
try {
translationProposals = await app.$axios.$get(`/translations/proposals`);
} catch {}
return {
stats,
abuseReports,
translationProposals
};
},
methods: {
@ -229,6 +323,16 @@
await this.$router.push('/' + this.config.user.route);
setTimeout(() => window.location.reload(), 500);
},
async rejectTranslationProposal(id) {
await this.$confirm('Do you want to reject this translation proposal?', 'danger');
await this.$post(`/translations/reject-proposal`, {id})
this.translationProposals = this.translationProposals.filter(tp => tp.id !== id);
},
async markTranslationProposalsDone() {
await this.$confirm('Did you put the translations in the SUML file and want to mark them as done?', 'success');
await this.$post(`/translations/proposals-done`)
this.translationProposals = [];
},
},
computed: {
profilesByLocale() {
@ -241,6 +345,13 @@
abuseReportsActiveCount() {
return this.abuseReports.filter(r => !r.isHandled).length;
},
translationsProposalsSuml() {
const data = {};
for (let tp of this.translationProposals) {
deepSet(data, tp.tKey, tp.tValue);
}
return new Suml().dump(data);
},
},
watch: {
userFilter() {

View File

@ -2,6 +2,7 @@ import { Router } from 'express';
import SQL from 'sql-template-strings';
import {ulid} from "ulid";
import { handleErrorAsync } from "../../src/helpers";
import mailer from "../../src/mailer";
const router = Router();
@ -20,7 +21,49 @@ router.post('/translations/propose', handleErrorAsync(async (req, res) => {
)`)
}
// TODO email
for (let email of ['contact@pronouns.page']) {
mailer(email, 'translationProposed', {locale: global.config.locale});
}
return res.json('OK');
}));
router.get('/translations/proposals', handleErrorAsync(async (req, res) => {
if (!req.isGranted('translations')) {
return res.status(401).json({error: 'Unauthorised'});
}
return res.json(
(await req.db.all(SQL`
SELECT t.id, t.tKey, t.tValue, u.username AS author
FROM translations t
LEFT JOIN users u ON t.author_id = u.id
WHERE locale = ${global.config.locale} AND status = 0
`)).map(tp => {
return {
...tp,
tValue: JSON.parse(tp.tValue),
}
})
);
}));
router.post('/translations/reject-proposal', handleErrorAsync(async (req, res) => {
if (!req.isGranted('translations')) {
return res.status(401).json({error: 'Unauthorised'});
}
await req.db.get(SQL`UPDATE translations SET status = -1 WHERE id = ${req.body.id}`)
return res.json('OK');
}));
router.post('/translations/proposals-done', handleErrorAsync(async (req, res) => {
if (!req.isGranted('translations')) {
return res.status(401).json({error: 'Unauthorised'});
}
await req.db.get(SQL`UPDATE translations SET status = 1 WHERE locale = ${global.config.locale} AND status = 0`)
return res.json('OK');
}));

View File

@ -272,3 +272,15 @@ export const obfuscateEmail = (email) => {
return `${usernamePublic}*****@*****.${tld}`;
}
// https://newbedev.com/dynamic-deep-setting-for-a-javascript-object
export const deepSet = (obj, path, value) => {
let a = path.split('.')
let o = obj;
while (a.length - 1) {
let n = a.shift()
if (!(n in o)) o[n] = {}
o = o[n]
}
o[a[0]] = value
}

View File

@ -92,7 +92,12 @@ const templates = {
subject: 'Cards queue is getting long',
text: 'There\'s {{count}} cards in the queue!',
html: '<p>There\'s {{count}} cards in the queue!</p>',
}
},
translationProposed: {
subject: '[{{locale}}] New translations proposed',
text: 'Check them out here: https://[[domain]]/admin',
html: '<p>Check them out here: <a href="https://[[domain]]/admin" target="_blank" rel="noopener">[[domain]]/admin</a></p>',
},
}
const applyTemplate = (template, context, params) => {

View File

@ -1,10 +1,12 @@
import translations from '../data/translations.suml';
import baseTranslations from '../locale/_base/translations.suml';
import expectedTranslations from '../locale/expectedTranslations';
class Translator {
constructor(translations, baseTranslations) {
constructor(translations, baseTranslations, expectedTranslations) {
this.translations = translations;
this.baseTranslations = baseTranslations;
this.expectedTranslations = expectedTranslations;
}
translate(key, params = {}, warn = true) {
@ -45,6 +47,10 @@ class Translator {
}
return value;
}
listMissingTranslations() {
return this.expectedTranslations.filter(k => !this.has(k));
}
}
export default new Translator(translations, baseTranslations);
export default new Translator(translations, baseTranslations, expectedTranslations);