Skip to content

Commit 76de7c3

Browse files
implement i18n for English and Russian
1 parent 0eaa954 commit 76de7c3

16 files changed

Lines changed: 890 additions & 99 deletions

package-lock.json

Lines changed: 513 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"devDependencies": {
2727
"@eslint/js": "^10.0.1",
2828
"clean-webpack-plugin": "^3.0.0",
29+
"copy-webpack-plugin": "^14.0.0",
2930
"css-loader": "^5.1.2",
3031
"eslint": "^10.1.0",
3132
"globals": "^17.4.0",
@@ -40,8 +41,10 @@
4041
"@popperjs/core": "^2.9.1",
4142
"bootstrap": "^5.3.8",
4243
"drag-drop-touch": "^1.3.1",
44+
"i18next": "^26.0.3",
45+
"i18next-http-backend": "^3.0.4",
4346
"postcss": "^8.5.8",
4447
"sweetalert2": "^11.26.24",
4548
"uuid": "^3.4.0"
4649
}
47-
}
50+
}

src/app.js

Lines changed: 89 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ import loggedInTemplate from "./templates/header/loggedIn.html";
1313

1414
// Module exports go here
1515
import "./modules/dragDrop";
16+
import { t } from "./modules/i18n";
17+
import "./modules/i18n-ui";
18+
// You could also do this, to ensure that i18n is loaded before the rest of the app:
19+
// import initI18n from "./modules/i18n";
20+
// await initI18n();
21+
// But it can be bad.
1622

1723
import Swal from "sweetalert2";
1824
import { User } from "./models/User";
@@ -71,13 +77,13 @@ function queryElements() {
7177
finishedTasksDiv = contentDiv.querySelector("[data-group=finished]");
7278
finishedTasksUl = finishedTasksDiv.querySelector(".task-list");
7379

74-
backlogAddButton = backlogTasksDiv.querySelector(".task-add-button");
80+
backlogAddButton = backlogTasksDiv.querySelector("#backlog-task-add-button");
7581
backlogSubmitButton = backlogTasksDiv.querySelector(".task-submit-button");
7682
addInputWrapper = contentDiv.querySelector(".add-input-wrapper");
7783
addInput = addInputWrapper.querySelector(".add-input");
7884
}
7985

80-
document.addEventListener("submit", function (event) {
86+
document.addEventListener("submit", async function (event) {
8187
if (event.target.matches("#app-login-form")) {
8288
event.preventDefault();
8389
const formData = new FormData(event.target);
@@ -101,6 +107,7 @@ document.addEventListener("submit", function (event) {
101107
const userContextmenu = headerRight.querySelector(".user-contextmenu");
102108
const listItem = document.createElement("li");
103109
listItem.classList.add("contextmenu-item");
110+
listItem.dataset.i18n = "manage-users";
104111
listItem.textContent = "Manage users";
105112
listItem.dataset.action = "manage-users";
106113
userContextmenu.prepend(listItem);
@@ -125,7 +132,9 @@ document.addEventListener("submit", function (event) {
125132
const [login, currentPassword, newPassword, newPasswordConfirmation] =
126133
formData.values();
127134
if (login === appState.currentUser.login && !newPassword) {
128-
swal.fire("You have not changed anything; aborting.");
135+
swal.fire(
136+
t("not-changed-anything", "You have not changed anything; aborting."),
137+
);
129138
return;
130139
}
131140

@@ -135,26 +144,35 @@ document.addEventListener("submit", function (event) {
135144
}
136145

137146
if (!login) {
138-
invalidate(loginInput, "The login must not be empty.");
147+
invalidate(loginInput, t("empty-login", "The login must not be empty."));
139148
}
140149
if (newPassword) {
141150
if (!currentPassword) {
142151
invalidate(
143152
currentPasswordInput,
144-
"You must confirm your current password to change it.",
153+
t(
154+
"must-confirm-current-password",
155+
"You must confirm your current password to change it.",
156+
),
145157
);
146158
} else if (currentPassword !== appState.currentUser.password) {
147159
invalidate(
148160
currentPasswordInput,
149-
"The provided password does not match your password.",
161+
t(
162+
"wrong-password",
163+
"The provided password does not match your password.",
164+
),
150165
);
151166
} else if (!newPasswordConfirmation) {
152167
invalidate(
153168
confirmNewPasswordInput,
154-
"You must confirm your new password.",
169+
t("must-confirm-new-password", "You must confirm your new password."),
155170
);
156171
} else if (newPassword !== newPasswordConfirmation) {
157-
invalidate(confirmNewPasswordInput, "Both passwords should match.");
172+
invalidate(
173+
confirmNewPasswordInput,
174+
t("passwords-do-not-match", "Both passwords should match."),
175+
);
158176
}
159177
}
160178

@@ -207,14 +225,17 @@ document.body.addEventListener("keyup", (event) => {
207225
});
208226

209227
document.body.addEventListener("click", (event) => {
210-
if (event.target === backlogAddButton) {
211-
if (addInputWrapper.classList.contains("d-none")) {
228+
if (
229+
backlogAddButton &&
230+
event.target.closest("#backlog-task-add-button") === backlogAddButton
231+
) {
232+
if (addInputWrapper.classList?.contains("d-none")) {
212233
addInputWrapper.classList.remove("d-none");
213234
}
214235
addInput.focus();
215236
backlogAddButton.classList.add("d-none");
216237
backlogSubmitButton.classList.remove("d-none");
217-
} else if (event.target.matches(".task-add-button")) {
238+
} else if (event.target.closest(".task-add-button")) {
218239
// Populate the select with valid options
219240
const sourceCategory = taskCategoryRelationships.get(
220241
event.target.closest(".task-group").dataset.group,
@@ -224,7 +245,9 @@ document.body.addEventListener("click", (event) => {
224245
appState.currentUser.canReadTask(task) &&
225246
task.category === sourceCategory,
226247
);
227-
const select = event.target.previousElementSibling;
248+
const select = event.target
249+
.closest(".task-group-controls")
250+
.querySelector(".card-select");
228251
function createOption(value, content, disabled = false, selected = false) {
229252
const option = document.createElement("option");
230253
option.value = value;
@@ -233,15 +256,17 @@ document.body.addEventListener("click", (event) => {
233256
if (selected) option.selected = "selected";
234257
return option;
235258
}
236-
const options = [createOption("", "Select a task...", true, true)];
259+
const options = [
260+
createOption("", t("select-a-task", "Select a task..."), true, true),
261+
];
237262
for (const task of sourceTasks) {
238263
options.push(createOption(task.id, task.title));
239264
}
240265

241266
select.innerHTML = "";
242267
select.append(...options);
243268
select.classList.remove("d-none");
244-
event.target.classList.add("d-none");
269+
event.target.closest(".task-add-button").classList.add("d-none");
245270
} else if (
246271
event.target !== addInput &&
247272
!addInputWrapper?.classList.contains("d-none") &&
@@ -289,7 +314,9 @@ document.body.addEventListener("click", (event) => {
289314
break;
290315
}
291316
default: {
292-
alert(`Unknown action ${action}`);
317+
alert(
318+
`${t("something-went-wrong", "Something went wrong")}: unknown action ${action}`,
319+
);
293320
break;
294321
}
295322
}
@@ -306,7 +333,7 @@ document.body.addEventListener("click", (event) => {
306333
let taskElement;
307334
if ((taskElement = event.target.closest(".task"))) {
308335
showEditPopup(
309-
taskElement.textContent,
336+
taskElement.querySelector(".task-title").textContent,
310337
taskElement.dataset.description,
311338
taskElement.dataset.id,
312339
);
@@ -329,24 +356,37 @@ document.body.addEventListener("click", (event) => {
329356
const sharedConfig = {
330357
showCancelButton: true,
331358
inputValidator: (value) => {
332-
if (!value) return `The new ${role}'s login cannot be empty.`;
359+
if (!value)
360+
return t(
361+
"new-login-required",
362+
`The new ${role}'s login cannot be empty.`,
363+
{ role },
364+
);
333365
},
334366
};
335367

336368
const loginPromptResult = await swal.fire({
337-
titleText: `Enter the new ${role}'s login`,
369+
titleText: t("enter-new-login", `Enter the new ${role}'s login`, {
370+
role,
371+
}),
338372
input: "text",
339-
inputLabel: `Enter login:`,
373+
inputLabel: t("enter-login", "Enter login:"),
340374
...sharedConfig,
341375
});
342376
if (loginPromptResult.isDismissed) {
343377
throw new Error();
344378
}
345379

346380
const passwordPromptResult = await swal.fire({
347-
titleText: `Enter ${loginPromptResult.value}'s password`,
381+
titleText: t(
382+
"enter-user-password",
383+
`Enter ${loginPromptResult.value}'s password`,
384+
{
385+
login: loginPromptResult.value,
386+
},
387+
),
348388
input: "password",
349-
inputLabel: `Enter password:`,
389+
inputLabel: t("enter-password", "Enter password:"),
350390
...sharedConfig,
351391
});
352392
if (passwordPromptResult.isDismissed) {
@@ -370,22 +410,22 @@ document.body.addEventListener("click", (event) => {
370410
if (userId === appState.currentUser.id) userDeletingThemselves = true;
371411

372412
const listItem = event.target.closest(".user-list-item");
373-
const userRole = listItem.dataset.role;
374413
const userLogin =
375414
listItem.querySelector(".user-list-login").textContent;
415+
const userRole = listItem.dataset.role;
376416

377417
const userPresentation = userDeletingThemselves
378-
? "<b>yourself</b>"
379-
: `the ${userRole} '${userLogin}'`;
418+
? `<b>${t("yourself", "yourself")}</b>`
419+
: `${t("the", "the")} ${t(`${userRole}-ya`, userRole)} "${userLogin}"`;
380420
swal
381421
.fire({
382-
title: "Confirm deletion",
422+
title: t("confirm-deletion", "Confirm deletion"),
383423
[userDeletingThemselves ? "html" : "text"]:
384-
`Are you really sure you want to delete ${userPresentation}?`,
424+
`${t("sure-you-want-to-delete", "Are you really sure you want to delete")} ${userPresentation}?`,
385425
icon: "question",
386426
showDenyButton: true,
387-
confirmButtonText: "Delete",
388-
denyButtonText: "No, keep",
427+
confirmButtonText: t("delete-confirmation", "Delete"),
428+
denyButtonText: t("delete-cancel", "No, keep"),
389429
reverseButtons: true,
390430
})
391431
.then((result) => {
@@ -407,7 +447,9 @@ document.body.addEventListener("click", (event) => {
407447
break;
408448
}
409449
default: {
410-
alert(`Unknown action ${manageUsersAction}!`);
450+
alert(
451+
`${t("something-went-wrong", "Something went wrong")}: unknown action ${manageUsersAction}!`,
452+
);
411453
break;
412454
}
413455
}
@@ -416,12 +458,15 @@ document.body.addEventListener("click", (event) => {
416458

417459
document.body.addEventListener("change", (event) => {
418460
let select;
419-
if ((select = event.target.closest("select"))) {
461+
if ((select = event.target.closest(".card-select"))) {
420462
const taskId = select.value;
421463
if (!appState.currentUser.canReadTask(Task.get(taskId))) {
422464
swal.fire({
423-
title: "Error",
424-
titleText: "You can't move a task you can't see.",
465+
title: t("erorr", "Error"),
466+
titleText: t(
467+
"can't-see-can't-move",
468+
"You can't move a task you can't see.",
469+
),
425470
icon: "error",
426471
});
427472
return;
@@ -524,8 +569,8 @@ function handlePopupAction(popup, action) {
524569
case "save":
525570
if (!title) {
526571
swal.fire({
527-
title: "Error",
528-
text: "Task title cannot be empty!",
572+
title: t("error", "Error"),
573+
text: t("title-can't-be-empty", "Task title cannot be empty!"),
529574
icon: "error",
530575
});
531576
} else {
@@ -537,10 +582,10 @@ function handlePopupAction(popup, action) {
537582
case "delete":
538583
swal
539584
.fire({
540-
titleText: `Are you sure you want to delete "${title}"?`,
585+
titleText: `${t("sure-you-want-to-delete", "Are you sure you want to delete")} "${title}"?`,
541586
showDenyButton: true,
542-
confirmButtonText: "Delete",
543-
denyButtonText: "No, keep it",
587+
confirmButtonText: t("delete-confirmation", "Delete"),
588+
denyButtonText: t("delete-cancel", "No, keep it"),
544589
reverseButtons: true,
545590
customClass: {
546591
confirmButton: "btn btn-danger",
@@ -556,7 +601,9 @@ function handlePopupAction(popup, action) {
556601
});
557602
break;
558603
default:
559-
alert(`Unknown popup action ${action}!`);
604+
alert(
605+
`${t("something-went-wrong", "Something went wrong")}: unknown popup action ${action}!`,
606+
);
560607
}
561608
}
562609

@@ -602,7 +649,10 @@ export function renderTasks(tasks = appState.tasks) {
602649
for (const task of accessibleTasks) {
603650
const listItem = document.createElement("li");
604651
listItem.classList.add("task");
605-
listItem.textContent = task.title;
652+
const taskTitleSpan = document.createElement("span");
653+
taskTitleSpan.classList.add("task-title");
654+
taskTitleSpan.textContent = task.title;
655+
listItem.append(taskTitleSpan);
606656
listItem.dataset.description = task.description;
607657
listItem.dataset.id = task.id;
608658
listItem.setAttribute("draggable", "true");

src/index.html

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
<meta charset="UTF-8">
55
<meta http-equiv="X-UA-Compatible" content="IE=edge">
66
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7-
<title>Simple TODO App</title>
7+
<title data-i18n="app-title">Kanban Board</title>
88
</head>
99
<body class="body">
1010
<nav class="navbar navbar-expand-lg navbar-dark">
1111
<div class="container-fluid">
12-
<a class="navbar-brand" href="#">SimpleTODO</a>
12+
<a class="navbar-brand" href="#" data-i18n="app-title">Kanban Board</a>
1313
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
1414
data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false"
1515
aria-label="Toggle navigation">
@@ -26,20 +26,26 @@
2626
</nav>
2727
<main class="main">
2828
<div class="container">
29-
<div id="content">Please Sign In to see your tasks!</div>
29+
<div id="content">
30+
<p data-i18n="please-sign-in">Please Sign In to see your tasks!</p>
31+
</div>
3032
</div>
3133
</main>
3234
<footer class="footer">
3335
<div class="tasks-counter visibility-hidden">
3436
<div>
35-
Active tasks: <span id="active-tasks-count">0</span>
37+
<span data-i18n="active-tasks">Active tasks</span>: <span id="active-tasks-count">0</span>
3638
</div>
3739
<div>
38-
Finished tasks: <span id="finished-tasks-count">0</span>
40+
<span data-i18n="finished-tasks">Finished tasks</span>: <span id="finished-tasks-count">0</span>
3941
</div>
4042
</div>
41-
<div>
42-
Kanban board by <a href="https://github.com/ivan-developer-01" class="author-link">Ivan</a>, 2026
43+
<div class="footer-right-part">
44+
<select class="language-select">
45+
<option value="en">English</option>
46+
<option value="ru">Русский</option>
47+
</select>
48+
<span><span data-i18n="app-title-by">Kanban Board by</span> <a href="https://github.com/Peter-developer01" class="author-link">Peter</a>, 2026</span>
4349
</div>
4450
</footer>
4551
</body>

0 commit comments

Comments
 (0)