working on meaningfull messages for task and project related events
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
@@ -64,7 +64,7 @@ public class Application {
|
||||
|
||||
var server = HttpServer.create(new InetSocketAddress(port), 0);
|
||||
try {
|
||||
new Translations().bindPath("/api/translations").on(server);
|
||||
new Translations(config).bindPath("/api/translations").on(server);
|
||||
new JournalModule(config).bindPath("/api/journal").on(server);
|
||||
new MessageApi().bindPath("/api/bus").on(server);
|
||||
new MessageSystem(config).bindPath("/api/message").on(server);
|
||||
|
||||
@@ -18,6 +18,7 @@ public abstract class Event<Payload extends Mappable> {
|
||||
|
||||
public enum EventType {
|
||||
CREATE,
|
||||
MEMBER_ADDED,
|
||||
UPDATE,
|
||||
DELETE;
|
||||
}
|
||||
@@ -59,7 +60,7 @@ public abstract class Event<Payload extends Mappable> {
|
||||
}
|
||||
|
||||
public Optional<String> diff(){
|
||||
return oldData == null ? empty() : of(Diff.MapDiff.diff(dropMarkdown(oldData),dropMarkdown(payload.toMap())));
|
||||
return oldData == null ? empty() : of(Diff.MapDiff.diff(filter(oldData),filter(payload.toMap())));
|
||||
}
|
||||
|
||||
|
||||
@@ -67,6 +68,10 @@ public abstract class Event<Payload extends Mappable> {
|
||||
return eventType;
|
||||
}
|
||||
|
||||
protected Map<String,Object> filter(Map<String,Object> map){
|
||||
return dropMarkdown(map);
|
||||
}
|
||||
|
||||
public boolean isIntendedFor(UmbrellaUser user){
|
||||
return audience().contains(user);
|
||||
}
|
||||
@@ -93,6 +98,10 @@ public abstract class Event<Payload extends Mappable> {
|
||||
return module;
|
||||
}
|
||||
|
||||
protected Map<String, Object> oldData() {
|
||||
return oldData;
|
||||
}
|
||||
|
||||
public Payload payload(){
|
||||
return payload;
|
||||
}
|
||||
|
||||
@@ -1,23 +1,36 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.messagebus.events;
|
||||
|
||||
import static de.srsoftware.umbrella.core.constants.Field.OBJECT;
|
||||
import static de.srsoftware.umbrella.core.constants.Field.USER;
|
||||
import static de.srsoftware.umbrella.core.ModuleRegistry.projectService;
|
||||
import static de.srsoftware.umbrella.core.ModuleRegistry.taskService;
|
||||
import static de.srsoftware.umbrella.core.constants.Field.*;
|
||||
import static de.srsoftware.umbrella.core.constants.Module.PROJECT;
|
||||
import static de.srsoftware.umbrella.core.model.Translatable.t;
|
||||
import static de.srsoftware.umbrella.messagebus.events.Event.EventType.MEMBER_ADDED;
|
||||
|
||||
import de.srsoftware.umbrella.core.constants.Field;
|
||||
import de.srsoftware.umbrella.core.model.*;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
public class ProjectEvent extends Event<Project>{
|
||||
|
||||
private UmbrellaUser newMember;
|
||||
public ProjectEvent(UmbrellaUser initiator, Project project, EventType type){
|
||||
super(initiator, PROJECT, project, type);
|
||||
newMember = null;
|
||||
}
|
||||
|
||||
public ProjectEvent(UmbrellaUser initiator, Project project, Map<String, Object> oldData){
|
||||
super(initiator, PROJECT, project, oldData);
|
||||
newMember = null;
|
||||
}
|
||||
|
||||
public ProjectEvent(UmbrellaUser initiator, Project project, UmbrellaUser newMember){
|
||||
super(initiator, PROJECT, project, MEMBER_ADDED);
|
||||
this.newMember = newMember;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -27,10 +40,30 @@ public class ProjectEvent extends Event<Project>{
|
||||
|
||||
@Override
|
||||
public Translatable describe() {
|
||||
return diff().map(UnTranslatable::new).map(Translatable.class::cast)
|
||||
.orElseGet(() -> t("[TODO: {object}.describe]",OBJECT,this.getClass().getSimpleName()));
|
||||
return switch (eventType()){
|
||||
case CREATE -> describeCreate();
|
||||
case DELETE -> t("The project '{project}' has been deleted by {user}", Field.PROJECT, payload().name(), USER, initiator().name());
|
||||
case MEMBER_ADDED -> describeMemberAdded();
|
||||
case UPDATE -> describeUpdate();
|
||||
};
|
||||
}
|
||||
|
||||
private Translatable describeCreate() {
|
||||
var head = t("You have been added to the new project '{project}', created by {user}':\n\n{body}", Field.PROJECT, payload().name(), BODY, payload().description(), USER, initiator().name());
|
||||
return t("{head}\n\n{link}","head",head,"link",link());
|
||||
}
|
||||
|
||||
private Translatable describeMemberAdded() {
|
||||
var head = t("'{name}' has been added to '{project}' by '{user}'.",NAME,newMember.name(),Field.PROJECT,payload().name(),USER,initiator().name());
|
||||
return t("{head}\n\n{link}","head",head,"link",link());
|
||||
}
|
||||
|
||||
private Translatable describeUpdate() {
|
||||
var head = t("Changes in project '{project}':\n\n{body}",Field.PROJECT,payload().name(),BODY,diff().orElse(""));
|
||||
return t("{head}\n\n{link}","head",head,"link",link());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean isIntendedFor(UmbrellaUser user) {
|
||||
for (var member : payload().members().values()){
|
||||
@@ -39,8 +72,13 @@ public class ProjectEvent extends Event<Project>{
|
||||
return false;
|
||||
}
|
||||
|
||||
private Translatable link() {
|
||||
return t("You can view/edit this project at {base_url}/project/{id}/view",ID,payload().id());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Translatable subject() {
|
||||
return t("{user} edited {object}",USER,initiator(),OBJECT,payload());
|
||||
return t("{user} edited {object}",USER,initiator(),OBJECT,payload().name());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import static de.srsoftware.umbrella.core.ModuleRegistry.taskService;
|
||||
import static de.srsoftware.umbrella.core.constants.Field.*;
|
||||
import static de.srsoftware.umbrella.core.constants.Module.TASK;
|
||||
import static de.srsoftware.umbrella.core.model.Translatable.t;
|
||||
import static de.srsoftware.umbrella.messagebus.events.Event.EventType.MEMBER_ADDED;
|
||||
|
||||
import de.srsoftware.umbrella.core.ModuleRegistry;
|
||||
import de.srsoftware.umbrella.core.constants.Field;
|
||||
@@ -17,12 +18,21 @@ import java.util.Map;
|
||||
|
||||
|
||||
public class TaskEvent extends Event<Task>{
|
||||
private final UmbrellaUser newMember;
|
||||
|
||||
public TaskEvent(UmbrellaUser initiator, Task task, EventType type){
|
||||
super(initiator, TASK, task, type);
|
||||
newMember = null;
|
||||
}
|
||||
|
||||
public TaskEvent(UmbrellaUser initiator, Task task, Map<String, Object> oldData){
|
||||
super(initiator, TASK, task, oldData);
|
||||
newMember = null;
|
||||
}
|
||||
|
||||
public TaskEvent(UmbrellaUser initiator, Task task, UmbrellaUser newMember) {
|
||||
super(initiator, TASK, task, MEMBER_ADDED);
|
||||
this.newMember = newMember;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -33,25 +43,45 @@ public class TaskEvent extends Event<Task>{
|
||||
@Override
|
||||
public Translatable describe() {
|
||||
return switch (eventType()){
|
||||
case CREATE -> {
|
||||
String parentName = null;
|
||||
var pid = payload().parentTaskId();
|
||||
if (pid != null) {
|
||||
var parent = taskService().load(List.of(pid)).get(pid);
|
||||
if (parent != null) parentName = parent.name();
|
||||
}
|
||||
if (parentName == null){
|
||||
var project = projectService().load(payload().projectId());
|
||||
if (project != null) parentName = project.name();
|
||||
}
|
||||
if (parentName == null) parentName = "?";
|
||||
yield t("'{task}' has been added to '{object}':\n\n{body}", Field.TASK, payload().name(), OBJECT, parentName, BODY, payload().description());
|
||||
}
|
||||
case CREATE -> describeCreate();
|
||||
case DELETE -> t("The task '{task}' has been deleted by {user}",Field.TASK, payload().name(), USER, initiator().name());
|
||||
case UPDATE -> t("Changes in task '{task}':\n\n{body}",Field.TASK,payload().name(),BODY,diff().orElse(""));
|
||||
case MEMBER_ADDED -> describeMemberAdded();
|
||||
case UPDATE -> describeUpdate();
|
||||
};
|
||||
}
|
||||
|
||||
private Translatable describeMemberAdded() {
|
||||
var head = t("'{name}' has been added to '{task}' by '{user}'.",NAME,newMember.name(),Field.TASK,payload().name(),USER,initiator().name());
|
||||
return t("{head}\n\n{link}","head",head,"link",link());
|
||||
}
|
||||
|
||||
private Translatable describeCreate() {
|
||||
String parentName = null;
|
||||
var pid = payload().parentTaskId();
|
||||
if (pid != null) {
|
||||
var parent = taskService().load(List.of(pid)).get(pid);
|
||||
if (parent != null) parentName = parent.name();
|
||||
}
|
||||
if (parentName == null){
|
||||
var project = projectService().load(payload().projectId());
|
||||
if (project != null) parentName = project.name();
|
||||
}
|
||||
if (parentName == null) parentName = "?";
|
||||
var head = t("'{task}' has been added to '{object}':\n\n{body}", Field.TASK, payload().name(), OBJECT, parentName, BODY, payload().description());
|
||||
return t("{head}\n\n{link}","head",head,"link",link());
|
||||
}
|
||||
|
||||
private Translatable describeUpdate() {
|
||||
var head = t("Changes in task '{task}':\n\n{body}",Field.TASK,payload().name(),BODY,diff().orElse(""));
|
||||
return t("{head}\n\n{link}","head",head,"link",link());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> filter(Map<String, Object> map) {
|
||||
map.remove(MEMBERS);
|
||||
return super.filter(map);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isIntendedFor(UmbrellaUser user) {
|
||||
for (var member : payload().members().values()){
|
||||
@@ -60,11 +90,15 @@ public class TaskEvent extends Event<Task>{
|
||||
return false;
|
||||
}
|
||||
|
||||
private Translatable link() {
|
||||
return t("You can view/edit this task at {base_url}/task/{id}/view",ID,payload().id());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Translatable subject() {
|
||||
return switch (eventType()){
|
||||
case CREATE -> t("The task '{task}' has been created", Field.TASK, payload().name());
|
||||
case DELETE -> t("The task '{task}' has been deleted",Field.TASK, payload().name());
|
||||
case UPDATE -> t("Task '{task} was edited",Field.TASK,payload().name());
|
||||
case MEMBER_ADDED, UPDATE -> t("Task '{task}' was edited",Field.TASK,payload().name());
|
||||
};
|
||||
}}
|
||||
|
||||
@@ -9,6 +9,7 @@ public class Field {
|
||||
public static final String ATTACHMENTS = "attachments";
|
||||
public static final String AUTHORIZATION = "Authorization";
|
||||
|
||||
public static final String BASE_URL = "umbrella.base_url";
|
||||
public static final String BANK_ACCOUNT = "bank_account";
|
||||
public static final String BODY = "body";
|
||||
|
||||
@@ -114,6 +115,7 @@ public class Field {
|
||||
public static final String PRICE = "single_price";
|
||||
public static final String PRICE_FORMAT = "price_format";
|
||||
public static final String PRIORITY = "priority";
|
||||
public static final String PROJECT = "project";
|
||||
public static final String PROJECT_ID = "project_id";
|
||||
public static final String PROPERTIES = "properties";
|
||||
|
||||
|
||||
@@ -50,11 +50,12 @@ public class ProjectModule extends BaseHandler implements ProjectService {
|
||||
ModuleRegistry.add(this);
|
||||
}
|
||||
|
||||
private void addMember(Project project, long userId) {
|
||||
private UmbrellaUser addMember(Project project, long userId) {
|
||||
var user = userService().loadUser(userId);
|
||||
var member = new Member(user,READ_ONLY);
|
||||
project.members().put(userId,member);
|
||||
project.dirty(MEMBERS);
|
||||
return user;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -217,12 +218,13 @@ public class ProjectModule extends BaseHandler implements ProjectService {
|
||||
if (!project.hasMember(user)) throw notAmember(t(PROJECT_WITH_ID,ID,project.name()));
|
||||
var old = project.toMap();
|
||||
var json = json(ex);
|
||||
UmbrellaUser newMember = null;
|
||||
if (json.has(DROP_MEMBER) && json.get(DROP_MEMBER) instanceof Number id) dropMember(project,id.longValue());
|
||||
if (json.has(MEMBERS) && json.get(MEMBERS) instanceof JSONObject memberJson) patchMembers(project,memberJson);
|
||||
if (json.has(NEW_MEMBER) && json.get(NEW_MEMBER) instanceof Number num) addMember(project,num.longValue());
|
||||
if (json.has(NEW_MEMBER) && json.get(NEW_MEMBER) instanceof Number num) newMember = addMember(project,num.longValue());
|
||||
|
||||
project = projectDb.save(project.patch(json), user);
|
||||
messageBus().dispatch(new ProjectEvent(user,project, old));
|
||||
messageBus().dispatch(newMember != null ? new ProjectEvent(user,project,newMember) : new ProjectEvent(user,project, old));
|
||||
return sendContent(ex,project.toMap());
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import static de.srsoftware.tools.jdbc.Query.SelectQuery.ALL;
|
||||
import static de.srsoftware.umbrella.core.Errors.*;
|
||||
import static de.srsoftware.umbrella.core.constants.Constants.TABLE_SETTINGS;
|
||||
import static de.srsoftware.umbrella.core.constants.Field.*;
|
||||
import static de.srsoftware.umbrella.core.constants.Field.PROJECT;
|
||||
import static de.srsoftware.umbrella.core.constants.Field.TYPE;
|
||||
import static de.srsoftware.umbrella.core.constants.Text.*;
|
||||
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.*;
|
||||
|
||||
@@ -7,6 +7,7 @@ import static de.srsoftware.umbrella.core.ModuleRegistry.*;
|
||||
import static de.srsoftware.umbrella.core.ResponseCode.HTTP_NOT_IMPLEMENTED;
|
||||
import static de.srsoftware.umbrella.core.Util.mapValues;
|
||||
import static de.srsoftware.umbrella.core.constants.Field.*;
|
||||
import static de.srsoftware.umbrella.core.constants.Field.PROJECT;
|
||||
import static de.srsoftware.umbrella.core.constants.Field.TAGS;
|
||||
import static de.srsoftware.umbrella.core.constants.Field.TASKS;
|
||||
import static de.srsoftware.umbrella.core.constants.Module.TASK;
|
||||
@@ -56,11 +57,12 @@ public class TaskModule extends BaseHandler implements TaskService {
|
||||
ModuleRegistry.add(this);
|
||||
}
|
||||
|
||||
private void addMember(Task task, long userId) {
|
||||
private UmbrellaUser addMember(Task task, long userId) {
|
||||
var user = userService().loadUser(userId);
|
||||
var member = new Member(user, READ_ONLY);
|
||||
task.members().put(userId, member);
|
||||
task.dirty(MEMBERS);
|
||||
return user;
|
||||
}
|
||||
|
||||
private boolean deleteTask(HttpExchange ex, long taskId, UmbrellaUser user) throws IOException {
|
||||
@@ -328,12 +330,15 @@ public class TaskModule extends BaseHandler implements TaskService {
|
||||
var member = task.members().get(user.id());
|
||||
if (member == null || member.permission() == READ_ONLY) throw forbidden("You are not a allowed to edit {object}!", OBJECT, task.name());
|
||||
var json = json(ex);
|
||||
if (json.has(PARENT_TASK_ID) && json.get(PARENT_TASK_ID) instanceof Number ptid && newParentIsSubtask(task, ptid.longValue())) throw forbidden("Task must not be sub-task of itself.");
|
||||
|
||||
UmbrellaUser newMember = null;
|
||||
if (json.has(DROP_MEMBER) && json.get(DROP_MEMBER) instanceof Number id) dropMember(task, id.longValue());
|
||||
if (json.has(MEMBERS) && json.get(MEMBERS) instanceof JSONObject memberJson) patchMembers(task, memberJson);
|
||||
if (json.has(NEW_MEMBER) && json.get(NEW_MEMBER) instanceof Number num) addMember(task, num.longValue());
|
||||
if (json.has(PARENT_TASK_ID) && json.get(PARENT_TASK_ID) instanceof Number ptid && newParentIsSubtask(task, ptid.longValue())) throw forbidden("Task must not be sub-task of itself.");
|
||||
if (json.has(NEW_MEMBER) && json.get(NEW_MEMBER) instanceof Number num) newMember = addMember(task, num.longValue());
|
||||
|
||||
task = taskDb.save(task.patch(json)).tags(tagService().getTags(TASK, taskId, user));
|
||||
messageBus().dispatch(new TaskEvent(user, task, old));
|
||||
messageBus().dispatch(newMember != null ? new TaskEvent(user, task, newMember) : new TaskEvent(user, task, old));
|
||||
return sendContent(ex, task);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
/* © SRSoftware 2025 */
|
||||
package de.srsoftware.umbrella.translations;
|
||||
|
||||
import static de.srsoftware.umbrella.core.constants.Field.BASE_URL;
|
||||
import static de.srsoftware.umbrella.core.exceptions.UmbrellaException.missingField;
|
||||
import static java.lang.System.Logger.Level.WARNING;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import de.srsoftware.configuration.Configuration;
|
||||
import de.srsoftware.tools.Path;
|
||||
import de.srsoftware.tools.PathHandler;
|
||||
import de.srsoftware.umbrella.core.ModuleRegistry;
|
||||
@@ -14,15 +17,21 @@ import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class Translations extends PathHandler implements Translator {
|
||||
private static final System.Logger LOG = System.getLogger("Translations");
|
||||
private final String baseUrl;
|
||||
|
||||
private HashMap<String, JSONObject> translations = new HashMap<>();
|
||||
|
||||
public Translations() {
|
||||
public Translations(Configuration config) {
|
||||
ModuleRegistry.add(this);
|
||||
Optional<String> baseUrl = config.get(BASE_URL);
|
||||
if (baseUrl.isEmpty()) throw missingField(BASE_URL);
|
||||
this.baseUrl = baseUrl.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -70,7 +79,7 @@ public class Translations extends PathHandler implements Translator {
|
||||
if (fills != null) for (var entry : fills.entrySet()) {
|
||||
translated = translated.replaceAll("\\{"+entry.getKey()+"}",entry.getValue());
|
||||
}
|
||||
return translated;
|
||||
return translated.replaceAll("\\{base_url}",baseUrl);
|
||||
} catch (IOException e) {
|
||||
return text;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"by": "von",
|
||||
|
||||
"cancel": "abbrechen",
|
||||
"Changes in task '{task}':\n\n{body}": "Änderungen an Aufgabe '{task}':\n\n{body}",
|
||||
"choose_type": "Typ wählen",
|
||||
"click_to_edit": "Anklicken zum Bearbeiten",
|
||||
"client_id": "Client-ID",
|
||||
@@ -218,6 +219,7 @@
|
||||
"my files": "Meine Dateien",
|
||||
|
||||
"name": "Name",
|
||||
"'{name}' has been added to '{task}' by '{user}'.": "'{name}' wurde von {user} zu '{task}' hinzugefügt.",
|
||||
"net_price": "Nettopreis",
|
||||
"net_sum": "Netto-Summe",
|
||||
"new_contact": "neuer Kontakt",
|
||||
@@ -332,6 +334,8 @@
|
||||
"tags": "Tags",
|
||||
"task": "Aufgabe",
|
||||
"task_list": "Aufgabenliste",
|
||||
"'{task}' has been added to '{object}':\n\n{body}": "'{task}' wurde zu '{object}' hinzugefügt:\n\n{body}",
|
||||
"Task '{task}' was edited": "Aufgabe '{task}' wurde bearbeitet",
|
||||
"tasks": "Aufgaben",
|
||||
"tasks_for_tag": "Aufgaben mit Tag „{tag}“",
|
||||
"tax_id": "Steuernummer",
|
||||
@@ -339,6 +343,9 @@
|
||||
"tax_rate": "Steuersatz",
|
||||
"template": "Vorlage",
|
||||
"theme": "Design",
|
||||
"The task '{task}' has been created": "Die Aufgabe '{task}' wurde angelegt",
|
||||
"The task '{task}' has been deleted": "Die Aufgabe '{task}' wurde gelöscht",
|
||||
"The task '{task}' has been deleted by {user}": "Die Aufgabe '{task}' wurde von {user} bearbeitet",
|
||||
"time ({id})": "Zeit ({id})",
|
||||
"times": "Zeiten",
|
||||
"timetracking": "Zeiterfassung",
|
||||
@@ -385,6 +392,7 @@
|
||||
"visible_to_guests": "Für Besucher sichtbar",
|
||||
|
||||
"year": "Jahr",
|
||||
"You can view/edit this task at {base_url}/task/{id}/view": "Du kannst diese Aufgabe unter {base_url}/task/{id}/view ansehen/bearbeiten.",
|
||||
"Your token to create a new password" : "Ihr Token zum Erstellen eines neuen Passworts",
|
||||
"your_profile": "dein Profil"
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"by": "by",
|
||||
|
||||
"cancel": "cancel",
|
||||
"Changes in task '{task}':\n\n{body}": "Changes in task '{task}':\n\n{body}",
|
||||
"choose_type": "choose type",
|
||||
"click_to_edit": "click to edit",
|
||||
"client_id": "client ID",
|
||||
@@ -218,6 +219,7 @@
|
||||
"my files": "my files",
|
||||
|
||||
"name": "Name",
|
||||
"'{name}' has been added to '{task}' by '{user}'.": "'{name}' has been added to '{task}' by '{user}'.",
|
||||
"net_price": "net price",
|
||||
"net_sum": "net sum",
|
||||
"new_contact": "new contact",
|
||||
@@ -332,6 +334,8 @@
|
||||
"tags": "tags",
|
||||
"task": "task",
|
||||
"task_list": "task list",
|
||||
"'{task}' has been added to '{object}':\n\n{body}": "'{task}' has been added to '{object}':\n\n{body}",
|
||||
"Task '{task}' was edited": "Task '{task}' was edited",
|
||||
"tasks": "tasks",
|
||||
"tasks_for_tag": "tasks with tag „{tag}“",
|
||||
"tax_id": "tax ID",
|
||||
@@ -339,6 +343,9 @@
|
||||
"tax_rate": "tax rate",
|
||||
"template": "template",
|
||||
"theme": "design",
|
||||
"The task '{task}' has been created": "The task '{task}' has been created",
|
||||
"The task '{task}' has been deleted": "The task '{task}' has been deleted",
|
||||
"The task '{task}' has been deleted by {user}": "The task '{task}' has been deleted by {user}",
|
||||
"time ({id})": "time ({id})",
|
||||
"times": "times",
|
||||
"timetracking": "time tracking",
|
||||
@@ -385,6 +392,7 @@
|
||||
"visible_to_guests": "visible to guests",
|
||||
|
||||
"year": "year",
|
||||
"You can view/edit this task at {base_url}/task/{id}/view": "You can view/edit this task at {base_url}/task/{id}/view",
|
||||
"Your token to create a new password" : "Your token to create a new password",
|
||||
"your_profile": "your profile"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user