Skip to content
Open
Show file tree
Hide file tree
Changes from 75 commits
Commits
Show all changes
97 commits
Select commit Hold shift + click to select a range
5acae82
feat: add shared i18n infrastructure
NAO-200 Apr 27, 2026
070e599
refactor: adapt basic components for i18n
NAO-200 Apr 30, 2026
25a3edd
refactor: port safe frontend i18n changes
NAO-200 May 1, 2026
5490b20
refactor: adapt manual frontend i18n changes
NAO-200 May 1, 2026
d80eb20
feat: manually adapt i18n changes
NAO-200 May 4, 2026
498f697
feat: localize study-related components
NAO-200 May 6, 2026
e2b2ed8
mend
NAO-200 May 6, 2026
00535d7
Merge remote-tracking branch 'origin/dev' into feat-155-project-33-i18n
NAO-200 May 7, 2026
ab08e8b
feat: port additional Vue components to i18n
NAO-200 May 9, 2026
cbf0b41
mend
NAO-200 May 9, 2026
7484b62
feat: add i18n support for study and template components
NAO-200 May 10, 2026
d1370ce
feat: port i18n for auth, templates and basic components
NAO-200 May 10, 2026
f3f9812
feat: add i18n for workflow and template modals
NAO-200 May 11, 2026
a2f0204
chore: add missing settings keys
NAO-200 May 12, 2026
cd2e0fc
fix: correct basic component i18n regressions
NAO-200 May 12, 2026
0cbb7c4
fix: correct dashboard and annotator i18n regressions
NAO-200 May 14, 2026
dbc6e8c
fix: correct final i18n regressions
NAO-200 May 16, 2026
14a9ced
mend
NAO-200 May 17, 2026
524e095
Merge branch 'dev' into feat-155-project-33-i18n
NAO-200 May 17, 2026
29a6dbf
feat: translate remaining hardcoded UI strings
NAO-200 May 17, 2026
395defa
feat: add i18n for model fields and dashboard UI
NAO-200 May 18, 2026
75598f9
feat: add backend i18n error handling
NAO-200 May 18, 2026
c3e05bd
feat: localize auth route errors
NAO-200 May 18, 2026
efa83e6
feat: localize two-factor auth errors
NAO-200 May 18, 2026
f61ee4a
fix: preserve exact auth API messages
NAO-200 May 18, 2026
f8d751f
feat: localize template errors
NAO-200 May 18, 2026
7425aa7
feat: localize study session errors
NAO-200 May 18, 2026
12a6091
feat: localize assignment errors
NAO-200 May 18, 2026
69d9ba1
feat: localize document user submission errors
NAO-200 May 18, 2026
e59c929
feat: localize background task errors
NAO-200 May 18, 2026
1ff58f7
feat: localize import validation errors
NAO-200 May 18, 2026
452fc8a
feat: localize socket tail errors
NAO-200 May 18, 2026
27379c5
feat: localize user password validation errors
NAO-200 May 19, 2026
556f4da
feat: localize dynamic model helper errors
NAO-200 May 19, 2026
435804f
feat: localize rpc background task errors
NAO-200 May 19, 2026
6e77fb9
feat: localize auto table metadata labels
NAO-200 May 19, 2026
57f5475
fix: localize annotation deletion warning
NAO-200 May 19, 2026
0b97239
fix: preserve Vue context in i18n callbacks and common online keys
NAO-200 May 25, 2026
26d83a9
feat: localize template placeholder metadata via i18n keys in DB
NAO-200 May 25, 2026
61f0172
fix: use i18n keys for tag color select labels
NAO-200 May 25, 2026
2465863
fix: localize seed default project in UI without DB keys
NAO-200 May 25, 2026
04c6050
fix: translate role names in rights management modal
NAO-200 May 26, 2026
b1c9ca8
fix: translate user right descriptions in rights modal
NAO-200 May 26, 2026
d1855b5
feat: localize seed workflows via i18n keys in DB
NAO-200 May 26, 2026
3f6d319
fix: use description i18n keys for workflow form description field
NAO-200 May 26, 2026
3ef9ee5
feat: i18n for settings descriptions
NAO-200 May 26, 2026
f600652
fix: translate i18n keys in dashboard logs
NAO-200 May 26, 2026
e20ee4a
fix: translate current workflow name in rename modal
NAO-200 May 26, 2026
4390369
feat: localize NLP skill names and descriptions
NAO-200 May 26, 2026
2b879a9
fix: use natural German word order in coordinator modal titles
NAO-200 May 26, 2026
6468534
fix: restore English from en locale in i18n migration downs
NAO-200 May 26, 2026
f7f63a4
fix: seed default project i18n
NAO-200 May 28, 2026
7156829
fix: localize seed configuration names
NAO-200 May 28, 2026
41688cf
fix: translate workflow names in session overview filter
NAO-200 May 29, 2026
4bc387a
refactor: centralize translateMaybeKey in form components
NAO-200 May 29, 2026
5f81e36
docs: add JSDoc to i18n and date helpers in utils
NAO-200 May 29, 2026
4f7e4cb
fix: refresh dashboard table columns on locale change
NAO-200 May 30, 2026
59fd28b
fix: translate seed workflows only and localize copy prefill
NAO-200 May 30, 2026
5ae4ee1
feat: add preferences modal and centralize browser locale handling
NAO-200 Jun 6, 2026
179aac3
merge: integrate dev into feat-155-project-33-i18n
NAO-200 Jun 7, 2026
743ba23
fix: polish settings import label and type 7 template placeholders
NAO-200 Jun 7, 2026
d8a9fdb
fix: localize assignment form fields and submission socket errors
NAO-200 Jun 7, 2026
d3cb39e
fix: localize assignment dashboard UI and navigation labels
NAO-200 Jun 8, 2026
a472d9d
fix: add transform migrations for dev seed i18n data
NAO-200 Jun 8, 2026
52fc751
fix: localize role management modal
NAO-200 Jun 8, 2026
1468e72
fix: defer settings and wizard i18n to tooltips only
NAO-200 Jun 11, 2026
4d36698
fix: keep dashboard logs in English and stop seed project/workflow i18n
NAO-200 Jun 11, 2026
0a188a9
fix: scope i18n transforms to template placeholders only
NAO-200 Jun 12, 2026
cdd9633
fix: keep NLP skill names untranslated in lists and selectors
NAO-200 Jun 12, 2026
ba6f1c8
fix: resolve socket errors to keys and English in catch hub
NAO-200 Jun 12, 2026
d718098
fix: replace plain English socket throws with i18n keys
NAO-200 Jun 12, 2026
7bf831a
fix: adapt generateError and TranslatableError for i18n socket errors
NAO-200 Jun 12, 2026
18be2ae
fix: use TranslatableError for schemaNotFound validation params
NAO-200 Jun 12, 2026
cfd68a5
fix: align auth check and document import warnings with i18n keys
NAO-200 Jun 12, 2026
af26f87
fix: localize Moodle submission ImportModal
NAO-200 Jun 12, 2026
bd22b31
fix: block user_setting overrides of protected system settings
NAO-200 Jun 17, 2026
dbb322b
feat: persist UI locale in app.locale and apply it from appSettings
NAO-200 Jun 22, 2026
26707b9
feat: add disableLanguageSelection right and block locale changes for…
NAO-200 Jun 22, 2026
85e8fd6
feat: add app.locale dropdown in admin settings and display metadata
NAO-200 Jun 22, 2026
3a61eda
Merge branch 'dev' into feat-155-project-33-i18n
NAO-200 Jun 24, 2026
b00fbe3
docs: add i18n guides for developers and researchers
NAO-200 Jul 10, 2026
23c0077
docs: expand i18n developer guide
NAO-200 Jul 10, 2026
dab9ee6
docs: explain language switcher restriction for researchers
NAO-200 Jul 10, 2026
e4ca3ea
feat: extend resolveLogText with params and use it in Socket error hub
NAO-200 Jul 10, 2026
8dba613
refactor: slim TranslatableError and harden resolveEnText fallback
NAO-200 Jul 10, 2026
0fa8a48
Merge branch 'dev' into feat-155-project-33-i18n
NAO-200 Jul 11, 2026
06234d6
feat: localize import/export format modals
NAO-200 Jul 11, 2026
3ecf11a
feat: wire dashboard import/export modal titles
NAO-200 Jul 11, 2026
152a20c
feat: localize export grades step options
NAO-200 Jul 11, 2026
862c204
feat: localize settings row and import leftovers
NAO-200 Jul 11, 2026
192a6e1
docs: add i18n-t guide and tighten v-html rules
NAO-200 Jul 11, 2026
a6d8f0e
feat: replace v-html with i18n-t in dashboard modals
NAO-200 Jul 11, 2026
57c1eb2
docs: document formatLocalized date/time helpers
NAO-200 Jul 11, 2026
d91e695
refactor: unify backend i18n on translateMaybeKey and fix log duplica…
NAO-200 Jul 13, 2026
f4aa58f
docs: improve i18n guide flowchart and backend migration docs
NAO-200 Jul 13, 2026
91b419b
refactor: use translateMaybeKey in migrations, remove resolveEnText
NAO-200 Jul 13, 2026
f935b60
refactor: rename messages.js to i18n-bundles.js
NAO-200 Jul 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions backend/db/migration-i18n-utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
'use strict';

