Skip to content
Snippets Groups Projects
Commit 8af5de2d authored by Philipp Sautner's avatar Philipp Sautner
Browse files

Merge branch 'keywords-to-be-selected' into 'staging'

Spellcheck integration

See merge request arsnova/topic-cloud!15
parents 91629f09 071114fd
No related merge requests found
Showing
with 333 additions and 135 deletions
{
"/languagetool": {
"target": "https://lt.frag.jetzt/v2/check",
"secure": true,
"changeOrigin": true,
"pathRewrite": {
"^/languagetool": ""
},
"logLevel": "debug"
},
"/spacy": {
"target": "https://spacy.frag.jetzt/dep",
"secure": true,
......
<ars-row ars-flex-box>
<ars-row>
<div class="lang-selection">
<button class="lang-btn" mat-button (click)="select.open()">
<i class="material-icons">language</i>
{{'spacy-dialog.' + (selectedLang === 'auto' ? 'auto' : languagetoolService.mapLanguageToSpacyModel(selectedLang)) | translate}}
<mat-select class="select-list" #select [(ngModel)]="selectedLang">
<mat-option *ngFor="let lang of languages" [value]="lang">
<span *ngIf="lang == 'de-DE'">{{ 'spacy-dialog.de' | translate }}</span>
<span *ngIf="lang == 'en-US'">{{ 'spacy-dialog.en' | translate }}</span>
<span *ngIf="lang == 'fr'">{{ 'spacy-dialog.fr' | translate }}</span>
<span *ngIf="lang == 'auto'">{{ 'spacy-dialog.auto' | translate }}</span>
</mat-option>
</mat-select>
</button>
</div>
<div class="anchor-wrp">
<div class="anchor-right">
<mat-form-field *ngIf="tags"
......@@ -29,20 +43,18 @@
<ars-row [overflow]="'auto'"
style="max-height:calc( 100vh - 250px )">
<mat-form-field style="width:100%;">
<textarea (focus)="eventService.makeFocusOnInputTrue()"
<input [disabled]="true" matInput>
<div [contentEditable]="true"
[spellcheck]="false"
(focus)="eventService.makeFocusOnInputTrue()"
style="margin-top:15px;width:100%;"
(blur)="eventService.makeFocusOnInputFalse()"
matInput
#commentBody
matTextareaAutosize
matAutosizeMinRows="5"
matAutosizeMaxRows="10"
maxlength="{{user.role === 3 ? 1000 : 500}}"
[formControl]="bodyForm"
aria-labelledby="ask-question-description"
autofocus
(input)="maxLength(commentBody)"
id="answer-input">
</textarea>
</div>
<mat-placeholder class="placeholder">
{{ 'comment-page.enter-comment' | translate }}
</mat-placeholder>
......@@ -53,14 +65,14 @@
</mat-hint>
<mat-hint align="end">
<span aria-hidden="true">
{{commentBody.value.length}} / {{user.role === 3 ? 1000 : 500}}
{{commentBody.innerText.length}} / {{user.role === 3 ? 1000 : 500}}
</span>
</mat-hint>
</mat-form-field>
</ars-row>
</mat-tab>
<mat-tab label="{{ 'comment-page.preview-comment' | translate }}"
[disabled]="!commentBody.value">
[disabled]="!commentBody.innerText">
<ars-row [height]="12"></ars-row>
<ars-row>
<mat-divider></mat-divider>
......@@ -68,7 +80,7 @@
<ars-row [height]="12"></ars-row>
<ars-row [overflow]="'auto'"
style="max-height:calc( 100vh - 250px )">
<markdown [data]="commentBody.value"></markdown>
<markdown [data]="commentBody.innerText"></markdown>
</ars-row>
</mat-tab>
</mat-tab-group>
......@@ -80,6 +92,7 @@
<ars-fill>
</ars-fill>
<ars-col>
<button (click)="grammarCheck(commentBody)">{{ 'comment-page.grammar-check' | translate}}</button>
<app-dialog-action-buttons
buttonsLabelSection="comment-page"
confirmButtonLabel="send"
......
......@@ -14,17 +14,38 @@ app-comment-list {
max-width: 800px;
}
textarea {
#answer-input {
line-height: 120%;
color: var(--on-surface);
caret-color: var(--on-surface);
-webkit-appearance: textarea;
min-height: 50px;
cursor: text;
&:focus {
outline: none;
}
}
.send {
color: var(--on-primary);
background-color: var(--primary);
}
::ng-deep .mat-select-value{
width: auto!important;
}
.material-icons{
margin-right: 18px;
}
.select-list{
width: calc(100% - 24px);
}
.lang-selection {
vertical-align: middle;
margin-right: 0;
}
mat-hint {
color: var(--on-surface) !important;
}
......@@ -45,9 +66,6 @@ mat-hint {
z-index:10000;
}
.tag-select{
}
.anchor-right{
@media screen and (max-width:500px) {
width:70px;
......@@ -82,6 +100,7 @@ mat-hint {
::ng-deep .mat-select-arrow-wrapper .mat-select-arrow {
color: var(--on-surface);
margin-right: 50px;
}
::ng-deep .mat-select-value-text {
......@@ -91,3 +110,11 @@ mat-hint {
::ng-deep .mat-primary .mat-option.mat-selected:not(.mat-option-disabled) {
color: var(--primary);
}
::ng-deep .mat-select-panel {
background: var(--dialog);
}
.mat-option {
color: var(--on-surface);
}
import { Component, Inject, OnInit, ViewChild } from '@angular/core';
import { Comment } from '../../../../models/comment';
import { NotificationService } from '../../../../services/util/notification.service';
import { MAT_DIALOG_DATA, MatDialog, MatDialogConfig, MatDialogRef } from '@angular/material/dialog';
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
import { TranslateService } from '@ngx-translate/core';
import { FormControl, Validators } from '@angular/forms';
import { User } from '../../../../models/user';
import { CommentListComponent } from '../../comment-list/comment-list.component';
import { EventService } from '../../../../services/util/event.service';
import { SpacyDialogComponent } from '../spacy-dialog/spacy-dialog.component';
import { LanguagetoolService, Language } from '../../../../services/http/languagetool.service';
@Component({
selector: 'app-submit-comment',
......@@ -23,18 +24,22 @@ export class CreateCommentComponent implements OnInit {
tags: string[];
selectedTag: string;
languages: Language[] = ['de-DE', 'en-US', 'fr', 'auto'];
selectedLang: Language = 'auto';
bodyForm = new FormControl('', [Validators.required]);
@ViewChild('commentBody', { static: true })commentBody: HTMLTextAreaElement;
@ViewChild('commentBody', { static: true })commentBody: HTMLDivElement;
constructor(
private notification: NotificationService,
public dialogRef: MatDialogRef<CommentListComponent>,
private translateService: TranslateService,
public dialog: MatDialog,
private translationService: TranslateService,
public eventService: EventService,
@Inject(MAT_DIALOG_DATA) public data: any) {
private notification: NotificationService,
public dialogRef: MatDialogRef<CommentListComponent>,
private translateService: TranslateService,
public dialog: MatDialog,
private translationService: TranslateService,
public languagetoolService: LanguagetoolService,
public eventService: EventService,
@Inject(MAT_DIALOG_DATA) public data: any) {
}
ngOnInit() {
......@@ -72,20 +77,29 @@ export class CreateCommentComponent implements OnInit {
}
openSpacyDialog(comment: Comment): void {
const dialogRef = this.dialog.open(SpacyDialogComponent, {
data: {
comment
this.checkSpellings(comment.body).subscribe((res) => {
let commentBodyChecked = comment.body;
const commentLang = this.languagetoolService.mapLanguageToSpacyModel(res.language.code);
for(let i = res.matches.length - 1; i >= 0; i--){
commentBodyChecked = commentBodyChecked.substr(0, res.matches[i].offset) +
commentBodyChecked.substr(res.matches[i].offset + res.matches[i].length, commentBodyChecked.length);
}
});
dialogRef.afterClosed()
.subscribe(result => {
if (result) {
this.dialogRef.close(result);
const dialogRef = this.dialog.open(SpacyDialogComponent, {
data: {
comment,
commentLang,
commentBodyChecked
}
});
}
dialogRef.afterClosed()
.subscribe(result => {
if (result) {
this.dialogRef.close(result);
}
});
});
};
/**
......@@ -99,7 +113,51 @@ export class CreateCommentComponent implements OnInit {
/**
* Returns a lambda which executes the dialog dedicated action on call.
*/
buildCreateCommentActionCallback(text: HTMLInputElement|HTMLTextAreaElement): () => void {
return () => this.closeDialog(text.value);
buildCreateCommentActionCallback(text: HTMLDivElement): () => void {
return () => this.closeDialog(text.innerText);
}
checkSpellings(text: string, language: Language = this.selectedLang) {
return this.languagetoolService.checkSpellings(text, language);
}
maxLength(commentBody: HTMLDivElement): void {
// Cut the text down to 500 or 1000 chars depending on the user role.
if(this.user.role === 3 && commentBody.innerText.length > 1000) {
commentBody.innerText = commentBody.innerText.slice(0, 1000);
} else if(this.user.role !== 3 && commentBody.innerText.length > 500){
commentBody.innerText = commentBody.innerText.slice(0, 500);
}
}
grammarCheck(commentBody: HTMLDivElement): void {
let wrongWords: string[] = [];
this.checkSpellings(commentBody.innerText).subscribe((wordsCheck) => {
if(wordsCheck.matches.length > 0 ) {
wordsCheck.matches.forEach(grammarError => {
const wrongWord = commentBody.innerText.slice(grammarError.offset, grammarError.offset + grammarError.length);
wrongWords.push(wrongWord);
});
this.checkSpellings(commentBody.innerHTML).subscribe((res) => {
for(let i = res.matches.length - 1; i >= 0; i--){ // Reverse for loop to make sure the offset is right.
const wrongWord = commentBody.innerHTML
.slice(res.matches[i].offset, res.matches[i].offset + res.matches[i].length);
if (wrongWords.includes(wrongWord)) { // Only replace the real Words, excluding the HTML tags
const msg = res.matches[i].message; // The explanation of the suggestion for improvement
const suggestions = res.matches[i].replacements; // The suggestions for improvement. Access: suggestions[x].value
const replacement = '<span style="text-decoration: underline wavy red">' // set the Styling for all marked words
// Select menu with suggestions has to be injected here.
+ wrongWord +
'</span>';
commentBody.innerHTML = commentBody.innerHTML.substr(0, res.matches[i].offset) +
replacement +
commentBody.innerHTML.substr(res.matches[i].offset + wrongWord.length,
commentBody.innerHTML.length);
}
}
});
}
});
}
}
<ars-row>
<div class="anchor-wrp">
<ars-row class="content-container">
<ars-row class="dialog-header">
<div class="lang-selection">
<mat-icon>more_vert</mat-icon>
<mat-select class="select-list" [(ngModel)]="selectedLang" (selectionChange)="evalInput($event.value)">
<mat-option *ngFor="let lang of commentLang" [value]="lang.lang">
<span *ngIf="lang.lang == 'de'">{{ 'spacy-dialog.german' | translate}}</span>
<span *ngIf="lang.lang == 'en'">{{ 'spacy-dialog.english' | translate}}</span>
<span *ngIf="lang.lang == 'fr'">{{ 'spacy-dialog.french' | translate}}</span>
</mat-option>
</mat-select>
</div>
<span *ngIf="keywords.length > 0">
<ars-row class="select-all-section">
<mat-icon>playlist_add_check</mat-icon>
<mat-label class="select-all-label" for="checkAll">{{ 'spacy-dialog.select-all' | translate }}</mat-label>
<mat-checkbox class="select-all-checkbox" id="checkAll" (change)="selectAll($event.checked)"></mat-checkbox>
</ars-row>
</span>
<span *ngIf="keywords.length > 0"></span>
<span *ngIf="keywords.length > 0">
<ars-row class="select-all-section">
<mat-checkbox class="select-all-checkbox" id="checkAll" (change)="selectAll(checkall.checked)" #checkall></mat-checkbox>
<mat-label class="select-all-label" for="checkAll" (click)="checkall.checked = !checkall.checked; selectAll(checkall.checked)">
<mat-icon class="select-all-icon">playlist_add_check</mat-icon>
{{ 'spacy-dialog.select-all' | translate }}
</mat-label>
</ars-row>
<ars-row ars-flex-box class="list-container">
<mat-list dense class="keywords-list">
<mat-list-item *ngFor="let keyword of keywords; let odd = odd; let even = even"
[class.keywords-alternate]="odd"
[class.keywords-even]="even"
[ngClass]="{'keyword-selected': keyword.selected}">
<span class="keyword-span" *ngIf="!keyword.editing">{{keyword.word}}</span>
<input class="keyword-span, isEditing" *ngIf="keyword.editing" [(ngModel)]="keyword.word"/>
<div class="keywords-actions">
<mat-checkbox [checked]="keyword.completed"
(change)="keyword.selected = $event.checked">
</mat-checkbox>
<button *ngIf="!keyword.editing"
(click)="onEdit(keyword)" mat-icon-button
[ngClass]="{'keywords-actions-selected': keyword.selected}">
<mat-icon>edit</mat-icon>
</button>
<button *ngIf="keyword.editing"
(click)="onEndEditing(keyword)" mat-icon-button
class = "edit-accept">
<mat-icon>check</mat-icon>
</button>
</div>
</mat-list-item>
</mat-list>
</ars-row>
<span *ngIf="keywords.length<=0">
<p>{{ 'spacy-dialog.empty-nouns' | translate}}</p>
</span>
</span>
<span *ngIf="keywords.length > 0"></span>
<ars-row class="list-container">
<mat-list dense class="keywords-list">
<mat-list-item *ngFor="let keyword of keywords; let odd = odd; let even = even; let i = index"
[class.keywords-alternate]="odd"
[class.keywords-even]="even"
[ngClass]="{'keyword-selected': keyword.selected, 'first-keyword': i === 0}">
<span class="keyword-span" *ngIf="!keyword.editing">{{keyword.word}}</span>
<input class="keyword-span, isEditing" *ngIf="keyword.editing" [(ngModel)]="keyword.word"/>
<div class="keywords-actions">
<mat-checkbox [checked]="keyword.completed"
(change)="keyword.selected = $event.checked"
[(ngModel)]="keyword.completed">
</mat-checkbox>
<button *ngIf="!keyword.editing"
(click)="onEdit(keyword)" mat-icon-button
[ngClass]="{'keywords-actions-selected': keyword.selected}">
<mat-icon>edit</mat-icon>
</button>
<button *ngIf="keyword.editing"
(click)="onEndEditing(keyword)" mat-icon-button
class = "edit-accept">
<mat-icon>check</mat-icon>
</button>
</div>
</mat-list-item>
</mat-list>
</ars-row>
<span *ngIf="keywords.length<=0">
<p>{{ 'spacy-dialog.empty-nouns' | translate}}</p>
</span>
</div>
</ars-row>
<ars-row ars-flex-box>
<ars-fill>
</ars-fill>
<ars-fill></ars-fill>
<ars-col>
<app-dialog-action-buttons
buttonsLabelSection="comment-page"
......
.mat-list-base[dense] .mat-list-item, .list-item {
font-size: 14px;
height: 35px !important;
font-size: 13px;
height: 40px !important;
}
.keywords-list {
height: 343px;
......@@ -9,19 +9,29 @@
overflow: auto;
padding-top: 0 !important;
}
.keywords-even {
background-color: var(--alt-dialog);
.first-keyword {
border-top: 1px solid var(--surface);
box-sizing: border-box;
}
.keywords-even, .keywords-alternate {
background-color: var(--dialog);
border-bottom: 1px solid var(--surface);
border-top-left-radius: 5px;
border-bottom-left-radius: 5px;
border-left: 3px solid var(--primary);
border-right: 1px solid var(--primary);
box-sizing: border-box;
}
.keyword-selected {
background: var(--surface);
box-shadow: inset -2px 0 0 1px var(--primary);
}
.keyword-span {
color: var(--on-surface) !important;
opacity: 1 !important;
}
.keyword-selected {
background: var(--primary-variant);
border-bottom: 1px solid var(--alt-surface);
}
::ng-deep .mat-checkbox-checked .mat-checkbox-background {
background: var(--cancel) !important;
background: var(--primary) !important;
}
.mat-checkbox-frame {
color: var(--secondary);
......@@ -31,14 +41,25 @@ background-color: var(--alt-dialog);
outline-offset: 8px;
}
.lang-selection {
display: inline-flex;
vertical-align: middle;
width:100%;
margin-right: 0;
}
.select-all-label{
padding-left: 15px;
cursor: pointer;
}
.select-list{
width: calc(100% - 24px);
}
.keywords-actions {
white-space: nowrap;
padding-left: 5px;
margin-left: auto;
color: var(--on-surface);
}
.lang-btn{
width: 100%;
}
.keywords-actions-selected {
color: var(--on-surface);
}
......@@ -53,19 +74,30 @@ background-color: var(--alt-dialog);
color: var(--on-surface);
}
.select-all-section {
display: inline-flex;
vertical-align: middle;
padding-top: 20px;
padding-bottom: 30px;
padding-left: 4px;
padding-bottom: 20px;
}
.select-all-label {
padding-left: 10px;
padding-left: 15px;
}
.select-all-icon {
float: left;
padding-left: 16px;
}
.select-all-checkbox {
margin-left: auto;
padding-right: 5px;
float: right;
padding-right: 3px;
}
.mat-select {
padding-left: 14px;
}
::ng-deep .mat-select-panel {
background: var(--dialog);
}
.mat-option.mat-active {
color: var(--primary);
}
.mat-option {
color: var(--on-surface);
}
import { AfterContentInit, Component, Inject, OnInit } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { CreateCommentComponent } from '../create-comment/create-comment.component';
import { SpacyService } from '../../../../services/http/spacy.service';
import { SpacyService, Model } from '../../../../services/http/spacy.service';
import { LanguageService } from '../../../../services/util/language.service';
import { Comment } from '../../../../models/comment';
......@@ -11,6 +12,7 @@ export interface Keyword {
editing: boolean;
selected: boolean;
}
@Component({
selector: 'app-spacy-dialog',
templateUrl: './spacy-dialog.component.html',
......@@ -18,13 +20,9 @@ export interface Keyword {
})
export class SpacyDialogComponent implements OnInit, AfterContentInit {
commentLang = [
{ lang: 'de' },
{ lang: 'en' },
{ lang: 'fr' },
];
selectedLang = localStorage.getItem('currentLang');
comment: Comment;
commentLang: Model;
commentBodyChecked: string;
keywords: Keyword[] = [];
constructor(
......@@ -36,10 +34,12 @@ export class SpacyDialogComponent implements OnInit, AfterContentInit {
ngOnInit(): void {
this.comment = this.data.comment;
this.commentLang = this.data.commentLang;
this.commentBodyChecked = this.data.commentBodyChecked;
}
ngAfterContentInit(): void {
this.evalInput(this.selectedLang);
this.evalInput(this.commentLang);
}
/**
......@@ -56,11 +56,10 @@ export class SpacyDialogComponent implements OnInit, AfterContentInit {
};
}
evalInput(model: string) {
evalInput(model: Model) {
const words: Keyword[] = [];
// N at first pos = all Nouns(NN de/en) including singular(NN, NNP en), plural (NNPS, NNS en), proper Noun(NNE, NE de)
this.spacyService.analyse(this.comment.body, model)
this.spacyService.analyse(this.commentBodyChecked, model)
.subscribe(res => {
for(const word of res.words) {
if (word.tag.charAt(0) === 'N') {
......@@ -74,12 +73,14 @@ export class SpacyDialogComponent implements OnInit, AfterContentInit {
}
this.keywords = words;
}, () => {
this.keywords = []
this.keywords = [];
});
}
onEdit(keyword){
keyword.editing = true;
keyword.completed = false;
keyword.selected = false;
}
onEndEditing(keyword){
......
......@@ -83,7 +83,7 @@ export class CommentService extends BaseHttpService {
return this.http.post<Comment>(connectionUrl,
{
roomId: comment.roomId, body: comment.body,
read: comment.read, creationTimestamp: comment.timestamp, tag: comment.tag, keywords: comment.keywords
read: comment.read, creationTimestamp: comment.timestamp, tag: comment.tag, keywords: JSON.stringify(comment.keywords)
}, httpOptions).pipe(
tap(_ => ''),
catchError(this.handleError<Comment>('addComment'))
......@@ -238,4 +238,4 @@ export class CommentService extends BaseHttpService {
hash = +userNumberString.substring(userNumberString.length - 4, userNumberString.length);
return hash;
}
}
}
\ No newline at end of file
/*import { TestBed } from '@angular/core/testing';
import { LanguagetoolService } from './languagetool.service';
describe('LanguagetoolService', () => {
let service: LanguagetoolService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(LanguagetoolService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
*/
\ No newline at end of file
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { BaseHttpService } from './base-http.service';
import { catchError } from 'rxjs/operators';
import { Model } from './spacy.service';
import { Observable } from 'rxjs';
export type Language = 'de-DE' | 'en-US' | 'fr' | 'auto';
@Injectable({
providedIn: 'root'
})
export class LanguagetoolService extends BaseHttpService {
constructor(private http: HttpClient) {
super();
}
mapLanguageToSpacyModel(language: Language): Model {
switch (language) {
case 'de-DE':
return 'de';
case 'en-US':
return 'en';
case 'fr':
return 'fr';
default:
return 'de';
}
}
checkSpellings(text: string, language: Language): Observable<any> {
const url = '/languagetool';
return this.http.get(url, {params: {
text, language
}})
.pipe(
catchError(this.handleError<any>('checkSpellings'))
);
}
}
......@@ -4,6 +4,8 @@ import { Observable } from 'rxjs';
import { BaseHttpService } from './base-http.service';
import { catchError } from 'rxjs/operators';
export type Model = 'de' | 'en' | 'fr';
export class Result {
arcs: Arc[];
words: Word[];
......
......@@ -83,9 +83,10 @@
"questions-blocked": "Fragen sind deaktiviert!"
},
"spacy-dialog":{
"german": "Deutsch",
"english": "Englisch",
"french": "Französisch",
"auto": "auto",
"de": "Deutsch",
"en": "Englisch",
"fr": "Französisch",
"empty-nouns": "Keine Nomen enthalten",
"select-all": "Alles auswählen"
},
......@@ -180,7 +181,8 @@
"edit-favorite": "Für einen Bonus markieren",
"edit-favorite-reset": "Markierung zurücksetzen",
"edit-bookmark": "Lesezeichen setzen",
"edit-bookmark-reset": "Markierung zurücksetzen"
"edit-bookmark-reset": "Markierung zurücksetzen",
"grammar-check": "Rechtschreibprüfung"
},
"content": {
"abort": "Abbrechen",
......
......@@ -84,9 +84,10 @@
"questions-blocked": "Questions are blocked!"
},
"spacy-dialog": {
"german": "German",
"english": "English",
"french": "French",
"auto": "auto",
"de": "German",
"en": "English",
"fr": "French",
"empty-nouns": "No nouns included",
"select-all": "Select all"
},
......@@ -181,8 +182,8 @@
"edit-favorite": "Mark for bonus",
"edit-favorite-reset": "Remove marker",
"edit-bookmark": "Bookmark",
"edit-bookmark-reset": "Remove marker"
"edit-bookmark-reset": "Remove marker",
"grammar-check": "Spell check"
},
"content": {
"abort": "Abort",
......
......@@ -88,9 +88,10 @@
"delete": "Löschen"
},
"spacy-dialog":{
"german": "Deutsch",
"english": "Englisch",
"french": "Französisch",
"auto": "auto",
"de": "Deutsch",
"en": "Englisch",
"fr": "Französisch",
"empty-nouns": "Keine Nomen enthalten",
"select-all": "Alles auswählen"
},
......@@ -146,7 +147,8 @@
"preview-comment": "Vorschau",
"show-more": "Mehr ansehen",
"show-less": "Weniger anzeigen",
"sure": "Bist du sicher?"
"sure": "Bist du sicher?",
"grammar-check": "Rechtschreibprüfung"
},
"home-page": {
......
......@@ -98,9 +98,10 @@
"delete": "Delete"
},
"spacy-dialog": {
"german": "German",
"english": "English",
"french": "French",
"auto": "auto",
"de": "German",
"en": "English",
"fr": "French",
"empty-nouns": "No nouns included",
"select-all": "Select all"
},
......@@ -155,7 +156,8 @@
"preview-comment": "Preview",
"show-more": "Show more",
"show-less": "Show less",
"delete": "Delete question"
"delete": "Delete question",
"grammar-check": "Spell check"
},
"home-page": {
"exactly-8": "A key is a combination of 8 digits.",
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment