Merge pull request #388 from marhali/feature/localize-it
Feature/localize it
This commit is contained in:
commit
474842672b
@ -7,6 +7,7 @@
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- Support for IntelliJ Platform version 2024.1
|
- Support for IntelliJ Platform version 2024.1
|
||||||
|
- "Localize It" action to extract translations based on current selection. Thanks to @JPilson
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
@ -0,0 +1,83 @@
|
|||||||
|
package de.marhali.easyi18n.action;
|
||||||
|
|
||||||
|
import com.intellij.openapi.actionSystem.AnAction;
|
||||||
|
import com.intellij.openapi.actionSystem.AnActionEvent;
|
||||||
|
import com.intellij.openapi.actionSystem.CommonDataKeys;
|
||||||
|
import com.intellij.openapi.actionSystem.DataContext;
|
||||||
|
import com.intellij.openapi.command.WriteCommandAction;
|
||||||
|
import com.intellij.openapi.editor.Editor;
|
||||||
|
import com.intellij.openapi.project.Project;
|
||||||
|
|
||||||
|
import de.marhali.easyi18n.dialog.AddDialog;
|
||||||
|
import de.marhali.easyi18n.model.KeyPath;
|
||||||
|
import de.marhali.easyi18n.settings.ProjectSettingsService;
|
||||||
|
import de.marhali.easyi18n.util.DocumentUtil;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an action to localize text in the editor.
|
||||||
|
*/
|
||||||
|
class LocalizeItAction extends AnAction {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void actionPerformed(@NotNull AnActionEvent anActionEvent) {
|
||||||
|
DataContext dataContext = anActionEvent.getDataContext();
|
||||||
|
Editor editor = CommonDataKeys.EDITOR.getData(dataContext);
|
||||||
|
|
||||||
|
if (editor == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String text = editor.getSelectionModel().getSelectedText();
|
||||||
|
|
||||||
|
if (text == null || text.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((text.startsWith("\"") && text.endsWith("\"")) || (text.startsWith("'") && text.endsWith("'"))) {
|
||||||
|
text = text.substring(1);
|
||||||
|
text = text.substring(0, text.length() - 1);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Project project = anActionEvent.getProject();
|
||||||
|
|
||||||
|
if (project == null) {
|
||||||
|
throw new RuntimeException("Project is null!");
|
||||||
|
}
|
||||||
|
|
||||||
|
AddDialog dialog = new AddDialog(project, new KeyPath(text), text, (key) -> replaceSelectedText(project, editor, key));
|
||||||
|
dialog.showAndHandle();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces the selected text in the editor with a new text generated from the provided key.
|
||||||
|
*
|
||||||
|
* @param project the project where the editor belongs
|
||||||
|
* @param editor the editor where the text is selected
|
||||||
|
* @param key the key used to generate the replacement text
|
||||||
|
*/
|
||||||
|
private void replaceSelectedText(Project project, @NotNull Editor editor, @NotNull String key) {
|
||||||
|
int selectionStart = editor.getSelectionModel().getSelectionStart();
|
||||||
|
int selectionEnd = editor.getSelectionModel().getSelectionEnd();
|
||||||
|
String flavorTemplate = ProjectSettingsService.get(project).getState().getFlavorTemplate();
|
||||||
|
DocumentUtil documentUtil = new DocumentUtil(editor.getDocument());
|
||||||
|
String replacement = buildReplacement(flavorTemplate, key, documentUtil);
|
||||||
|
WriteCommandAction.runWriteCommandAction(editor.getProject(), () -> documentUtil.getDocument().replaceString(selectionStart, selectionEnd, replacement));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a replacement string based on the provided flavor template, key, and document util.
|
||||||
|
*
|
||||||
|
* @param flavorTemplate the flavor template string
|
||||||
|
* @param key the key used to generate the replacement text
|
||||||
|
* @param documentUtil the document util object used to determine the document type
|
||||||
|
* @return the built replacement string
|
||||||
|
*/
|
||||||
|
private String buildReplacement(String flavorTemplate, String key, DocumentUtil documentUtil) {
|
||||||
|
if (documentUtil.isVue() || documentUtil.isJsOrTs()) return flavorTemplate + "('" + key + "')";
|
||||||
|
|
||||||
|
return flavorTemplate + "(\"" + key + "\")";
|
||||||
|
}
|
||||||
|
}
|
@ -13,6 +13,8 @@ import de.marhali.easyi18n.settings.ProjectSettingsService;
|
|||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dialog to create a new translation with all associated locale values.
|
* Dialog to create a new translation with all associated locale values.
|
||||||
* Supports optional prefill technique for translation key or locale value.
|
* Supports optional prefill technique for translation key or locale value.
|
||||||
@ -20,6 +22,8 @@ import org.jetbrains.annotations.Nullable;
|
|||||||
*/
|
*/
|
||||||
public class AddDialog extends TranslationDialog {
|
public class AddDialog extends TranslationDialog {
|
||||||
|
|
||||||
|
private Consumer<String> onCreated;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a new create dialog with prefilled fields
|
* Constructs a new create dialog with prefilled fields
|
||||||
* @param project Opened project
|
* @param project Opened project
|
||||||
@ -35,6 +39,16 @@ public class AddDialog extends TranslationDialog {
|
|||||||
|
|
||||||
setTitle(bundle.getString("action.add"));
|
setTitle(bundle.getString("action.add"));
|
||||||
}
|
}
|
||||||
|
public AddDialog(@NotNull Project project, @Nullable KeyPath prefillKey, @Nullable String prefillLocale,Consumer<String> onCreated) {
|
||||||
|
super(project, new Translation(prefillKey != null ? prefillKey : new KeyPath(),
|
||||||
|
prefillLocale != null
|
||||||
|
? new TranslationValue(ProjectSettingsService.get(project).getState().getPreviewLocale(), prefillLocale)
|
||||||
|
: null)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.onCreated = onCreated;
|
||||||
|
setTitle(bundle.getString("action.add"));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructs a new create dialog without prefilled fields.
|
* Constructs a new create dialog without prefilled fields.
|
||||||
@ -47,8 +61,13 @@ public class AddDialog extends TranslationDialog {
|
|||||||
@Override
|
@Override
|
||||||
protected @Nullable TranslationUpdate handleExit(int exitCode) {
|
protected @Nullable TranslationUpdate handleExit(int exitCode) {
|
||||||
if(exitCode == DialogWrapper.OK_EXIT_CODE) {
|
if(exitCode == DialogWrapper.OK_EXIT_CODE) {
|
||||||
|
if(onCreated != null) {
|
||||||
|
onCreated.accept(this.getKeyField().getText());
|
||||||
|
}
|
||||||
|
|
||||||
return new TranslationCreate(getState());
|
return new TranslationCreate(getState());
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,6 +68,10 @@ abstract class TranslationDialog extends DialogWrapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public JTextField getKeyField() {
|
||||||
|
return keyField;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers a callback that is called on dialog close with the final state.
|
* Registers a callback that is called on dialog close with the final state.
|
||||||
* If the user aborts the dialog no callback is called.
|
* If the user aborts the dialog no callback is called.
|
||||||
|
@ -33,4 +33,5 @@ public interface ProjectSettings {
|
|||||||
|
|
||||||
// Experimental Configuration
|
// Experimental Configuration
|
||||||
boolean isAlwaysFold();
|
boolean isAlwaysFold();
|
||||||
|
String getFlavorTemplate();
|
||||||
}
|
}
|
||||||
|
@ -64,6 +64,7 @@ public class ProjectSettingsComponent extends ProjectSettingsComponentState {
|
|||||||
.addVerticalGap(24)
|
.addVerticalGap(24)
|
||||||
.addComponent(new TitledSeparator(bundle.getString("settings.experimental.title")))
|
.addComponent(new TitledSeparator(bundle.getString("settings.experimental.title")))
|
||||||
.addComponent(constructAlwaysFoldField())
|
.addComponent(constructAlwaysFoldField())
|
||||||
|
.addLabeledComponent(bundle.getString("settings.experimental.flavor-template"), constructFlavorTemplate(), 1, false)
|
||||||
.addComponentFillVertically(new JPanel(), 0)
|
.addComponentFillVertically(new JPanel(), 0)
|
||||||
.getPanel();
|
.getPanel();
|
||||||
}
|
}
|
||||||
@ -219,9 +220,15 @@ public class ProjectSettingsComponent extends ProjectSettingsComponentState {
|
|||||||
return alwaysFold;
|
return alwaysFold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private JComponent constructFlavorTemplate() {
|
||||||
|
flavorTemplate = new ExtendableTextField(20);
|
||||||
|
flavorTemplate.setToolTipText(bundle.getString("settings.experimental.flavor-template-tooltip"));
|
||||||
|
return flavorTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
private ItemListener handleParserChange() {
|
private ItemListener handleParserChange() {
|
||||||
return e -> {
|
return e -> {
|
||||||
if(e.getStateChange() == ItemEvent.SELECTED) {
|
if (e.getStateChange() == ItemEvent.SELECTED) {
|
||||||
// Automatically suggest file pattern option on parser change
|
// Automatically suggest file pattern option on parser change
|
||||||
ParserStrategyType newStrategy = ParserStrategyType.fromIndex(parserStrategy.getSelectedIndex());
|
ParserStrategyType newStrategy = ParserStrategyType.fromIndex(parserStrategy.getSelectedIndex());
|
||||||
filePattern.setText(newStrategy.getExampleFilePattern());
|
filePattern.setText(newStrategy.getExampleFilePattern());
|
||||||
|
@ -40,6 +40,8 @@ public class ProjectSettingsComponentState {
|
|||||||
// Experimental configuration
|
// Experimental configuration
|
||||||
protected JCheckBox alwaysFold;
|
protected JCheckBox alwaysFold;
|
||||||
|
|
||||||
|
protected JTextField flavorTemplate;
|
||||||
|
|
||||||
protected ProjectSettingsState getState() {
|
protected ProjectSettingsState getState() {
|
||||||
// Every field needs to provide its state
|
// Every field needs to provide its state
|
||||||
ProjectSettingsState state = new ProjectSettingsState();
|
ProjectSettingsState state = new ProjectSettingsState();
|
||||||
@ -63,6 +65,7 @@ public class ProjectSettingsComponentState {
|
|||||||
state.setAssistance(assistance.isSelected());
|
state.setAssistance(assistance.isSelected());
|
||||||
|
|
||||||
state.setAlwaysFold(alwaysFold.isSelected());
|
state.setAlwaysFold(alwaysFold.isSelected());
|
||||||
|
state.setFlavorTemplate(flavorTemplate.getText());
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@ -88,5 +91,6 @@ public class ProjectSettingsComponentState {
|
|||||||
assistance.setSelected(state.isAssistance());
|
assistance.setSelected(state.isAssistance());
|
||||||
|
|
||||||
alwaysFold.setSelected(state.isAlwaysFold());
|
alwaysFold.setSelected(state.isAlwaysFold());
|
||||||
|
flavorTemplate.setText(state.getFlavorTemplate());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,15 @@ public class ProjectSettingsState implements ProjectSettings {
|
|||||||
// Experimental configuration
|
// Experimental configuration
|
||||||
@Property private Boolean alwaysFold;
|
@Property private Boolean alwaysFold;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `flavorTemplate` specifies the format used for replacing strings with their i18n (internationalization) counterparts.
|
||||||
|
* For example:
|
||||||
|
* In many situations, the default representation for i18n follows the `$i18n.t('key')` pattern. However, this can vary depending on
|
||||||
|
* the specific framework or developers' preferences for handling i18n. The ability to dynamically change this template adds flexibility and customization
|
||||||
|
* to cater to different i18n handling methods.
|
||||||
|
*/
|
||||||
|
@Property private String flavorTemplate;
|
||||||
|
|
||||||
public ProjectSettingsState() {
|
public ProjectSettingsState() {
|
||||||
this(new DefaultPreset());
|
this(new DefaultPreset());
|
||||||
}
|
}
|
||||||
@ -65,6 +74,7 @@ public class ProjectSettingsState implements ProjectSettings {
|
|||||||
this.assistance = defaults.isAssistance();
|
this.assistance = defaults.isAssistance();
|
||||||
|
|
||||||
this.alwaysFold = defaults.isAlwaysFold();
|
this.alwaysFold = defaults.isAlwaysFold();
|
||||||
|
this.flavorTemplate = defaults.getFlavorTemplate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -143,6 +153,11 @@ public class ProjectSettingsState implements ProjectSettings {
|
|||||||
return alwaysFold;
|
return alwaysFold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getFlavorTemplate() {
|
||||||
|
return this.flavorTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
public void setLocalesDirectory(String localesDirectory) {
|
public void setLocalesDirectory(String localesDirectory) {
|
||||||
this.localesDirectory = localesDirectory;
|
this.localesDirectory = localesDirectory;
|
||||||
}
|
}
|
||||||
@ -203,6 +218,10 @@ public class ProjectSettingsState implements ProjectSettings {
|
|||||||
this.alwaysFold = alwaysFold;
|
this.alwaysFold = alwaysFold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setFlavorTemplate(String flavorTemplate){
|
||||||
|
this.flavorTemplate = flavorTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if (this == o) return true;
|
if (this == o) return true;
|
||||||
@ -222,7 +241,8 @@ public class ProjectSettingsState implements ProjectSettings {
|
|||||||
&& Objects.equals(previewLocale, that.previewLocale)
|
&& Objects.equals(previewLocale, that.previewLocale)
|
||||||
&& Objects.equals(nestedKeys, that.nestedKeys)
|
&& Objects.equals(nestedKeys, that.nestedKeys)
|
||||||
&& Objects.equals(assistance, that.assistance)
|
&& Objects.equals(assistance, that.assistance)
|
||||||
&& Objects.equals(alwaysFold, that.alwaysFold);
|
&& Objects.equals(alwaysFold, that.alwaysFold)
|
||||||
|
&& Objects.equals(flavorTemplate,that.flavorTemplate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -230,7 +250,7 @@ public class ProjectSettingsState implements ProjectSettings {
|
|||||||
return Objects.hash(
|
return Objects.hash(
|
||||||
localesDirectory, folderStrategy, parserStrategy, filePattern, includeSubDirs,
|
localesDirectory, folderStrategy, parserStrategy, filePattern, includeSubDirs,
|
||||||
sorting, namespaceDelimiter, sectionDelimiter, contextDelimiter, pluralDelimiter,
|
sorting, namespaceDelimiter, sectionDelimiter, contextDelimiter, pluralDelimiter,
|
||||||
defaultNamespace, previewLocale, nestedKeys, assistance, alwaysFold
|
defaultNamespace, previewLocale, nestedKeys, assistance, alwaysFold,flavorTemplate
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,6 +272,7 @@ public class ProjectSettingsState implements ProjectSettings {
|
|||||||
", nestedKeys=" + nestedKeys +
|
", nestedKeys=" + nestedKeys +
|
||||||
", assistance=" + assistance +
|
", assistance=" + assistance +
|
||||||
", alwaysFold=" + alwaysFold +
|
", alwaysFold=" + alwaysFold +
|
||||||
|
", flavorTemplate=" + flavorTemplate +
|
||||||
'}';
|
'}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -86,4 +86,9 @@ public class DefaultPreset implements ProjectSettings {
|
|||||||
public boolean isAlwaysFold() {
|
public boolean isAlwaysFold() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getFlavorTemplate() {
|
||||||
|
return "$i18n.t";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -86,4 +86,9 @@ public class ReactI18NextPreset implements ProjectSettings {
|
|||||||
public boolean isAlwaysFold() {
|
public boolean isAlwaysFold() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getFlavorTemplate() {
|
||||||
|
return "$i18n.t";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,4 +85,9 @@ public class VueI18nPreset implements ProjectSettings {
|
|||||||
public boolean isAlwaysFold() {
|
public boolean isAlwaysFold() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getFlavorTemplate() {
|
||||||
|
return "$i18n.t";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
36
src/main/java/de/marhali/easyi18n/util/DocumentUtil.java
Normal file
36
src/main/java/de/marhali/easyi18n/util/DocumentUtil.java
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package de.marhali.easyi18n.util;
|
||||||
|
|
||||||
|
import com.intellij.openapi.editor.Document;
|
||||||
|
import com.intellij.openapi.fileEditor.FileDocumentManager;
|
||||||
|
import com.intellij.openapi.fileTypes.FileType;
|
||||||
|
import com.intellij.openapi.vfs.VirtualFile;
|
||||||
|
|
||||||
|
public class DocumentUtil {
|
||||||
|
protected Document document;
|
||||||
|
private FileType fileType;
|
||||||
|
|
||||||
|
public DocumentUtil(Document document) {
|
||||||
|
setDocument(document);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Document getDocument() {
|
||||||
|
return document;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDocument(Document document) {
|
||||||
|
this.document = document;
|
||||||
|
FileDocumentManager fileDocumentManager = FileDocumentManager.getInstance();
|
||||||
|
VirtualFile virtualFile = fileDocumentManager.getFile(document);
|
||||||
|
if (virtualFile != null) {
|
||||||
|
fileType = virtualFile.getFileType();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isJsOrTs() {
|
||||||
|
return (fileType.getDefaultExtension().contains("js") || fileType.getDescription().contains("ts"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isVue() {
|
||||||
|
return fileType.getDefaultExtension().contains("vue");
|
||||||
|
}
|
||||||
|
}
|
@ -22,6 +22,13 @@
|
|||||||
>
|
>
|
||||||
<add-to-group group-id="NewGroup"/>
|
<add-to-group group-id="NewGroup"/>
|
||||||
</action>
|
</action>
|
||||||
|
<action id="de.marhali.easyi18n.action.LocalizeItAction"
|
||||||
|
class="de.marhali.easyi18n.action.LocalizeItAction"
|
||||||
|
text="Localize It"
|
||||||
|
description="Apply localization to the selected string"
|
||||||
|
icon="/icons/translate13.svg">
|
||||||
|
<add-to-group group-id="EditorPopupMenu" anchor="last"/>
|
||||||
|
</action>
|
||||||
</actions>
|
</actions>
|
||||||
|
|
||||||
<extensions defaultExtensionNs="com.intellij">
|
<extensions defaultExtensionNs="com.intellij">
|
||||||
|
@ -60,6 +60,8 @@ settings.editor.assistance.tooltip=Activates editor support to reference, auto-c
|
|||||||
settings.experimental.title=Experimental Configuration
|
settings.experimental.title=Experimental Configuration
|
||||||
settings.experimental.always-fold.title=Always fold translation keys
|
settings.experimental.always-fold.title=Always fold translation keys
|
||||||
settings.experimental.always-fold.tooltip=Forces the editor to always display the value behind a translation key. The value cannot be unfolded when this function is active.
|
settings.experimental.always-fold.tooltip=Forces the editor to always display the value behind a translation key. The value cannot be unfolded when this function is active.
|
||||||
|
settings.experimental.flavor-template =I18n flavor template
|
||||||
|
settings.experimental.flavor-template-tooltip = Specify how to replace strings with i18n representation.
|
||||||
error.io=An error occurred while processing translation files. \n\
|
error.io=An error occurred while processing translation files. \n\
|
||||||
Config: {0} => {1} ({2}) \n\
|
Config: {0} => {1} ({2}) \n\
|
||||||
Path: {3} \n\
|
Path: {3} \n\
|
||||||
|
@ -163,6 +163,11 @@ public class KeyPathConverterTest {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getFlavorTemplate() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isIncludeSubDirs() {
|
public boolean isIncludeSubDirs() {
|
||||||
return false;
|
return false;
|
||||||
|
@ -236,6 +236,11 @@ public class PropertiesMapperTest extends AbstractMapperTest {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getFlavorTemplate() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isIncludeSubDirs() {
|
public boolean isIncludeSubDirs() {
|
||||||
return false;
|
return false;
|
||||||
|
@ -85,4 +85,9 @@ public class SettingsTestPreset implements ProjectSettings {
|
|||||||
public boolean isAlwaysFold() {
|
public boolean isAlwaysFold() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getFlavorTemplate() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user