/**
* Helpers for Sequelize migrations that store i18n keys in the database.
* Down migrations resolve English source strings from en locale JSON.
*
* @author Andrii Nikitin
*/
const { t, hasKey } = require('../utils/i18n');

/**
* @param {string} i18nKey
* @returns {string|null} English text, or `null` if the key is missing from `en` JSON.
* Callers that skip updates on `null` leave i18n keys in the DB; the frontend then
* typically shows that raw key string (e.g. `translateMaybeKey` passes it through when
* there is no translation).
*/
function resolveEnText(i18nKey) {
if (typeof i18nKey !== 'string' || !hasKey(i18nKey, 'en')) {
return null;
}
return t(i18nKey, {}, 'en');
}

module.exports = { resolveEnText, t, hasKey };
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"use strict";

const { resolveEnText } = require("../migration-i18n-utils");

/**
* Replace English placeholderLabel / placeholderDescription with i18n keys.
* Help tooltips stay in templates.json (frontend derives help key from description key).
*
* @type {import('sequelize-cli').Migration}
*/

const TYPE_SLUG = {
1: "emailGeneral",
2: "emailStudySession",
3: "emailAssignment",
6: "emailStudyClose",
};

const PLACEHOLDER_ROWS = [
{ type: 1, placeholderKey: "username" },
{ type: 1, placeholderKey: "firstName" },
{ type: 1, placeholderKey: "lastName" },
{ type: 1, placeholderKey: "link" },
{ type: 1, placeholderKey: "otp" },
{ type: 1, placeholderKey: "tokenExpiry" },
{ type: 2, placeholderKey: "username" },
{ type: 2, placeholderKey: "link" },
{ type: 3, placeholderKey: "username" },
{ type: 3, placeholderKey: "assignmentType" },
{ type: 3, placeholderKey: "assignmentName" },
{ type: 3, placeholderKey: "link" },
{ type: 6, placeholderKey: "username" },
{ type: 6, placeholderKey: "studyName" },
];

function keysFor(type, placeholderKey) {
const slug = TYPE_SLUG[type];
const base = "templates.placeholders";
return {
placeholderLabel: `${base}.labels.${slug}.${placeholderKey}`,
placeholderDescription: `${base}.descriptions.${slug}.${placeholderKey}`,
};
}

/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
for (const row of PLACEHOLDER_ROWS) {
await queryInterface.bulkUpdate(
"placeholder",
{
...keysFor(row.type, row.placeholderKey),
updatedAt: new Date(),
},
{
type: row.type,
placeholderKey: row.placeholderKey,
},
{},
);
}
},

async down(queryInterface) {
for (const row of PLACEHOLDER_ROWS) {
const keys = keysFor(row.type, row.placeholderKey);
const placeholderLabel = resolveEnText(keys.placeholderLabel);
const placeholderDescription = resolveEnText(keys.placeholderDescription);
if (!placeholderLabel || !placeholderDescription) {
continue;
}
await queryInterface.bulkUpdate(
"placeholder",
{
placeholderLabel,
placeholderDescription,
updatedAt: new Date(),
},
{
type: row.type,
placeholderKey: row.placeholderKey,
},
{}
);
}
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"use strict";

const { resolveEnText } = require("../migration-i18n-utils");

/**
* Replace English placeholderLabel / placeholderDescription for email template type 7.
*
* @type {import('sequelize-cli').Migration}
*/

const TYPE_SLUG = "emailSubmissionUpload";

const PLACEHOLDER_ROWS = [
{ type: 7, placeholderKey: "username" },
{ type: 7, placeholderKey: "assignmentName" },
{ type: 7, placeholderKey: "eventType" },
{ type: 7, placeholderKey: "assignmentId" },
{ type: 7, placeholderKey: "submissionId" },
{ type: 7, placeholderKey: "timestamp" },
];

function keysFor(placeholderKey) {
const base = "templates.placeholders";
return {
placeholderLabel: `${base}.labels.${TYPE_SLUG}.${placeholderKey}`,
placeholderDescription: `${base}.descriptions.${TYPE_SLUG}.${placeholderKey}`,
};
}

/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface) {
for (const row of PLACEHOLDER_ROWS) {
await queryInterface.bulkUpdate(
"placeholder",
{
...keysFor(row.placeholderKey),
updatedAt: new Date(),
},
{
type: row.type,
placeholderKey: row.placeholderKey,
},
{}
);
}
},

async down(queryInterface) {
for (const row of PLACEHOLDER_ROWS) {
const keys = keysFor(row.placeholderKey);
const placeholderLabel = resolveEnText(keys.placeholderLabel);
const placeholderDescription = resolveEnText(keys.placeholderDescription);
if (!placeholderLabel || !placeholderDescription) {
continue;
}
await queryInterface.bulkUpdate(
"placeholder",
{
placeholderLabel,
placeholderDescription,
updatedAt: new Date(),
},
{
type: row.type,
placeholderKey: row.placeholderKey,
},
{}
);
}
},
};
32 changes: 16 additions & 16 deletions backend/db/models/assignment.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,54 +14,54 @@ module.exports = (sequelize, DataTypes) => {
static fields = [
{
key: "name",
label: "Assignment Name:",
placeholder: "Assignment 1",
label: "assignments.fields.name.label",
placeholder: "assignments.fields.name.placeholder",
type: "text",
required: true,
default: "",
},
{
key: "description",
label: "Description:",
help: "Optional description shown for this assignment.",
label: "assignments.fields.description.label",
help: "assignments.fields.description.help",
type: "textarea",
required: false,
},
{
key: "maxRevisions",
label: "Maximum Revisions:",
label: "assignments.fields.maxRevisions.label",
type: "slider",
class: "custom-slider-class",
min: 1,
max: 20,
step: 1,
unit: "revision(s)",
unit: "assignments.fields.maxRevisions.unit",
unlimitedAtMax: true,
unlimitedLabel: "unlimited",
unlimitedLabel: "assignments.fields.maxRevisions.unlimitedLabel",
unlimitedStoredValue: 0,
required: true,
default: 1,
help: "Maximum number of allowed revision copies for this assignment. Move to the end for unlimited.",
help: "assignments.fields.maxRevisions.help",
},
{
key: "start",
label: "Start Time:",
label: "assignments.fields.start.label",
type: "datetime",
size: 6,
default: null,
required: false,
},
{
key: "end",
label: "End Time:",
label: "assignments.fields.end.label",
type: "datetime",
size: 6,
default: null,
required: false,
},
{
key: "validationConfigurationId",
label: "Validation Configuration:",
label: "assignments.fields.validationConfigurationId.label",
type: "select",
options: {
table: "configuration",
Expand All @@ -72,23 +72,23 @@ module.exports = (sequelize, DataTypes) => {
],
},
required: true,
help: "Validation is applied before submission upload.",
help: "assignments.fields.validationConfigurationId.help",
},
{
key: "allowReUpload",
label: "Allow Re-Upload:",
label: "assignments.fields.allowReUpload.label",
type: "switch",
default: false,
required: false,
help: "If enabled, users can replace or delete uploaded submissions.",
help: "assignments.fields.allowReUpload.help",
},
{
key: "notifyOnSubmissionUpload",
label: "Notify on Submission Upload:",
label: "assignments.fields.notifyOnSubmissionUpload.label",
type: "switch",
default: false,
required: false,
help: "If enabled, sends an email when a student uploads or re-uploads a submission.",
help: "assignments.fields.notifyOnSubmissionUpload.help",
},
];

Expand Down
20 changes: 10 additions & 10 deletions backend/db/models/configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,49 +13,49 @@ module.exports = (sequelize, DataTypes) => {
static fields = [
{
key: "name",
label: "Name",
placeholder: "Configuration name",
label: "basic.configuration.fields.name.label",
placeholder: "basic.configuration.fields.name.placeholder",
type: "text",
required: true,
default: "",
},
{
key: "description",
label: "Description",
placeholder: "Optional description",
label: "basic.configuration.fields.description.label",
placeholder: "basic.configuration.fields.description.placeholder",
type: "text",
required: false,
default: "",
},
{
key: "userId",
label: "User ID",
label: "basic.configuration.fields.userId.label",
placeholder: "#",
type: "text",
required: true,

},
{
key: "hideInFrontend",
label: "Hide in Frontend",
label: "basic.configuration.fields.hideInFrontend.label",
type: "switch",
required: false,
default: false,
},
{
key: "type",
label: "Type",
label: "basic.configuration.fields.type.label",
placeholder: "0",
type: "select",
options: [
{ name: "Assessment", value: 0 },
{ name: "Validation", value: 1 },
{ name: "basic.configuration.types.assessment", value: 0 },
{ name: "basic.configuration.types.validation", value: 1 },
],
required: true,
},
{
key: "content",
label: "Config (JSON)",
label: "basic.configuration.fields.content.label",
placeholder: "{ }",
type: "json",
required: true,
Expand Down
Loading
Loading