diff --git a/.firebaserc b/.firebaserc index aac27d606..798bcd131 100644 --- a/.firebaserc +++ b/.firebaserc @@ -17,4 +17,4 @@ }, "etags": {}, "dataconnectEmulatorConfig": {} -} \ No newline at end of file +} diff --git a/.github/workflows/i18n-diff-guard.yml b/.github/workflows/i18n-diff-guard.yml index f07e7e68c..4e099bc28 100644 --- a/.github/workflows/i18n-diff-guard.yml +++ b/.github/workflows/i18n-diff-guard.yml @@ -27,7 +27,7 @@ jobs: - name: Run i18n diff guard id: i18n_check continue-on-error: true - run: node i18n-diff-guard.js + run: node i18n-diff-guard.mjs - name: Post PR comment on failure if: steps.i18n_check.outcome == 'failure' diff --git a/.husky/pre-commit b/.husky/pre-commit index 30804a592..72d78fc77 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,5 +1,5 @@ #!/usr/bin/env sh echo "๐Ÿ” running pre-commit hook" -node i18n-diff-guard.js +node i18n-diff-guard.mjs npm run test npx lint-staged \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 504f56f7a..d54119811 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,11 +11,15 @@ Thank you for your interest in contributing to RUXAILAB! This document provides - [Code Standards](#code-standards) - [Testing](#testing) - [Reporting Issues](#reporting-issues) +- [Issue Labeling Guide](#issue-labeling-guide) +- [Pre-commit Hooks](#pre-commit-hooks) ## Code of Conduct We are committed to providing a welcoming and inclusive environment for all contributors. Please be respectful and constructive in all interactions. +For Code of Conduct discussions and support, please use our Discord channel: https://discord.com/channels/1209902463239593984/1209902463713288293 + ## Getting Started RUXAILAB is a Vue.js-based platform for usability testing and heuristic evaluation. Before contributing, familiarize yourself with: @@ -171,22 +175,22 @@ Then create a Pull Request on GitHub: - Target the `develop` branch - Provide a clear description of your changes - Reference any related issues +- Keep at most **2 open Pull Requests** per contributor at any time (additional PRs are automatically closed by workflow) - Wait for review and address any feedback ## Code Standards ### Formatting -We use **Prettier** for code formatting. Configuration is in `.prettierrc`. +We use **Prettier** for code formatting. Prettier is automatically applied through pre-commit hooks, so you don't need to manually format most of the time. ```bash -# Format all files -npm run format - -# Check formatting -npm run format:check +# If needed, manually format files +npx prettier --write src/your-file.vue ``` +See [Code Formatting Rules](#code-formatting-rules) section below for detailed information about formatting standards. + ### Linting We use **ESLint** for code quality. Configuration is in `eslint.config.mjs`. @@ -199,6 +203,8 @@ npm run lint npm run lint:fix ``` +See [Code Formatting Rules](#code-formatting-rules) section below for detailed ESLint configuration information. + ### Vue.js Best Practices - Use Composition API for new components @@ -255,6 +261,8 @@ npm run test:e2e:headed ## Reporting Issues +Each contributor may have a maximum of **5 open issues** at a time. + ### Bug Reports When reporting bugs, please include: @@ -282,7 +290,270 @@ Use the [Feature Request template](https://github.com/uramakilab/remote-usabilit For questions or general discussion, use [GitHub Discussions](https://github.com/uramakilab/remote-usability-lab/discussions). -## Additional Resources +## Issue Labeling Guide + +Issues in RUXAILAB are automatically labeled based on the issue title format and content. Understanding how labels work helps with issue organization and discoverability. + +### Auto-Applied Labels + +Issues are automatically labeled when opened or edited. Here's how to use labels effectively: + +#### 1. **Issue Type Labels** (Auto-applied based on title format) + +Use conventional commit prefixes in your issue title to get proper type labels: + +| Prefix | Label | Usage | +| ----------- | --------------- | ------------------------------ | +| `feat:` | `feature` | New features or enhancements | +| `fix:` | `bug` | Bug reports and fixes | +| `docs:` | `documentation` | Documentation improvements | +| `refactor:` | `refactor` | Code refactoring | +| `test:` | `tests` | Test additions or improvements | +| `perf:` | `performance` | Performance improvements | +| `ci:` | `ci` | CI/CD workflow changes | +| `chore:` | `chore` | Maintenance tasks | +| `build:` | `build` | Build system changes | +| `style:` | `style` | Formatting and style changes | + +**Examples:** + +- โœ… `feat: add dark mode support` โ†’ gets `feature` label +- โœ… `fix: correct navbar alignment` โ†’ gets `bug` label +- โœ… `docs: update installation guide` โ†’ gets `documentation` label +- โŒ `Add dark mode support` โ†’ no type label (ambiguous) + +#### 2. **User Type Labels** (Auto-applied based on user role) + +- **`maintainer-issue`** - Issues opened by maintainers and team members +- **`community-issue`** - Issues opened by community contributors + +These labels help maintainers identify which issues come from the core team vs. the community. + +#### 3. **Other Auto-Applied Labels** + +- **`bug`** - Auto-applied for bug reports (if title contains `[bug]`, `๐Ÿž`, or matches bug template) +- **`help`** - Auto-applied if issue title or body mentions "help wanted" or "need help" + +#### 4. **Manual Labels** (Applied by maintainers) + +Some labels are applied manually by maintainers: + +- **`good first issue`** - Good starting points for new contributors +- **`blocked`** - Issues blocked by other work +- **`wontfix`** - Issues that won't be worked on +- **`duplicate`** - Duplicate of another issue +- **`priority-high`** / **`priority-medium`** / **`priority-low`** - Issue priority levels +- **`security`** - Security-related issues +- **`dependencies`** - Dependency update issues + +### Writing Issues with Proper Labels + +To ensure your issue gets the correct labels: + +1. **Use conventional commit format** for the title: + + ``` + (): + ``` + + - `type`: One of feat, fix, docs, refactor, test, perf, ci, chore, build, style + - `scope` (optional): Component or area affected, e.g., `auth`, `dashboard` + - `description`: Brief description + + Examples: + - `feat(auth): add social login providers` + - `fix(ux-creation): resolve template duplication bug` + - `docs(api): update Firebase setup instructions` + +2. **Use issue templates**: GitHub provides templates for bug reports and feature requests that auto-apply some labels + +3. **Be clear and descriptive**: This helps maintainers understand the issue scope and apply additional labels correctly + +## Pre-commit Hooks + +This project uses **Husky** and **lint-staged** to automatically format and lint code before each commit. This ensures code quality standards are maintained. + +### How It Works + +When you commit changes, the following automatically runs: + +1. **Husky** intercepts the commit +2. **lint-staged** runs linting and formatting on staged files: + - **ESLint** checks for code quality issues + - **Prettier** formats code for consistency + +### Pre-commit Hook Rules + +```bash +# Files matching src/**/*.{js,vue} will have: +- eslint --fix # Auto-fix ESLint issues +- prettier --write # Auto-format with Prettier +``` + +### Setup + +Pre-commit hooks are set up automatically when you run: + +```bash +npm install +``` + +This installs Husky and sets up the `.husky/pre-commit` hook. + +### If Hooks Don't Run + +If pre-commit hooks aren't running, initialize Husky: + +```bash +npx husky install +``` + +### Skipping Pre-commit Hooks (Not Recommended) + +If you absolutely need to skip the pre-commit hook, use: + +```bash +git commit --no-verify +``` + +โš ๏ธ **Warning**: This bypasses code quality checks. Avoid this in production code. + +### Manual Code Quality Checks + +You can also manually run these commands: + +```bash +# Check for ESLint issues +npm run lint + +# Auto-fix ESLint issues +npm run lint:fix + +# Format code with Prettier (if needed manually) +npm run format + +# Or use Prettier directly to format specific files +npx prettier --write src/your-file.vue +``` + +### Code Formatting Rules + +#### Prettier Configuration + +RUXAILAB uses Prettier for consistent code formatting. While there's no `.prettierrc` file, Prettier uses sensible defaults: + +- **Print Width**: 80 characters +- **Tabs**: 2 spaces (no tabs) +- **Quotes**: Double quotes for strings +- **Semicolons**: Enabled +- **Trailing Commas**: ES5 style (where valid in older JS) +- **Arrow Functions**: Always add parentheses around parameters + +#### ESLint Configuration + +ESLint configuration is in `eslint.config.mjs`: + +- **Parser**: Uses Vue 3 template parser with Babel support +- **Plugins**: Vue, Vuetify, Vue I18n +- **Extends**: Vue recommended rules, Vuetify recommendations, Prettier integration +- **Browsers Globals**: Browser APIs are available + +Key rules enforced: + +- Vue 3-specific syntax requirements +- Proper component structure +- No unused variables +- Proper i18n usage + +### Vue.js Code Style Examples + +#### โœ… Good Component Structure + +```vue + + + + + +``` + +#### โœ… Good JavaScript Style + +```javascript +// Use const by default, let if you need to reassign +const API_ENDPOINT = 'https://api.example.com' +let selectedUser = null + +// Arrow functions for callbacks +const items = data.map((item) => ({ + ...item, + formatted: item.value.toUpperCase(), +})) + +// Destructuring in function parameters +function processData({ name, age, email }) { + return { + name: name.trim(), + age: parseInt(age), + email: email.toLowerCase(), + } +} + +// Use template literals +const message = `Hello, ${userName}! You have ${messageCount} new messages.` + +// Async/await for promises +async function fetchUserData(userId) { + try { + const response = await fetch(`/api/users/${userId}`) + if (!response.ok) { + throw new Error(`API error: ${response.status}`) + } + return await response.json() + } catch (error) { + console.error('Failed to fetch user:', error) + throw error + } +} +``` - [README.md](README.md) - Project overview and setup - [Vue.js Documentation](https://vuejs.org/) diff --git a/README.md b/README.md index 1459b9c33..c04712401 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,11 @@ UX Remote LAB provides a collaborative environment for creators to share their p - [Request a Feature ๐Ÿš€](https://github.com/uramakilab/remote-usability-lab/issues/new) - [Ask a Question ๐Ÿค—](https://github.com/uramakilab/remote-usability-lab/discussions) -For commercial support, academic collaborations, and answers to common questions, please use [Get Support]() to contact us. +For commercial support, academic collaborations, and answers to common questions, please contact us by one of our communications channels: + +- [Discord Server](https://discord.gg/YnkDk9BNYK) +- [Discussions](https://github.com/ruxailab/RUXAILAB/discussions) +- Email: `ruxailab@gmail.com` ### Development Environment @@ -205,4 +209,4 @@ Visit `http://localhost:5000` in your browser to access the UX Remote LAB platfo ## License -MIT ยฉ [RUXAILAB](https://github.com/uramakilab/remote-usability-lab) +MIT ยฉ [RUXAILAB](https://github.com/ruxailab/RUXAILAB) diff --git a/functions/.env.example b/functions/.env.example index 87ffd0bf1..a74d839ea 100644 --- a/functions/.env.example +++ b/functions/.env.example @@ -4,4 +4,5 @@ SMTP_USER= SMTP_PASS= SMTP_SECURE= SITE_URL= -RUXAILAB_FUNCTIONS_REGION= \ No newline at end of file +RUXAILAB_FUNCTIONS_REGION= +EYE_LAB_CORS_ORIGINS= \ No newline at end of file diff --git a/functions/.gitignore b/functions/.gitignore index c2658d7d1..4210c525c 100644 --- a/functions/.gitignore +++ b/functions/.gitignore @@ -1 +1,6 @@ node_modules/ + +# Functions env files +.env +.env.* +!.env.example diff --git a/functions/src/f.firebase.js b/functions/src/f.firebase.js index a374f2c26..9b3920c05 100644 --- a/functions/src/f.firebase.js +++ b/functions/src/f.firebase.js @@ -1,4 +1,5 @@ import admin from 'firebase-admin' +import 'dotenv/config' import { onObjectDeleted, onObjectFinalized, @@ -6,18 +7,26 @@ import { } from 'firebase-functions/storage' import firebaseFunctions from 'firebase-functions/v2' -const REGION = process.env.RUXAILAB_FUNCTIONS_REGION || 'europe-west6' +function getRegion() { + return process.env.RUXAILAB_FUNCTIONS_REGION || 'europe-west6' +} function onRequest({ handler, opts = {} }) { - return firebaseFunctions.https.onRequest({ region: REGION, ...opts }, handler) + return firebaseFunctions.https.onRequest( + { region: getRegion(), ...opts }, + handler, + ) } function onCall({ handler, options = {} }) { - return firebaseFunctions.https.onCall({ region: REGION, ...options }, handler) + return firebaseFunctions.https.onCall( + { region: getRegion(), ...options }, + handler, + ) } function onTrigger({ path, event, handler }) { - const baseOptions = { region: REGION } + const baseOptions = { region: getRegion() } const firestoreEvents = { created: (p, h) => @@ -49,7 +58,7 @@ function onTrigger({ path, event, handler }) { } function onStorageTrigger({ event, handler }) { - const baseOptions = { region: REGION } + const baseOptions = { region: getRegion() } const storageEvents = { finalized: (h) => onObjectFinalized(baseOptions, h), deleted: (h) => onObjectDeleted(baseOptions, h), diff --git a/functions/src/helpers/addSubTypeInUser.js b/functions/src/helpers/addSubTypeInUser.js index e066fd06f..ea32c54c1 100644 --- a/functions/src/helpers/addSubTypeInUser.js +++ b/functions/src/helpers/addSubTypeInUser.js @@ -1,4 +1,5 @@ -import { admin, functions } from "../f.firebase.js"; +import { admin, functions } from '../f.firebase.js' +import logger from '../utils/logger.js' export const addSubTypeInUser = functions.onRequest({ handler: async (req, res) => { @@ -6,35 +7,42 @@ export const addSubTypeInUser = functions.onRequest({ const snap = await db.collection('users').get() const docs = snap.docs - console.log(`Users Encontrados: ${snap.size}`) + logger.info('addSubTypeInUser: users found', { count: snap.size }) for (let i = 0; i < docs.length; i += 500) { const slice = docs.slice(i, i + 500) - const results = await Promise.all(slice.map(async (doc) => { - const data = doc.data() - if (!data.myTests) return null + const results = await Promise.all( + slice.map(async (doc) => { + const data = doc.data() + if (!data.myTests) return null - const updates = await Promise.all( - Object.entries(data.myTests).map(([key, entry]) => - handleEntry(key, entry, db) + const updates = await Promise.all( + Object.entries(data.myTests).map(([key, entry]) => + handleEntry(key, entry, db), + ), ) - ) - const updatePayload = Object.assign({}, ...updates.filter(Boolean)) - if (Object.keys(updatePayload).length === 0) return null + const updatePayload = Object.assign({}, ...updates.filter(Boolean)) + if (Object.keys(updatePayload).length === 0) return null - return { ref: doc.ref, updatePayload } - })) + return { ref: doc.ref, updatePayload } + }), + ) - const batch = db.batch(); - results.filter(Boolean).forEach(r => batch.update(r.ref, r.updatePayload)); - await batch.commit(); + const batch = db.batch() + results + .filter(Boolean) + .forEach((r) => batch.update(r.ref, r.updatePayload)) + await batch.commit() - console.log(`Batch commit: ${Math.min(i + 500, docs.length)} / ${docs.length}`) + logger.info('addSubTypeInUser: batch committed', { + processed: Math.min(i + 500, docs.length), + total: docs.length, + }) } return res.status(200).send() - } + }, }) async function handleEntry(key, entry, db) { @@ -43,7 +51,8 @@ async function handleEntry(key, entry, db) { const payload = {} if (entry.testType === 'User') payload[`${base}.testType`] = 'USER' - if (entry.testType === 'CardSorting') payload[`${base}.testType`] = 'CARD_SORTING' + if (entry.testType === 'CardSorting') + payload[`${base}.testType`] = 'CARD_SORTING' if (entry.testType === 'HEURISTICS') payload[`${base}.testType`] = 'HEURISTIC' if (entry.testType == 'User' || entry.testType == 'USER') { @@ -52,8 +61,16 @@ async function handleEntry(key, entry, db) { const testdDoc = await db.collection('tests').doc(entry.testDocId).get() const testData = testdDoc.data() - if (testData.subType == 'USER_MODERATED' || testData.userTestType == 'moderated') payload[`${base}.subType`] = 'USER_MODERATED' - if (testData.subType == 'USER_UNMODERATED' || testData.userTestType == 'unmoderated') payload[`${base}.subType`] = 'USER_UNMODERATED' + if ( + testData.subType == 'USER_MODERATED' || + testData.userTestType == 'moderated' + ) + payload[`${base}.subType`] = 'USER_MODERATED' + if ( + testData.subType == 'USER_UNMODERATED' || + testData.userTestType == 'unmoderated' + ) + payload[`${base}.subType`] = 'USER_UNMODERATED' } return payload diff --git a/functions/src/https/email.js b/functions/src/https/email.js index 5cbb7fd23..b377be348 100644 --- a/functions/src/https/email.js +++ b/functions/src/https/email.js @@ -1,12 +1,13 @@ -import { admin, functions } from "../f.firebase.js"; -import nodemailer from "nodemailer"; -import * as fs from "fs"; -import * as path from "path"; -import logger from "../utils/logger.js"; +import { admin, functions } from '../f.firebase.js' +import nodemailer from 'nodemailer' +import * as fs from 'fs' +import * as path from 'path' +import { logger } from 'firebase-functions' export const sendEmail = functions.onCall({ handler: async (data) => { - const content = data.data; + // Firebase callable passes the argument directly + const content = data.data || data const transporter = nodemailer.createTransport({ host: process.env.SMTP_HOST, @@ -16,48 +17,71 @@ export const sendEmail = functions.onCall({ user: process.env.SMTP_USER, pass: process.env.SMTP_PASS, }, - }); + }) - let htmlTemplate = ""; + let htmlTemplate = '' if (content.template === 'invite') { - const templatePath = path.join(process.cwd(), "src/templates/mails/invitations.html"); - htmlTemplate = fs.readFileSync(templatePath, "utf-8"); + const templatePath = path.join( + process.cwd(), + 'src/templates/mails/invitations.html', + ) + htmlTemplate = fs.readFileSync(templatePath, 'utf-8') htmlTemplate = htmlTemplate - .replace("{{site}}", process.env.SITE_URL) - .replace("{{message}}", content.data.message) + .replace('{{site}}', process.env.SITE_URL) + .replace('{{message}}', content.data.message) .replace(/{{testTitle}}/g, content.data.testTitle) .replace(/{{testDescription}}/g, content.data.testDescription) .replace(/{{adminEmail}}/g, content.data.adminEmail) - .replace(/{{adminName}}/g, content.data.adminName); - } - else if (content.template === 'passwordReset') { + .replace(/{{adminName}}/g, content.data.adminName) + } else if (content.template === 'passwordReset') { const actionCodeSettings = { url: `${process.env.SITE_URL}/signin`, handleCodeInApp: false, } - const link = await admin.auth().generatePasswordResetLink(content.to, actionCodeSettings); - const templatePath = path.join(process.cwd(), "src/templates/mails/passwordReset.html"); - htmlTemplate = fs.readFileSync(templatePath, "utf-8"); + const link = await admin + .auth() + .generatePasswordResetLink(content.to, actionCodeSettings) + const templatePath = path.join( + process.cwd(), + 'src/templates/mails/passwordReset.html', + ) + htmlTemplate = fs.readFileSync(templatePath, 'utf-8') + htmlTemplate = htmlTemplate.replace('{{resetLink}}', link) + } else if (content.template === 'emailVerification') { + const actionCodeSettings = { + url: `${process.env.SITE_URL}/verify-email`, + handleCodeInApp: false, + } + + const link = await admin + .auth() + .generateEmailVerificationLink(content.to, actionCodeSettings) + const templatePath = path.join( + process.cwd(), + 'src/templates/mails/emailVerification.html', + ) + htmlTemplate = fs.readFileSync(templatePath, 'utf-8') htmlTemplate = htmlTemplate - .replace("{{resetLink}}", link); + .replace('{{verificationLink}}', link) + .replace('{{userName}}', content.data.userName || 'User') } const mail = { - from: 'no-reply@ruxailab.com', + from: process.env.SMTP_USER || 'no-reply@ruxailab.com', to: content.to, subject: content.subject, html: htmlTemplate, attachments: content.attachments ?? [], - }; + } try { - await transporter.sendMail(mail); - logger.info('Email sent successfully to', { to: content.to }); - return 'Email sent successfully.'; + await transporter.sendMail(mail) + logger.info('Email sent successfully to', { to: content.to }) + return 'Email sent successfully.' } catch (err) { - logger.error('Error sending email:', { error: err }); - return err; + logger.error('Error sending email:', { error: err }) + return err } - } + }, }) diff --git a/functions/src/https/eyeTracking.js b/functions/src/https/eyeTracking.js index e1c8500ab..926f426bc 100644 --- a/functions/src/https/eyeTracking.js +++ b/functions/src/https/eyeTracking.js @@ -1,102 +1,102 @@ import { admin, functions } from '../f.firebase.js' -import logger from "../utils/logger.js"; +import logger from '../utils/logger.js' + +const calibrationCorsOrigins = (process.env.EYE_LAB_CORS_ORIGINS || '') + .split(',') + .map((origin) => origin.trim()) + .filter(Boolean) export const receiveCalibration = functions.onRequest({ - handler: async (req, res) => { - if (req.method !== "POST") { - return res.status(405).send("Method Not Allowed"); - } - - try { - const { - session_id, - screen_height, - screen_width, - k - } = req.body; - - if (!session_id) { - return res.status(400).json({ error: "session_id is required" }); - } - - const db = admin.firestore(); - - const calibRef = db.collection("calibrations").doc(); - const calibId = calibRef.id; - - const calibrationData = { - session_id, - screen_height, - screen_width, - k, - createdAt: admin.firestore.FieldValue.serverTimestamp(), - }; - - await calibRef.set(calibrationData); - - const userDocRef = db.collection("users").doc(session_id); - const userDoc = await userDocRef.get(); - - if (userDoc.exists) { - await userDocRef.update({ - calibrationId: calibId - }); - } else { - await userDocRef.set({ - calibrationId: calibId - }); - } - - return res.status(200).json({ message: "Calibration saved and user updated successfully" }); - - } catch (error) { - logger.error("Error saving calibration:", { error }); - return res.status(500).json({ error: error.message }); - } + handler: async (req, res) => { + if (req.method !== 'POST') { + return res.status(405).send('Method Not Allowed') + } + + try { + const { session_id, screen_height, screen_width, k } = req.body + + if (!session_id) { + return res.status(400).json({ error: 'session_id is required' }) + } + + const db = admin.firestore() + + const calibRef = db.collection('calibrations').doc() + const calibId = calibRef.id + + const calibrationData = { + session_id, + screen_height, + screen_width, + k, + createdAt: admin.firestore.FieldValue.serverTimestamp(), + } + + await calibRef.set(calibrationData) + + const userDocRef = db.collection('users').doc(session_id) + const userDoc = await userDocRef.get() + + if (userDoc.exists) { + await userDocRef.update({ + calibrationId: calibId, + }) + } else { + await userDocRef.set({ + calibrationId: calibId, + }) + } + + return res + .status(200) + .json({ message: 'Calibration saved and user updated successfully' }) + } catch (error) { + logger.error('Error saving calibration:', { error }) + return res.status(500).json({ error: error.message }) } -}); + }, +}) export const getCalibrationConfig = functions.onRequest({ - opts: { - cors: [ - "https://eye-tracking-28179.web.app", - ], - }, - handler: async (req, res) => { - if (req.method !== "GET") { - return res.status(405).send("Method Not Allowed"); - } - - try { - const { testId } = req.query; - - if (!testId) { - return res.status(400).json({ error: "testId is required" }); - } - - const db = admin.firestore(); - const testRef = db.collection("tests").doc(testId); - const testDoc = await testRef.get(); - - if (!testDoc.exists) { - return res.status(404).json({ error: "Test not found" }); - } - - const testData = testDoc.data(); - const calibrationConfig = testData.calibrationConfig || null; - - if (!calibrationConfig) { - return res.status(404).json({ error: "Calibration config not found in test" }); - } - - return res.status(200).json({ - testId, - calibrationConfig - }); - - } catch (error) { - logger.error("Error getting calibration config:", { error }); - return res.status(500).json({ error: error.message }); - } + opts: { + cors: calibrationCorsOrigins, + }, + handler: async (req, res) => { + if (req.method !== 'GET') { + return res.status(405).send('Method Not Allowed') + } + + try { + const { testId } = req.query + + if (!testId) { + return res.status(400).json({ error: 'testId is required' }) + } + + const db = admin.firestore() + const testRef = db.collection('tests').doc(testId) + const testDoc = await testRef.get() + + if (!testDoc.exists) { + return res.status(404).json({ error: 'Test not found' }) + } + + const testData = testDoc.data() + const calibrationConfig = testData.calibrationConfig || null + + if (!calibrationConfig) { + return res + .status(404) + .json({ error: 'Calibration config not found in test' }) + } + + return res.status(200).json({ + testId, + calibrationConfig, + }) + } catch (error) { + logger.error('Error getting calibration config:', { error }) + return res.status(500).json({ error: error.message }) } -}); \ No newline at end of file + }, +}) diff --git a/functions/src/scheduled/cleanupGhostSessions.js b/functions/src/scheduled/cleanupGhostSessions.js index 2596bf139..f2f38f9d2 100644 --- a/functions/src/scheduled/cleanupGhostSessions.js +++ b/functions/src/scheduled/cleanupGhostSessions.js @@ -1,65 +1,69 @@ - import { onSchedule } from 'firebase-functions/v2/scheduler' import { admin } from '../f.firebase.js' +import logger from '../utils/logger.js' // Run every 6 hours -export const cleanupGhostSessions = onSchedule('every 6 hours', async (event) => { - const db = admin.database() - const roomsRef = db.ref('rooms') - const callsRef = db.ref('calls') +export const cleanupGhostSessions = onSchedule( + 'every 6 hours', + async (event) => { + const db = admin.database() + const roomsRef = db.ref('rooms') + const callsRef = db.ref('calls') - const now = Date.now() - const cutoffTime = now - 6 * 60 * 60 * 1000 // 6 hours ago + const now = Date.now() + const cutoffTime = now - 6 * 60 * 60 * 1000 // 6 hours ago - try { - const changes = {} - let deletedCount = 0 + try { + const changes = {} + let deletedCount = 0 - // 1. Process Rooms - const roomsSnapshot = await roomsRef.get() - if (roomsSnapshot.exists()) { - roomsSnapshot.forEach((child) => { - const roomId = child.key - const roomData = child.val() + // 1. Process Rooms + const roomsSnapshot = await roomsRef.get() + if (roomsSnapshot.exists()) { + roomsSnapshot.forEach((child) => { + const roomId = child.key + const roomData = child.val() - const createdAt = roomData.createdAt || 0 - const lastUpdate = roomData.lastUpdate || 0 - const lastActive = Math.max(createdAt, lastUpdate) + const createdAt = roomData.createdAt || 0 + const lastUpdate = roomData.lastUpdate || 0 + const lastActive = Math.max(createdAt, lastUpdate) - // Check if room is stale - if ( - (lastActive > 0 && lastActive < cutoffTime) || - lastActive === 0 - ) { - changes[`rooms/${roomId}`] = null - changes[`calls/${roomId}`] = null // Ensure call is deleted too - deletedCount++ - } - }) - } + // Check if room is stale + if ((lastActive > 0 && lastActive < cutoffTime) || lastActive === 0) { + changes[`rooms/${roomId}`] = null + changes[`calls/${roomId}`] = null // Ensure call is deleted too + deletedCount++ + } + }) + } - // 2. Process Calls (Check for orphans) - const callsSnapshot = await callsRef.get() - if (callsSnapshot.exists()) { - callsSnapshot.forEach((child) => { - const callId = child.key - // If we already marked this call for deletion via room check, skip - if (changes[`calls/${callId}`] === null) return - - // If a call exists but NO room exists for it, it is an orphan -> DELETE. - const roomExists = roomsSnapshot.hasChild(callId) - if (!roomExists) { - changes[`calls/${callId}`] = null - deletedCount++ - } - }) - } + // 2. Process Calls (Check for orphans) + const callsSnapshot = await callsRef.get() + if (callsSnapshot.exists()) { + callsSnapshot.forEach((child) => { + const callId = child.key + // If we already marked this call for deletion via room check, skip + if (changes[`calls/${callId}`] === null) return + + // If a call exists but NO room exists for it, it is an orphan -> DELETE. + const roomExists = roomsSnapshot.hasChild(callId) + if (!roomExists) { + changes[`calls/${callId}`] = null + deletedCount++ + } + }) + } - if (deletedCount > 0) { - await db.ref().update(changes) - console.log(`Cleaned up ${deletedCount} items (ghost sessions/calls).`) + if (deletedCount > 0) { + await db.ref().update(changes) + logger.info('cleanupGhostSessions: cleaned up ghost sessions', { + deletedCount, + }) + } + } catch (error) { + logger.error('cleanupGhostSessions: failed to clean up ghost sessions', { + error: error.message, + }) } - } catch (error) { - console.error('Error cleaning up ghost sessions:', error) - } -}) + }, +) diff --git a/functions/src/templates/mails/emailVerification.html b/functions/src/templates/mails/emailVerification.html new file mode 100644 index 000000000..a57c17d70 --- /dev/null +++ b/functions/src/templates/mails/emailVerification.html @@ -0,0 +1,196 @@ + + + + + + Verify Your Email + + + +
+ +
+ +

Verify Your Email

+

Complete your registration

+
+ + +
+
+ Hello {{userName}}, +
+ +
+ Thank you for signing up! To complete your registration and start using RUXAILAB, please verify your email address by clicking the button below. +
+ + + +
+ + + +
+ + diff --git a/functions/src/templates/mails/invitations.html b/functions/src/templates/mails/invitations.html index e0553a1cb..3f0b840c7 100644 --- a/functions/src/templates/mails/invitations.html +++ b/functions/src/templates/mails/invitations.html @@ -46,7 +46,7 @@ RUXAILAB Logo diff --git a/i18n-diff-guard.js b/i18n-diff-guard.mjs similarity index 100% rename from i18n-diff-guard.js rename to i18n-diff-guard.mjs diff --git a/package-lock.json b/package-lock.json index 204a5f029..77cd1e17f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@tiptap/vue-3": "^2.27.2", "@vue/babel-plugin-jsx": "^2.0.1", "@vueup/vue-quill": "^1.2.0", - "axios": "^1.13.5", + "axios": "^1.13.6", "buffer": "^6.0.3", "chart.js": "^4.5.1", "chartjs-adapter-date-fns": "^3.0.0", @@ -41,7 +41,7 @@ "process": "^0.11.10", "stream-browserify": "^3.0.0", "uid-generator": "^2.0.0", - "vue": "^3.5.28", + "vue": "^3.5.29", "vue-chartjs": "^5.3.3", "vue-i18n": "^11.2.7", "vue-loader": "^17.4.2", @@ -50,7 +50,7 @@ "vue3-quill": "^0.3.1", "vue3-text-clamp": "^0.1.2", "vuedraggable": "^4.1.0", - "vuetify": "^3.11.8", + "vuetify": "^3.12.1", "vuex": "^4.0.2", "wavesurfer.js": "^7.12.1" }, @@ -58,12 +58,12 @@ "@babel/core": "^7.29.0", "@babel/eslint-parser": "^7.28.6", "@babel/plugin-proposal-optional-chaining": "^7.21.0", - "@intlify/eslint-plugin-vue-i18n": "^4.1.1", + "@intlify/eslint-plugin-vue-i18n": "^4.2.0", "@playwright/test": "^1.58.2", "@testing-library/cypress": "^10.0.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/vue": "^8.1.0", - "@types/node": "^25.2.2", + "@types/node": "^25.3.2", "@vue/cli-plugin-babel": "^5.0.8", "@vue/cli-plugin-router": "^5.0.8", "@vue/cli-plugin-unit-jest": "^5.0.8", @@ -73,10 +73,10 @@ "@vue/test-utils": "^2.4.6", "@vue/vue3-jest": "^27.0.0", "babel-plugin-transform-require-context": "^0.1.1", - "cypress": "^15.10.0", - "eslint": "^9.39.2", + "cypress": "^15.11.0", + "eslint": "^9.39.3", "eslint-config-prettier": "^10.1.8", - "eslint-plugin-vue": "^10.7.0", + "eslint-plugin-vue": "^10.8.0", "eslint-plugin-vuetify": "^2.5.2", "globals": "^17.3.0", "husky": "^9.1.7", @@ -85,7 +85,7 @@ "prettier": "^3.8.1", "sass": "~1.32.6", "sass-loader": "~8.0.0", - "vue-eslint-parser": "^10.1.3" + "vue-eslint-parser": "^10.4.0" } }, "node_modules/@achrinza/node-ipc": { @@ -2185,9 +2185,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.3.tgz", + "integrity": "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==", "dev": true, "license": "MIT", "engines": { @@ -3286,9 +3286,9 @@ } }, "node_modules/@intlify/eslint-plugin-vue-i18n": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@intlify/eslint-plugin-vue-i18n/-/eslint-plugin-vue-i18n-4.1.1.tgz", - "integrity": "sha512-TgeLqdNEwt9wDOxXwJ7lSxw4f2saXd0QfZ4ZM3oCU4m1H1r1tBbuV6nRuhtsy4DwGqC0nlFFhsvKONlEyUxZkg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@intlify/eslint-plugin-vue-i18n/-/eslint-plugin-vue-i18n-4.2.0.tgz", + "integrity": "sha512-1TtqzK0yrbjwYDs92Ob5O2xEMN5gcCM2MrdJ6FKA85ag6g1psyBGabxOPGiFzIcCyzrROGH72iIkvHkGKn8XWQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3307,13 +3307,13 @@ "lodash": "^4.17.21", "parse5": "^7.1.2", "semver": "^7.5.4", - "synckit": "^0.10.0" + "synckit": "^0.11.0" }, "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "eslint": "^8.0.0 || ^9.0.0-0", + "eslint": "^8.0.0 || ^9.0.0-0 || ^10.0.0", "jsonc-eslint-parser": "^2.3.0", "vue-eslint-parser": "^10.0.0", "yaml-eslint-parser": "^1.2.2" @@ -5330,12 +5330,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.2.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.2.tgz", - "integrity": "sha512-BkmoP5/FhRYek5izySdkOneRyXYN35I860MFAGupTdebyE66uZaR+bXLHq8k4DirE5DwQi3NuhvRU1jqTVwUrQ==", + "version": "25.3.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.2.tgz", + "integrity": "sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q==", "license": "MIT", "dependencies": { - "undici-types": "~7.16.0" + "undici-types": "~7.18.0" } }, "node_modules/@types/node-fetch": { @@ -6128,39 +6128,39 @@ "license": "ISC" }, "node_modules/@vue/compiler-core": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.28.tgz", - "integrity": "sha512-kviccYxTgoE8n6OCw96BNdYlBg2GOWfBuOW4Vqwrt7mSKWKwFVvI8egdTltqRgITGPsTFYtKYfxIG8ptX2PJHQ==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.29.tgz", + "integrity": "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==", "license": "MIT", "dependencies": { "@babel/parser": "^7.29.0", - "@vue/shared": "3.5.28", + "@vue/shared": "3.5.29", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "node_modules/@vue/compiler-dom": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.28.tgz", - "integrity": "sha512-/1ZepxAb159jKR1btkefDP+J2xuWL5V3WtleRmxaT+K2Aqiek/Ab/+Ebrw2pPj0sdHO8ViAyyJWfhXXOP/+LQA==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.29.tgz", + "integrity": "sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==", "license": "MIT", "dependencies": { - "@vue/compiler-core": "3.5.28", - "@vue/shared": "3.5.28" + "@vue/compiler-core": "3.5.29", + "@vue/shared": "3.5.29" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.28.tgz", - "integrity": "sha512-6TnKMiNkd6u6VeVDhZn/07KhEZuBSn43Wd2No5zaP5s3xm8IqFTHBj84HJah4UepSUJTro5SoqqlOY22FKY96g==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.29.tgz", + "integrity": "sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==", "license": "MIT", "dependencies": { "@babel/parser": "^7.29.0", - "@vue/compiler-core": "3.5.28", - "@vue/compiler-dom": "3.5.28", - "@vue/compiler-ssr": "3.5.28", - "@vue/shared": "3.5.28", + "@vue/compiler-core": "3.5.29", + "@vue/compiler-dom": "3.5.29", + "@vue/compiler-ssr": "3.5.29", + "@vue/shared": "3.5.29", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.6", @@ -6168,13 +6168,13 @@ } }, "node_modules/@vue/compiler-ssr": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.28.tgz", - "integrity": "sha512-JCq//9w1qmC6UGLWJX7RXzrGpKkroubey/ZFqTpvEIDJEKGgntuDMqkuWiZvzTzTA5h2qZvFBFHY7fAAa9475g==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.29.tgz", + "integrity": "sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.28", - "@vue/shared": "3.5.28" + "@vue/compiler-dom": "3.5.29", + "@vue/shared": "3.5.29" } }, "node_modules/@vue/component-compiler-utils": { @@ -6271,53 +6271,53 @@ "license": "MIT" }, "node_modules/@vue/reactivity": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.28.tgz", - "integrity": "sha512-gr5hEsxvn+RNyu9/9o1WtdYdwDjg5FgjUSBEkZWqgTKlo/fvwZ2+8W6AfKsc9YN2k/+iHYdS9vZYAhpi10kNaw==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.29.tgz", + "integrity": "sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA==", "license": "MIT", "dependencies": { - "@vue/shared": "3.5.28" + "@vue/shared": "3.5.29" } }, "node_modules/@vue/runtime-core": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.28.tgz", - "integrity": "sha512-POVHTdbgnrBBIpnbYU4y7pOMNlPn2QVxVzkvEA2pEgvzbelQq4ZOUxbp2oiyo+BOtiYlm8Q44wShHJoBvDPAjQ==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.29.tgz", + "integrity": "sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.28", - "@vue/shared": "3.5.28" + "@vue/reactivity": "3.5.29", + "@vue/shared": "3.5.29" } }, "node_modules/@vue/runtime-dom": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.28.tgz", - "integrity": "sha512-4SXxSF8SXYMuhAIkT+eBRqOkWEfPu6nhccrzrkioA6l0boiq7sp18HCOov9qWJA5HML61kW8p/cB4MmBiG9dSA==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.29.tgz", + "integrity": "sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg==", "license": "MIT", "dependencies": { - "@vue/reactivity": "3.5.28", - "@vue/runtime-core": "3.5.28", - "@vue/shared": "3.5.28", + "@vue/reactivity": "3.5.29", + "@vue/runtime-core": "3.5.29", + "@vue/shared": "3.5.29", "csstype": "^3.2.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.28.tgz", - "integrity": "sha512-pf+5ECKGj8fX95bNincbzJ6yp6nyzuLDhYZCeFxUNp8EBrQpPpQaLX3nNCp49+UbgbPun3CeVE+5CXVV1Xydfg==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.29.tgz", + "integrity": "sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g==", "license": "MIT", "dependencies": { - "@vue/compiler-ssr": "3.5.28", - "@vue/shared": "3.5.28" + "@vue/compiler-ssr": "3.5.29", + "@vue/shared": "3.5.29" }, "peerDependencies": { - "vue": "3.5.28" + "vue": "3.5.29" } }, "node_modules/@vue/shared": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.28.tgz", - "integrity": "sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.29.tgz", + "integrity": "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==", "license": "MIT" }, "node_modules/@vue/test-utils": { @@ -7219,9 +7219,9 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.13.5", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", - "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.11", @@ -9167,9 +9167,9 @@ "license": "MIT" }, "node_modules/cypress": { - "version": "15.10.0", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-15.10.0.tgz", - "integrity": "sha512-OtUh7OMrfEjKoXydlAD1CfG2BvKxIqgWGY4/RMjrqQ3BKGBo5JFKoYNH+Tpcj4xKxWH4XK0Xri+9y8WkxhYbqQ==", + "version": "15.11.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-15.11.0.tgz", + "integrity": "sha512-NXDE6/fqZuzh1Zr53nyhCCa4lcANNTYWQNP9fJO+tzD3qVTDaTUni5xXMuigYjMujQ7CRiT9RkJJONmPQSsDFw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -9211,9 +9211,10 @@ "proxy-from-env": "1.0.0", "request-progress": "^3.0.0", "supports-color": "^8.1.1", - "systeminformation": "^5.27.14", + "systeminformation": "^5.31.1", "tmp": "~0.2.4", "tree-kill": "1.2.2", + "tslib": "1.14.1", "untildify": "^4.0.0", "yauzl": "^2.10.0" }, @@ -9338,6 +9339,13 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/cypress/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" + }, "node_modules/dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -10323,9 +10331,9 @@ } }, "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "version": "9.39.3", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.3.tgz", + "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", "dependencies": { @@ -10335,7 +10343,7 @@ "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", + "@eslint/js": "9.39.3", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -10428,9 +10436,9 @@ } }, "node_modules/eslint-plugin-vue": { - "version": "10.7.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.7.0.tgz", - "integrity": "sha512-r2XFCK4qlo1sxEoAMIoTTX0PZAdla0JJDt1fmYiworZUX67WeEGqm+JbyAg3M+pGiJ5U6Mp5WQbontXWtIW7TA==", + "version": "10.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.8.0.tgz", + "integrity": "sha512-f1J/tcbnrpgC8suPN5AtdJ5MQjuXbSU9pGRSSYAuF3SHoiYCOdEX6O22pLaRyLHXvDcOe+O5ENgc1owQ587agA==", "dev": true, "license": "MIT", "dependencies": { @@ -10447,7 +10455,7 @@ "peerDependencies": { "@stylistic/eslint-plugin": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", "@typescript-eslint/parser": "^7.0.0 || ^8.0.0", - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "vue-eslint-parser": "^10.0.0" }, "peerDependenciesMeta": { @@ -20916,33 +20924,25 @@ "license": "MIT" }, "node_modules/synckit": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.10.4.tgz", - "integrity": "sha512-2SG1TnJGjMkD4+gblONMGYSrwAzYi+ymOitD+Jb/iMYm57nH20PlkVeMQRah3yDMKEa0QQYUF/QPWpdW7C6zNg==", + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", "dev": true, "license": "MIT", "dependencies": { - "@pkgr/core": "^0.2.0", - "tslib": "^2.8.1" + "@pkgr/core": "^0.2.9" }, "engines": { "node": "^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/unts" + "url": "https://opencollective.com/synckit" } }, - "node_modules/synckit/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD" - }, "node_modules/systeminformation": { - "version": "5.30.7", - "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.30.7.tgz", - "integrity": "sha512-33B/cftpaWdpvH+Ho9U1b08ss8GQuLxrWHelbJT1yw4M48Taj8W3ezcPuaLoIHZz5V6tVHuQPr5BprEfnBLBMw==", + "version": "5.31.1", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.31.1.tgz", + "integrity": "sha512-6pRwxoGeV/roJYpsfcP6tN9mep6pPeCtXbUOCdVa0nme05Brwcwdge/fVNhIZn2wuUitAKZm4IYa7QjnRIa9zA==", "dev": true, "license": "MIT", "os": [ @@ -21620,9 +21620,9 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { @@ -21866,16 +21866,16 @@ "license": "MIT" }, "node_modules/vue": { - "version": "3.5.28", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.28.tgz", - "integrity": "sha512-BRdrNfeoccSoIZeIhyPBfvWSLFP4q8J3u8Ju8Ug5vu3LdD+yTM13Sg4sKtljxozbnuMu1NB1X5HBHRYUzFocKg==", + "version": "3.5.29", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.29.tgz", + "integrity": "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==", "license": "MIT", "dependencies": { - "@vue/compiler-dom": "3.5.28", - "@vue/compiler-sfc": "3.5.28", - "@vue/runtime-dom": "3.5.28", - "@vue/server-renderer": "3.5.28", - "@vue/shared": "3.5.28" + "@vue/compiler-dom": "3.5.29", + "@vue/compiler-sfc": "3.5.29", + "@vue/runtime-dom": "3.5.29", + "@vue/server-renderer": "3.5.29", + "@vue/shared": "3.5.29" }, "peerDependencies": { "typescript": "*" @@ -21904,16 +21904,16 @@ "license": "MIT" }, "node_modules/vue-eslint-parser": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.2.0.tgz", - "integrity": "sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.4.0.tgz", + "integrity": "sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg==", "dev": true, "license": "MIT", "dependencies": { "debug": "^4.4.0", - "eslint-scope": "^8.2.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", + "eslint-scope": "^8.2.0 || ^9.0.0", + "eslint-visitor-keys": "^4.2.0 || ^5.0.0", + "espree": "^10.3.0 || ^11.0.0", "esquery": "^1.6.0", "semver": "^7.6.3" }, @@ -21924,7 +21924,7 @@ "url": "https://github.com/sponsors/mysticatea" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" } }, "node_modules/vue-eslint-parser/node_modules/eslint-scope": { @@ -22116,9 +22116,9 @@ } }, "node_modules/vuetify": { - "version": "3.11.8", - "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.11.8.tgz", - "integrity": "sha512-4iKnntOnLFFklygZjzlVfcHrtLO8+iK4HOhiia6HP2U8v82x+ngaSCgm+epvPrGyCMfCpfuEttqD2qElrr1axw==", + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.12.1.tgz", + "integrity": "sha512-JDHDzs1e195YJ9L3X4nWQySGSMyTxr0BefIY4+l/CpAgTd9pPV5F6oZzI8ZLuikMxS4HhfSGHteOAe6u/zh4vQ==", "license": "MIT", "funding": { "type": "github", diff --git a/package.json b/package.json index 3669b2df7..acd37051b 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "@tiptap/vue-3": "^2.27.2", "@vue/babel-plugin-jsx": "^2.0.1", "@vueup/vue-quill": "^1.2.0", - "axios": "^1.13.5", + "axios": "^1.13.6", "buffer": "^6.0.3", "chart.js": "^4.5.1", "chartjs-adapter-date-fns": "^3.0.0", @@ -54,7 +54,7 @@ "process": "^0.11.10", "stream-browserify": "^3.0.0", "uid-generator": "^2.0.0", - "vue": "^3.5.28", + "vue": "^3.5.29", "vue-chartjs": "^5.3.3", "vue-i18n": "^11.2.7", "vue-loader": "^17.4.2", @@ -63,7 +63,7 @@ "vue3-quill": "^0.3.1", "vue3-text-clamp": "^0.1.2", "vuedraggable": "^4.1.0", - "vuetify": "^3.11.8", + "vuetify": "^3.12.1", "vuex": "^4.0.2", "wavesurfer.js": "^7.12.1" }, @@ -71,12 +71,12 @@ "@babel/core": "^7.29.0", "@babel/eslint-parser": "^7.28.6", "@babel/plugin-proposal-optional-chaining": "^7.21.0", - "@intlify/eslint-plugin-vue-i18n": "^4.1.1", + "@intlify/eslint-plugin-vue-i18n": "^4.2.0", "@playwright/test": "^1.58.2", "@testing-library/cypress": "^10.0.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/vue": "^8.1.0", - "@types/node": "^25.2.2", + "@types/node": "^25.3.2", "@vue/cli-plugin-babel": "^5.0.8", "@vue/cli-plugin-router": "^5.0.8", "@vue/cli-plugin-unit-jest": "^5.0.8", @@ -86,10 +86,10 @@ "@vue/test-utils": "^2.4.6", "@vue/vue3-jest": "^27.0.0", "babel-plugin-transform-require-context": "^0.1.1", - "cypress": "^15.10.0", - "eslint": "^9.39.2", + "cypress": "^15.11.0", + "eslint": "^9.39.3", "eslint-config-prettier": "^10.1.8", - "eslint-plugin-vue": "^10.7.0", + "eslint-plugin-vue": "^10.8.0", "eslint-plugin-vuetify": "^2.5.2", "globals": "^17.3.0", "husky": "^9.1.7", @@ -98,7 +98,7 @@ "prettier": "^3.8.1", "sass": "~1.32.6", "sass-loader": "~8.0.0", - "vue-eslint-parser": "^10.1.3" + "vue-eslint-parser": "^10.4.0" }, "lint-staged": { "src/**/*.{js,vue}": [ diff --git a/src/app/plugins/locales/ar.json b/src/app/plugins/locales/ar.json index 15ad53358..e966ac0d3 100644 --- a/src/app/plugins/locales/ar.json +++ b/src/app/plugins/locales/ar.json @@ -43,7 +43,34 @@ "fair": "ู…ุชูˆุณุท", "good": "ุฌูŠุฏ", "strong": "ู‚ูˆูŠ" - } + }, + "emailNotVerified": "ุงู„ุจุฑูŠุฏ ุงู„ุฅู„ูƒุชุฑูˆู†ูŠ ุบูŠุฑ ู…ูุชุญู‚ู‚", + "verificationEmailSent": "ุชู… ุฅุฑุณุงู„ ุจุฑูŠุฏ ุงู„ุชุญู‚ู‚ ุจู†ุฌุงุญ!", + "errorSendingVerification": "ุฎุทุฃ ููŠ ุฅุฑุณุงู„ ุจุฑูŠุฏ ุงู„ุชุญู‚ู‚", + "verifyEmailTitle": "ุชุญู‚ู‚ ู…ู† ุจุฑูŠุฏูƒ ุงู„ุฅู„ูƒุชุฑูˆู†ูŠ", + "verifyEmailSubtitle": "ู„ู‚ุฏ ุฃุฑุณู„ู†ุง ุฑุงุจุท ุชุญู‚ู‚ ุฅู„ู‰ ุจุฑูŠุฏูƒ ุงู„ุฅู„ูƒุชุฑูˆู†ูŠ", + "emailLabel": "ุงู„ุจุฑูŠุฏ ุงู„ุฅู„ูƒุชุฑูˆู†ูŠ", + "verifyingEmail": "ุฌุงุฑูŠ ุงู„ุชุญู‚ู‚...", + "emailAlreadyVerified": "ู‡ู„ ุชู… ุงู„ุชุญู‚ู‚ ู…ู† ุงู„ุจุฑูŠุฏ ุงู„ุฅู„ูƒุชุฑูˆู†ูŠ ุจุงู„ูุนู„ุŸ", + "emailVerified": "ุชู… ุงู„ุชุญู‚ู‚ ู…ู† ุงู„ุจุฑูŠุฏ ุงู„ุฅู„ูƒุชุฑูˆู†ูŠ ุจู†ุฌุงุญ!", + "checkEmailTitle": "ุชุญู‚ู‚ ู…ู† ุจุฑูŠุฏูƒ ุงู„ุฅู„ูƒุชุฑูˆู†ูŠ", + "checkEmailStep1": "ุชุญู‚ู‚ ู…ู† ุตู†ุฏูˆู‚ ุงู„ูˆุงุฑุฏ ู„ู„ุญุตูˆู„ ุนู„ู‰ ุจุฑูŠุฏ ุชุญู‚ู‚", + "checkEmailStep2": "ุงู†ู‚ุฑ ุนู„ู‰ ุงู„ุฑุงุจุท ููŠ ุงู„ุจุฑูŠุฏ ู„ู„ุชุญู‚ู‚ ู…ู† ุญุณุงุจูƒ", + "checkEmailStep3": "ุนุฏ ู‡ู†ุง ูˆุณูŠุชู… ุงู„ุชุญู‚ู‚ ู…ู† ุญุณุงุจูƒ", + "checkSpamFolder": "ู„ุง ุชุฑู‰ุŸ ุชุญู‚ู‚ ู…ู† ู…ุฌู„ุฏ ุงู„ุจุฑูŠุฏ ุงู„ุนุดูˆุงุฆูŠ", + "resending": "ุฌุงุฑูŠ ุงู„ุฅุนุงุฏุฉ...", + "resendEmail": "ุฅุนุงุฏุฉ ุฅุฑุณุงู„ ุงู„ุจุฑูŠุฏ", + "changeEmail": "ุชุบูŠูŠุฑ ุงู„ุจุฑูŠุฏ ุงู„ุฅู„ูƒุชุฑูˆู†ูŠ", + "continueToDashboard": "ุงู„ู…ุชุงุจุนุฉ ุฅู„ู‰ ู„ูˆุญุฉ ุงู„ุชุญูƒู…", + "signOut": "ุชุณุฌูŠู„ ุงู„ุฎุฑูˆุฌ", + "newEmail": "ุงู„ุจุฑูŠุฏ ุงู„ุฅู„ูƒุชุฑูˆู†ูŠ ุงู„ุฌุฏูŠุฏ", + "enterNewEmail": "ุฃุฏุฎู„ ุนู†ูˆุงู† ุจุฑูŠุฏูƒ ุงู„ุฅู„ูƒุชุฑูˆู†ูŠ ุงู„ุฌุฏูŠุฏ", + "saving": "ุฌุงุฑูŠ ุงู„ุญูุธ...", + "errorCheckingVerification": "ุฎุทุฃ ููŠ ูุญุต ุญุงู„ุฉ ุงู„ุชุญู‚ู‚", + "errorResendingEmail": "ุฎุทุฃ ููŠ ุฅุนุงุฏุฉ ุฅุฑุณุงู„ ุจุฑูŠุฏ ุงู„ุชุญู‚ู‚", + "invalidEmail": "ุนู†ูˆุงู† ุจุฑูŠุฏ ุฅู„ูƒุชุฑูˆู†ูŠ ุบูŠุฑ ุตุงู„ุญ", + "emailUpdatedVerification": "ุชู… ุชุญุฏูŠุซ ุงู„ุจุฑูŠุฏ! ู„ู‚ุฏ ุฃุฑุณู„ู†ุง ุฑุงุจุท ุชุญู‚ู‚ ุฅู„ู‰ ุจุฑูŠุฏูƒ ุงู„ุฌุฏูŠุฏ.", + "errorUpdatingEmail": "ุฎุทุฃ ููŠ ุชุญุฏูŠุซ ุงู„ุจุฑูŠุฏ ุงู„ุฅู„ูƒุชุฑูˆู†ูŠ" }, "common": { "enterTextHere": "ุฃุฏุฎู„ ุงู„ู†ุต ู‡ู†ุง...", @@ -112,6 +139,7 @@ "user": "ู…ุณุชุฎุฏู…", "id": "ID", "itemsPerPage": "ุนุฏุฏ ุงู„ุนู†ุงุตุฑ ููŠ ุงู„ุตูุญุฉ:", + "saving": "ุฌุงุฑูŠ ุงู„ุญูุธ...", "visible": "ู…ุฑุฆูŠ" }, "storage": { @@ -384,7 +412,10 @@ "globalError": "ุญุฏุซ ุฎุทุฃุŒ ู†ุนุชุฐุฑ ุนู† ุงู„ุฅุฒุนุงุฌ.", "cameraPermissionDenied": "ูŠุชุทู„ุจ ุงู„ู…ุชุงุจุนุฉ ุงู„ุณู…ุงุญ ุจุงู„ูˆุตูˆู„ ุฅู„ู‰ ุงู„ูƒุงู…ูŠุฑุง. ูŠุฑุฌู‰ ุชูุนูŠู„ ุฅุฐู† ุงู„ูƒุงู…ูŠุฑุง ููŠ ุฅุนุฏุงุฏุงุช ุงู„ู…ุชุตูุญ.", "sendError": "ูุดู„ ุฅุฑุณุงู„ ุงู„ุฏุนูˆุฉ. ูŠุฑุฌู‰ ุงู„ู…ุญุงูˆู„ุฉ ู…ุฑุฉ ุฃุฎุฑู‰", - "missingVariableTitles": "ู„ุง ูŠู…ูƒู† ุงู„ุญูุธ: ุจุนุถ ุงู„ู…ุชุบูŠุฑุงุช ุชูุชู‚ุฏ ุงู„ุนู†ุงูˆูŠู†" + "missingVariableTitles": "ู„ุง ูŠู…ูƒู† ุงู„ุญูุธ: ุจุนุถ ุงู„ู…ุชุบูŠุฑุงุช ุชูุชู‚ุฏ ุงู„ุนู†ุงูˆูŠู†", + "failedToLoadAnswers": "Failed to load answers", + "failedToUpdateAnswer": "Failed to update answer", + "failedToSaveAnswerCooperator": "Failed to save cooperator answer" }, "Introduction": { "title": "ู…ุฎุชุจุฑ UX ุนู† ุจุนุฏ", @@ -1194,6 +1225,44 @@ "note": "ุฏูˆุฑูƒ ู‡ูˆ ุงู„ุชุณู‡ูŠู„ ูˆุงู„ู…ุฑุงู‚ุจุฉ. ูŠุฌุจ ุนู„ู‰ ุงู„ู…ุดุงุฑูƒ ุฅูƒู…ุงู„ ุงู„ู…ู‡ุงู… ุจุดูƒู„ ู…ุณุชู‚ู„ ู…ุง ู„ู… ูŠุทู„ุจ ุงู„ู…ุณุงุนุฏุฉ ุจุดูƒู„ ุฎุงุต.", "startSession": "ุจุฏุก ุงู„ุฌู„ุณุฉ ุงู„ุฎุงุถุนุฉ ู„ู„ุฅุดุฑุงู" }, + "VideoCallPanel": { + "toolsPanelTitle": "ู„ูˆุญุฉ ุงู„ุฃุฏูˆุงุช", + "sessionControl": "ุงู„ุชุญูƒู… ููŠ ุงู„ุฌู„ุณุฉ", + "joinRoomInfo": "ุนู†ุงุตุฑ ุงู„ุงู†ุถู…ุงู… ุฅู„ู‰ ุงู„ุบุฑูุฉ ุฃุตุจุญุช ุงู„ุขู† ููŠ ุงู„ูˆุงุฌู‡ุฉ ุงู„ุฑุฆูŠุณูŠุฉ ุจุงู„ุฃุนู„ู‰", + "proceedNextStep": "ุงู„ุงู†ุชู‚ุงู„ ุฅู„ู‰ ุงู„ุฎุทูˆุฉ ุงู„ุชุงู„ูŠุฉ", + "endCall": "ุฅู†ู‡ุงุก ุงู„ู…ูƒุงู„ู…ุฉ", + "activeCall": "ู…ูƒุงู„ู…ุฉ ู†ุดุทุฉ", + "participants": "ุงู„ู…ุดุงุฑูƒูˆู†", + "you": "ุฃู†ุช", + "observator": "ู…ุฑุงู‚ุจ", + "moderator": "ู…ุดุฑู", + "connected": "ู…ุชุตู„", + "disconnected": "ุบูŠุฑ ู…ุชุตู„", + "camera": "ุงู„ูƒุงู…ูŠุฑุง", + "noCamera": "ุจุฏูˆู† ูƒุงู…ูŠุฑุง", + "microphone": "ุงู„ู…ูŠูƒุฑูˆููˆู†", + "noMicrophone": "ุจุฏูˆู† ู…ูŠูƒุฑูˆููˆู†", + "settings": "ุงู„ุฅุนุฏุงุฏุงุช", + "disableCamera": "ุฅูŠู‚ุงู ุงู„ูƒุงู…ูŠุฑุง", + "enableCamera": "ุชุดุบูŠู„ ุงู„ูƒุงู…ูŠุฑุง", + "muteMicrophone": "ูƒุชู… ุงู„ู…ูŠูƒุฑูˆููˆู†", + "unmuteMicrophone": "ุฅู„ุบุงุก ูƒุชู… ุงู„ู…ูŠูƒุฑูˆููˆู†", + "stopScreenShare": "ุฅูŠู‚ุงู ู…ุดุงุฑูƒุฉ ุงู„ุดุงุดุฉ", + "shareScreen": "ู…ุดุงุฑูƒุฉ ุงู„ุดุงุดุฉ", + "moderatorOnlySteps": "ูู‚ุท ุงู„ู…ุดุฑู ูŠู…ูƒู†ู‡ ุชุบูŠูŠุฑ ุงู„ุฎุทูˆุงุช" + }, + "VideoCall": { + "screenSharingLabel": "ู…ุดุงุฑูƒุฉ ุงู„ุดุงุดุฉ", + "cameraOff": "ุงู„ูƒุงู…ูŠุฑุง ู…ุชูˆู‚ูุฉ", + "yourVideo": "ููŠุฏูŠูˆูƒ", + "yourPreview": "ู…ุนุงูŠู†ุชูƒ", + "observatorMode": "ูˆุถุน ุงู„ู…ุฑุงู‚ุจ", + "waitingForModeratorToStartSession": "ุจุงู†ุชุธุงุฑ ุงู„ู…ุดุฑู ู„ุจุฏุก ุงู„ุฌู„ุณุฉ...", + "observeAllFeedsNotice": "ุณุชุชู…ูƒู† ู…ู† ู…ุดุงู‡ุฏุฉ ุฌู…ูŠุน ุงู„ููŠุฏูŠูˆู‡ุงุช ุฏูˆู† ุฅุฑุณุงู„ ููŠุฏูŠูˆูƒ.", + "waitingForParticipants": "ุจุงู†ุชุธุงุฑ ุงู„ู…ุดุงุฑูƒูŠู†...", + "waitingForModerator": "ุจุงู†ุชุธุงุฑ ุงู„ู…ุดุฑู...", + "autoStartWhenModeratorOpensRoom": "ุณุชุจุฏุฃ ู…ูƒุงู„ู…ุฉ ุงู„ููŠุฏูŠูˆ ุชู„ู‚ุงุฆูŠุง ุนู†ุฏู…ุง ูŠูุชุญ ุงู„ู…ุดุฑู ุงู„ุบุฑูุฉ." + }, "WelcomeStep": { "welcome": "ู…ุฑุญุจู‹ุง ุจูƒ ููŠ RUXAILAB!", "description": "ุฃู†ุช ุนู„ู‰ ูˆุดูƒ ุงู„ู…ุดุงุฑูƒุฉ ููŠ ุงุฎุชุจุงุฑ ู…ุณุชุฎุฏู… ูŠู‡ุฏู ุฅู„ู‰ ุชู‚ูŠูŠู… ุณู‡ูˆู„ุฉ ุงู„ุงุณุชุฎุฏุงู… ูˆุงู„ูู‡ู… ู„ุชุทุจูŠู‚ ุฑู‚ู…ูŠ. ูŠุณุงุนุฏู†ุง ู‡ุฐุง ุงู„ู†ูˆุน ู…ู† ุงู„ุงุฎุชุจุงุฑุงุช ููŠ ุงูƒุชุดุงู ุงู„ุญูˆุงุฌุฒ ุงู„ุชูƒู†ูˆู„ูˆุฌูŠุฉ ุงู„ู…ุญุชู…ู„ุฉ ูˆุชุญุณูŠู† ุงู„ุชุฌุฑุจุฉ ู„ู„ุฌู…ูŠุน.", @@ -1584,11 +1653,7 @@ "SelfTest": { "title": "ุงุฎุชุจุงุฑ ุฐุงุชูŠ", "type": "ุบูŠุฑ ู…ุดุฑู ุนู„ูŠู‡", - "text": [ - "ุงู„ุฅุฌุงุจุฉ ููŠ ูˆู‚ุช ูุฑุงุบ", - "ุชุญู„ูŠู„ ู…ุชู‚ุฏู… ู„ู„ุฅุฌุงุจุงุช", - "ุชุฎุตูŠุต ุงู„ู…ู‡ู…ุฉ" - ] + "text": ["ุงู„ุฅุฌุงุจุฉ ููŠ ูˆู‚ุช ูุฑุงุบ", "ุชุญู„ูŠู„ ู…ุชู‚ุฏู… ู„ู„ุฅุฌุงุจุงุช", "ุชุฎุตูŠุต ุงู„ู…ู‡ู…ุฉ"] }, "LiveTest": { "title": "ุงุฎุชุจุงุฑ ู…ุจุงุดุฑ", diff --git a/src/app/plugins/locales/de.json b/src/app/plugins/locales/de.json index 7998a1aeb..97ee04f30 100644 --- a/src/app/plugins/locales/de.json +++ b/src/app/plugins/locales/de.json @@ -43,7 +43,34 @@ "fair": "Mittel", "good": "Gut", "strong": "Stark" - } + }, + "emailNotVerified": "E-Mail nicht verifiziert", + "verificationEmailSent": "Bestรคtigungs-E-Mail erfolgreich versendet!", + "errorSendingVerification": "Fehler beim Versenden der Bestรคtigungs-E-Mail", + "verifyEmailTitle": "Verifizieren Sie Ihre E-Mail", + "verifyEmailSubtitle": "Wir haben einen Bestรคtigungslink an Ihre E-Mail gesendet", + "emailLabel": "E-Mail", + "verifyingEmail": "Verifizierung...", + "emailAlreadyVerified": "E-Mail bereits verifiziert?", + "emailVerified": "E-Mail erfolgreich verifiziert!", + "checkEmailTitle": "รœberprรผfen Sie Ihre E-Mail", + "checkEmailStep1": "รœberprรผfen Sie Ihren Posteingang auf eine Bestรคtigungs-E-Mail", + "checkEmailStep2": "Klicken Sie auf den Link in der E-Mail, um Ihr Konto zu verifizieren", + "checkEmailStep3": "Kehren Sie hier zurรผck und Ihr Konto wird verifiziert", + "checkSpamFolder": "Nicht gesehen? รœberprรผfen Sie Ihren Spam-Ordner", + "resending": "Wird erneut gesendet...", + "resendEmail": "E-Mail erneut senden", + "changeEmail": "E-Mail รคndern", + "continueToDashboard": "Weiter zum Dashboard", + "signOut": "Abmelden", + "newEmail": "Neue E-Mail", + "enterNewEmail": "Geben Sie Ihre neue E-Mail-Adresse ein", + "saving": "Wird gespeichert...", + "errorCheckingVerification": "Fehler beim รœberprรผfen des Verifizierungsstatus", + "errorResendingEmail": "Fehler beim erneuten Versenden der Bestรคtigungs-E-Mail", + "invalidEmail": "Ungรผltige E-Mail-Adresse", + "emailUpdatedVerification": "E-Mail aktualisiert! Wir haben einen Bestรคtigungslink an Ihre neue E-Mail gesendet.", + "errorUpdatingEmail": "Fehler beim Aktualisieren der E-Mail" }, "common": { "enterTextHere": "Text hier eingeben...", @@ -112,6 +139,7 @@ "user": "Benutzer", "id": "ID", "itemsPerPage": "Elemente pro Seite:", + "saving": "Wird gespeichert...", "visible": "Sichtbar" }, "storage": { @@ -384,7 +412,10 @@ "globalError": "Ein Fehler ist aufgetreten, entschuldigen Sie die Unannehmlichkeiten.", "cameraPermissionDenied": "Der Zugriff auf die Kamera ist erforderlich, um fortzufahren. Bitte erlauben Sie den Kamerazugriff in den Browser-Einstellungen.", "sendError": "Einladung konnte nicht gesendet werden. Bitte versuchen Sie es erneut", - "missingVariableTitles": "Kann nicht speichern: Einige Variablen fehlen Titel" + "missingVariableTitles": "Kann nicht speichern: Einige Variablen fehlen Titel", + "failedToLoadAnswers": "Fehler beim Laden der Antworten", + "failedToUpdateAnswer": "Fehler beim Aktualisieren der Antwort", + "failedToSaveAnswerCooperator": "Fehler beim Speichern der Cooperator-Antwort" }, "Introduction": { "title": "UX Remote LAB", @@ -1195,6 +1226,44 @@ "note": "Ihre Rolle besteht darin, zu fรถrdern und zu beobachten. Der Teilnehmer sollte die Aufgaben selbststรคndig erledigen, es sei denn, er bittet ausdrรผcklich um Hilfe.", "startSession": "Moderierte Sitzung starten" }, + "VideoCallPanel": { + "toolsPanelTitle": "Werkzeugbereich", + "sessionControl": "Sitzungssteuerung", + "joinRoomInfo": "Die Steuerung zum Beitreten befindet sich jetzt oben in der Hauptoberflaeche", + "proceedNextStep": "Zum naechsten Schritt", + "endCall": "Anruf beenden", + "activeCall": "Aktiver Anruf", + "participants": "Teilnehmer", + "you": "Du", + "observator": "Beobachter", + "moderator": "Moderator", + "connected": "Verbunden", + "disconnected": "Getrennt", + "camera": "Kamera", + "noCamera": "Keine Kamera", + "microphone": "Mikrofon", + "noMicrophone": "Kein Mikrofon", + "settings": "Einstellungen", + "disableCamera": "Kamera ausschalten", + "enableCamera": "Kamera einschalten", + "muteMicrophone": "Mikrofon stummschalten", + "unmuteMicrophone": "Mikrofon aktivieren", + "stopScreenShare": "Bildschirmfreigabe beenden", + "shareScreen": "Bildschirm freigeben", + "moderatorOnlySteps": "Nur der Moderator kann Schritte aendern" + }, + "VideoCall": { + "screenSharingLabel": "Bildschirm wird geteilt", + "cameraOff": "Kamera ist aus", + "yourVideo": "Dein Video", + "yourPreview": "Deine Vorschau", + "observatorMode": "Beobachtermodus", + "waitingForModeratorToStartSession": "Warte darauf, dass der Moderator die Sitzung startet...", + "observeAllFeedsNotice": "Du kannst alle Video-Feeds beobachten, ohne deinen eigenen zu senden.", + "waitingForParticipants": "Warte auf Teilnehmer...", + "waitingForModerator": "Warte auf Moderator...", + "autoStartWhenModeratorOpensRoom": "Der Videoanruf startet automatisch, wenn der Moderator den Raum oeffnet." + }, "WelcomeStep": { "welcome": "Willkommen bei RUXAILAB!", "description": "Sie werden an einem Benutzertest teilnehmen, der die Benutzerfreundlichkeit und Verstรคndlichkeit einer digitalen Anwendung bewertet. Diese Art von Test hilft uns, mรถgliche technologische Barrieren zu erkennen und die Erfahrung fรผr alle zu verbessern.", diff --git a/src/app/plugins/locales/en.json b/src/app/plugins/locales/en.json index 9588ef59b..3b5021409 100644 --- a/src/app/plugins/locales/en.json +++ b/src/app/plugins/locales/en.json @@ -37,6 +37,33 @@ }, "passwordStrength": "Password Strength", "number": "Number", + "verifyEmailTitle": "Verify Your Email", + "verifyEmailSubtitle": "We've sent a verification link to your email", + "emailLabel": "Email", + "verifyingEmail": "Verifying...", + "emailAlreadyVerified": "Email already verified?", + "checkEmailTitle": "Check Your Email", + "checkEmailStep1": "Check your inbox for a verification email", + "checkEmailStep2": "Click the link in the email to verify your account", + "checkEmailStep3": "Return here and your account will be verified", + "checkSpamFolder": "Don't see it? Check your spam folder", + "resendEmail": "Resend Email", + "resending": "Resending...", + "changeEmail": "Change Email", + "signOut": "Sign Out", + "emailVerified": "Email verified successfully!", + "continueToDashboard": "Continue to Dashboard", + "newEmail": "New Email", + "enterNewEmail": "Enter your new email address", + "emailUpdatedVerification": "Email updated! We've sent a verification link to your new email.", + "verificationEmailSent": "Verification email sent successfully!", + "errorCheckingVerification": "Error checking verification status", + "errorResendingEmail": "Error resending verification email", + "errorUpdatingEmail": "Error updating email", + "success": "Success", + "invalidEmail": "Invalid email address", + "emailNotVerified": "Email not verified", + "errorSendingVerification": "Error sending verification email", "strength": { "veryWeak": "Very Weak", "weak": "Weak", @@ -71,6 +98,7 @@ "text": "Text", "value": "Value", "save": "Save", + "saving": "Saving...", "editDelete": "Edit/Delete", "type": "Type", "noNotifications": "You don't have notifications yet", @@ -393,7 +421,11 @@ "globalError": "An error has occurred, sorry for the inconvenience.", "cameraPermissionDenied": "Camera permission denied. Please allow camera access in your browser settings.", "sendError": "Failed to send invitation. Please try again", - "missingVariableTitles": "Cannot save: Some variables are missing titles" + "missingVariableTitles": "Cannot save: Some variables are missing titles", + "failedToLoadAnswers": "Failed to load answers. Please try again.", + "failedToUpdateAnswer": "Failed to update answer. Please try again.", + "failedToRemoveCooperator": "Failed to remove cooperator. Please try again.", + "failedToSaveAnswerCooperator": "Failed to save cooperator answer" }, "Introduction": { "title": "UX Remote LAB", @@ -1189,6 +1221,44 @@ "note": "Your role is to facilitate and observe. The participant should complete tasks independently unless they specifically request help.", "startSession": "Start Moderated Session" }, + "VideoCallPanel": { + "toolsPanelTitle": "Tools Panel", + "sessionControl": "Session Control", + "joinRoomInfo": "Join room controls are now in the main interface above", + "proceedNextStep": "Proceed to Next Step", + "endCall": "End Call", + "activeCall": "Active call", + "participants": "Participants", + "you": "You", + "observator": "Observer", + "moderator": "Moderator", + "connected": "Connected", + "disconnected": "Disconnected", + "camera": "Camera", + "noCamera": "No camera", + "microphone": "Microphone", + "noMicrophone": "No microphone", + "settings": "Settings", + "disableCamera": "Turn off camera", + "enableCamera": "Turn on camera", + "muteMicrophone": "Mute microphone", + "unmuteMicrophone": "Unmute microphone", + "stopScreenShare": "Stop sharing screen", + "shareScreen": "Share screen", + "moderatorOnlySteps": "Only the moderator can change steps" + }, + "VideoCall": { + "screenSharingLabel": "Sharing screen", + "cameraOff": "Camera is off", + "yourVideo": "Your video", + "yourPreview": "Your preview", + "observatorMode": "Observer Mode", + "waitingForModeratorToStartSession": "Waiting for moderator to start the session...", + "observeAllFeedsNotice": "You will be able to observe all video feeds without sending your own.", + "waitingForParticipants": "Waiting for participants...", + "waitingForModerator": "Waiting for moderator...", + "autoStartWhenModeratorOpensRoom": "The video call will start automatically when the moderator opens the room." + }, "buttons": { "done": "Done" }, diff --git a/src/app/plugins/locales/es.json b/src/app/plugins/locales/es.json index ee9a68c34..7b8ad8099 100644 --- a/src/app/plugins/locales/es.json +++ b/src/app/plugins/locales/es.json @@ -43,7 +43,34 @@ "fair": "Regular", "good": "Buena", "strong": "Fuerte" - } + }, + "emailNotVerified": "Correo electrรณnico no verificado", + "verificationEmailSent": "ยกCorreo de verificaciรณn enviado exitosamente!", + "errorSendingVerification": "Error al enviar el correo de verificaciรณn", + "verifyEmailTitle": "Verifique su correo electrรณnico", + "verifyEmailSubtitle": "Hemos enviado un enlace de verificaciรณn a su correo electrรณnico", + "emailLabel": "Correo electrรณnico", + "verifyingEmail": "Verificando...", + "emailAlreadyVerified": "ยฟEl correo electrรณnico ya estรก verificado?", + "emailVerified": "ยกCorreo electrรณnico verificado exitosamente!", + "checkEmailTitle": "Verifique su correo electrรณnico", + "checkEmailStep1": "Revise su bandeja de entrada para recibir un correo de verificaciรณn", + "checkEmailStep2": "Haga clic en el enlace del correo para verificar su cuenta", + "checkEmailStep3": "Vuelva aquรญ y su cuenta serรก verificada", + "checkSpamFolder": "ยฟNo lo ve? Revise su carpeta de spam", + "resending": "Reenviando...", + "resendEmail": "Reenviar correo", + "changeEmail": "Cambiar correo electrรณnico", + "continueToDashboard": "Continuar al panel de control", + "signOut": "Cerrar sesiรณn", + "newEmail": "Nuevo correo electrรณnico", + "enterNewEmail": "Ingrese su nueva direcciรณn de correo electrรณnico", + "saving": "Guardando...", + "errorCheckingVerification": "Error al verificar el estado de verificaciรณn", + "errorResendingEmail": "Error al reenviar el correo de verificaciรณn", + "invalidEmail": "Direcciรณn de correo electrรณnico invรกlida", + "emailUpdatedVerification": "ยกCorreo actualizado! Hemos enviado un enlace de verificaciรณn a su nuevo correo.", + "errorUpdatingEmail": "Error al actualizar el correo electrรณnico" }, "common": { "enterTextHere": "Ingrese texto aquรญ...", @@ -112,6 +139,7 @@ "user": "Usuario", "id": "ID", "itemsPerPage": "Elementos por pรกgina:", + "saving": "Guardando...", "visible": "Visible" }, "storage": { @@ -384,7 +412,10 @@ "globalError": "Ha ocurrido un error, disculpa las molestias.", "cameraPermissionDenied": "Se requiere acceso a la cรกmara para continuar. Por favor, permita el acceso a la cรกmara en la configuraciรณn de su navegador.", "sendError": "No se pudo enviar la invitaciรณn. Por favor, intenta de nuevo.", - "missingVariableTitles": "No se puede guardar: Algunas variables no tienen tรญtulo" + "missingVariableTitles": "No se puede guardar: Algunas variables no tienen tรญtulo", + "failedToLoadAnswers": "Error al cargar respuestas", + "failedToUpdateAnswer": "Error al actualizar respuesta", + "failedToSaveAnswerCooperator": "Error al guardar respuesta del cooperador" }, "Introduction": { "title": "UX LAB Remoto", @@ -1194,6 +1225,44 @@ "note": "Su funciรณn es facilitar y observar. El participante debe completar las tareas de forma independiente a menos que solicite ayuda especรญficamente.", "startSession": "Iniciar Sesiรณn Moderada" }, + "VideoCallPanel": { + "toolsPanelTitle": "Panel de herramientas", + "sessionControl": "Control de sesion", + "joinRoomInfo": "Los controles para unirse estan ahora en la interfaz principal superior", + "proceedNextStep": "Continuar al siguiente paso", + "endCall": "Finalizar llamada", + "activeCall": "Llamada activa", + "participants": "Participantes", + "you": "Tu", + "observator": "Observador", + "moderator": "Moderador", + "connected": "Conectado", + "disconnected": "Desconectado", + "camera": "Camara", + "noCamera": "Sin camara", + "microphone": "Microfono", + "noMicrophone": "Sin microfono", + "settings": "Configuracion", + "disableCamera": "Desactivar camara", + "enableCamera": "Activar camara", + "muteMicrophone": "Silenciar microfono", + "unmuteMicrophone": "Activar microfono", + "stopScreenShare": "Detener compartir pantalla", + "shareScreen": "Compartir pantalla", + "moderatorOnlySteps": "Solo el moderador puede cambiar los pasos" + }, + "VideoCall": { + "screenSharingLabel": "Compartiendo pantalla", + "cameraOff": "La camara esta apagada", + "yourVideo": "Tu video", + "yourPreview": "Tu vista previa", + "observatorMode": "Modo observador", + "waitingForModeratorToStartSession": "Esperando a que el moderador inicie la sesion...", + "observeAllFeedsNotice": "Podras observar todas las transmisiones de video sin enviar la tuya.", + "waitingForParticipants": "Esperando participantes...", + "waitingForModerator": "Esperando al moderador...", + "autoStartWhenModeratorOpensRoom": "La videollamada comenzara automaticamente cuando el moderador abra la sala." + }, "WelcomeStep": { "welcome": "ยกBienvenido a RUXAILAB!", "description": "Vas a participar en una prueba de usuario que tiene como objetivo evaluar la facilidad de uso y comprensiรณn de una aplicaciรณn digital. Este tipo de prueba nos ayuda a detectar posibles barreras tecnolรณgicas y mejorar la experiencia para todos.", diff --git a/src/app/plugins/locales/fr.json b/src/app/plugins/locales/fr.json index 0b26d531f..de43e95c0 100644 --- a/src/app/plugins/locales/fr.json +++ b/src/app/plugins/locales/fr.json @@ -43,7 +43,34 @@ "fair": "Moyen", "good": "Bon", "strong": "Fort" - } + }, + "emailNotVerified": "E-mail non vรฉrifiรฉ", + "verificationEmailSent": "E-mail de vรฉrification envoyรฉ avec succรจs!", + "errorSendingVerification": "Erreur lors de l'envoi de l'e-mail de vรฉrification", + "verifyEmailTitle": "Vรฉrifiez votre e-mail", + "verifyEmailSubtitle": "Nous avons envoyรฉ un lien de vรฉrification ร  votre e-mail", + "emailLabel": "E-mail", + "verifyingEmail": "Vรฉrification...", + "emailAlreadyVerified": "E-mail dรฉjร  vรฉrifiรฉ ?", + "emailVerified": "E-mail vรฉrifiรฉ avec succรจs!", + "checkEmailTitle": "Vรฉrifiez votre e-mail", + "checkEmailStep1": "Vรฉrifiez votre boรฎte de rรฉception pour un e-mail de vรฉrification", + "checkEmailStep2": "Cliquez sur le lien dans l'e-mail pour vรฉrifier votre compte", + "checkEmailStep3": "Revenez ici et votre compte sera vรฉrifiรฉ", + "checkSpamFolder": "Vous ne le voyez pas? Vรฉrifiez votre dossier de spam", + "resending": "Renvoi...", + "resendEmail": "Renvoyer l'e-mail", + "changeEmail": "Changer l'e-mail", + "continueToDashboard": "Continuer vers le tableau de bord", + "signOut": "Dรฉconnexion", + "newEmail": "Nouvel e-mail", + "enterNewEmail": "Entrez votre nouvelle adresse e-mail", + "saving": "Enregistrement...", + "errorCheckingVerification": "Erreur lors de la vรฉrification du statut de vรฉrification", + "errorResendingEmail": "Erreur lors du renvoi de l'e-mail de vรฉrification", + "invalidEmail": "Adresse e-mail invalide", + "emailUpdatedVerification": "E-mail mis ร  jour! Nous avons envoyรฉ un lien de vรฉrification ร  votre nouvel e-mail.", + "errorUpdatingEmail": "Erreur lors de la mise ร  jour de l'e-mail" }, "common": { "enterTextHere": "Entrez le texte ici...", @@ -112,6 +139,7 @@ "user": "Utilisateur", "id": "ID", "itemsPerPage": "ร‰lรฉments par page :", + "saving": "Enregistrement...", "visible": "Visible" }, "storage": { @@ -384,7 +412,10 @@ "globalError": "Une erreur est survenue, dรฉsolรฉ pour le dรฉsagrรฉment.", "cameraPermissionDenied": "L'accรจs ร  la camรฉra est requis pour continuer. Veuillez autoriser l'accรจs ร  la camรฉra dans les paramรจtres de votre navigateur.", "sendError": "ร‰chec de l'envoi de l'invitation. Veuillez rรฉessayer", - "missingVariableTitles": "Impossible d'enregistrer : Certaines variables manquent de titres" + "missingVariableTitles": "Impossible d'enregistrer : Certaines variables manquent de titres", + "failedToLoadAnswers": "ร‰chec du chargement des rรฉponses", + "failedToUpdateAnswer": "ร‰chec de la mise ร  jour de la rรฉponse", + "failedToSaveAnswerCooperator": "ร‰chec de l'enregistrement de la rรฉponse du coopรฉrateur" }, "Introduction": { "title": "UX Remote LAB", @@ -1194,6 +1225,44 @@ "note": "Votre rรดle est de faciliter et d'observer. Le participant doit effectuer les tรขches de maniรจre indรฉpendante, sauf s'il demande spรฉcifiquement de l'aide.", "startSession": "Dรฉmarrer la session modรฉrรฉe" }, + "VideoCallPanel": { + "toolsPanelTitle": "Panneau d'outils", + "sessionControl": "Controle de session", + "joinRoomInfo": "Les controles pour rejoindre la salle sont maintenant dans l'interface principale ci-dessus", + "proceedNextStep": "Passer a l'etape suivante", + "endCall": "Terminer l'appel", + "activeCall": "Appel actif", + "participants": "Participants", + "you": "Vous", + "observator": "Observateur", + "moderator": "Moderateur", + "connected": "Connecte", + "disconnected": "Deconnecte", + "camera": "Camera", + "noCamera": "Sans camera", + "microphone": "Microphone", + "noMicrophone": "Sans microphone", + "settings": "Parametres", + "disableCamera": "Desactiver la camera", + "enableCamera": "Activer la camera", + "muteMicrophone": "Couper le microphone", + "unmuteMicrophone": "Activer le microphone", + "stopScreenShare": "Arreter le partage d'ecran", + "shareScreen": "Partager l'ecran", + "moderatorOnlySteps": "Seul le moderateur peut changer les etapes" + }, + "VideoCall": { + "screenSharingLabel": "Partage d'ecran", + "cameraOff": "La camera est desactivee", + "yourVideo": "Votre video", + "yourPreview": "Votre apercu", + "observatorMode": "Mode observateur", + "waitingForModeratorToStartSession": "En attente du moderateur pour demarrer la session...", + "observeAllFeedsNotice": "Vous pourrez observer tous les flux video sans envoyer le votre.", + "waitingForParticipants": "En attente des participants...", + "waitingForModerator": "En attente du moderateur...", + "autoStartWhenModeratorOpensRoom": "L'appel video demarrera automatiquement lorsque le moderateur ouvrira la salle." + }, "WelcomeStep": { "welcome": "Bienvenue sur RUXAILAB !", "description": "Vous allez participer ร  un test utilisateur qui vise ร  รฉvaluer la facilitรฉ d'utilisation et la comprรฉhension d'une application numรฉrique. Ce type de test nous aide ร  dรฉtecter les barriรจres technologiques possibles et ร  amรฉliorer l'expรฉrience pour tout le monde.", diff --git a/src/app/plugins/locales/hi.json b/src/app/plugins/locales/hi.json index ebbdecd72..f185e2b99 100644 --- a/src/app/plugins/locales/hi.json +++ b/src/app/plugins/locales/hi.json @@ -43,7 +43,34 @@ "fair": "เค เฅ€เค•", "good": "เค…เคšเฅเค›เคพ", "strong": "เคฎเคœเคฌเฅ‚เคค" - } + }, + "emailNotVerified": "เคˆเคฎเฅ‡เคฒ เคธเคคเฅเคฏเคพเคชเคฟเคค เคจเคนเฅ€เค‚ เคนเฅˆ", + "verificationEmailSent": "เคธเคคเฅเคฏเคพเคชเคจ เคˆเคฎเฅ‡เคฒ เคธเคซเคฒเคคเคพเคชเฅ‚เคฐเฅเคตเค• เคญเฅ‡เคœเคพ เค—เคฏเคพ!", + "errorSendingVerification": "เคธเคคเฅเคฏเคพเคชเคจ เคˆเคฎเฅ‡เคฒ เคญเฅ‡เคœเคจเฅ‡ เคฎเฅ‡เค‚ เคคเฅเคฐเฅเคŸเคฟ", + "verifyEmailTitle": "เค…เคชเคจเฅ€ เคˆเคฎเฅ‡เคฒ เคธเคคเฅเคฏเคพเคชเคฟเคค เค•เคฐเฅ‡เค‚", + "verifyEmailSubtitle": "เคนเคฎเคจเฅ‡ เค†เคชเค•เฅ€ เคˆเคฎเฅ‡เคฒ เคชเคฐ เคเค• เคธเคคเฅเคฏเคพเคชเคจ เคฒเคฟเค‚เค• เคญเฅ‡เคœเคพ เคนเฅˆ", + "emailLabel": "เคˆเคฎเฅ‡เคฒ", + "verifyingEmail": "เคธเคคเฅเคฏเคพเคชเคจ เคœเคพเคฐเฅ€...", + "emailAlreadyVerified": "เค•เฅเคฏเคพ เคˆเคฎเฅ‡เคฒ เคชเคนเคฒเฅ‡ เคธเฅ‡ เคธเคคเฅเคฏเคพเคชเคฟเคค เคนเฅˆ?", + "emailVerified": "เคˆเคฎเฅ‡เคฒ เคธเคซเคฒเคคเคพเคชเฅ‚เคฐเฅเคตเค• เคธเคคเฅเคฏเคพเคชเคฟเคค!", + "checkEmailTitle": "เค…เคชเคจเฅ€ เคˆเคฎเฅ‡เคฒ เคœเคพเค‚เคšเฅ‡เค‚", + "checkEmailStep1": "เคธเคคเฅเคฏเคพเคชเคจ เคˆเคฎเฅ‡เคฒ เค•เฅ‡ เคฒเคฟเค เค…เคชเคจเฅ‡ เค‡เคจเคฌเฅ‰เค•เฅเคธ เค•เฅ€ เคœเคพเค‚เคš เค•เคฐเฅ‡เค‚", + "checkEmailStep2": "เค…เคชเคจเฅ‡ เค–เคพเคคเฅ‡ เค•เฅ‹ เคธเคคเฅเคฏเคพเคชเคฟเคค เค•เคฐเคจเฅ‡ เค•เฅ‡ เคฒเคฟเค เคˆเคฎเฅ‡เคฒ เคฎเฅ‡เค‚ เคฒเคฟเค‚เค• เคชเคฐ เค•เฅเคฒเคฟเค• เค•เคฐเฅ‡เค‚", + "checkEmailStep3": "เคฏเคนเคพเค‚ เคตเคพเคชเคธ เค†เคเค‚ เค”เคฐ เค†เคชเค•เคพ เค–เคพเคคเคพ เคธเคคเฅเคฏเคพเคชเคฟเคค เคนเฅ‹ เคœเคพเคเค—เคพ", + "checkSpamFolder": "เคจเคนเฅ€เค‚ เคฆเฅ‡เค– เคฐเคนเฅ‡? เค…เคชเคจเฅ‡ เคธเฅเคชเฅˆเคฎ เคซเฅ‹เคฒเฅเคกเคฐ เค•เฅ€ เคœเคพเค‚เคš เค•เคฐเฅ‡เค‚", + "resending": "เคชเฅเคจเคƒ เคญเฅ‡เคœ เคฐเคนเฅ‡ เคนเฅˆเค‚...", + "resendEmail": "เคˆเคฎเฅ‡เคฒ เคซเคฟเคฐ เคธเฅ‡ เคญเฅ‡เคœเฅ‡เค‚", + "changeEmail": "เคˆเคฎเฅ‡เคฒ เคฌเคฆเคฒเฅ‡เค‚", + "continueToDashboard": "เคกเฅˆเคถเคฌเฅ‹เคฐเฅเคก เคชเคฐ เคœเคพเคฐเฅ€ เคฐเค–เฅ‡เค‚", + "signOut": "เคธเคพเค‡เคจ เค†เค‰เคŸ", + "newEmail": "เคจเคˆ เคˆเคฎเฅ‡เคฒ", + "enterNewEmail": "เค…เคชเคจเคพ เคจเคฏเคพ เคˆเคฎเฅ‡เคฒ เคชเคคเคพ เคฆเคฐเฅเคœ เค•เคฐเฅ‡เค‚", + "saving": "เคธเคนเฅ‡เคœเคพ เคœเคพ เคฐเคนเคพ เคนเฅˆ...", + "errorCheckingVerification": "เคธเคคเฅเคฏเคพเคชเคจ เคธเฅเคฅเคฟเคคเคฟ เคœเคพเค‚เคšเคจเฅ‡ เคฎเฅ‡เค‚ เคคเฅเคฐเฅเคŸเคฟ", + "errorResendingEmail": "เคธเคคเฅเคฏเคพเคชเคจ เคˆเคฎเฅ‡เคฒ เคชเฅเคจเคƒ เคญเฅ‡เคœเคจเฅ‡ เคฎเฅ‡เค‚ เคคเฅเคฐเฅเคŸเคฟ", + "invalidEmail": "เค…เคฎเคพเคจเฅเคฏ เคˆเคฎเฅ‡เคฒ เคชเคคเคพ", + "emailUpdatedVerification": "เคˆเคฎเฅ‡เคฒ เค…เคชเคกเฅ‡เคŸ เค•เคฟเคฏเคพ เค—เคฏเคพ! เคนเคฎเคจเฅ‡ เค†เคชเค•เฅ€ เคจเคˆ เคˆเคฎเฅ‡เคฒ เคชเคฐ เคเค• เคธเคคเฅเคฏเคพเคชเคจ เคฒเคฟเค‚เค• เคญเฅ‡เคœเคพ เคนเฅˆเฅค", + "errorUpdatingEmail": "เคˆเคฎเฅ‡เคฒ เค…เคชเคกเฅ‡เคŸ เค•เคฐเคจเฅ‡ เคฎเฅ‡เค‚ เคคเฅเคฐเฅเคŸเคฟ" }, "common": { "enterTextHere": "เคฏเคนเคพเค เคŸเฅ‡เค•เฅเคธเฅเคŸ เคฆเคฐเฅเคœ เค•เคฐเฅ‡เค‚...", @@ -112,6 +139,7 @@ "user": "เค‰เคชเคฏเฅ‹เค—เค•เคฐเฅเคคเคพ", "id": "เค†เคˆเคกเฅ€", "itemsPerPage": "เคชเฅเคฐเคคเคฟ เคชเฅƒเคทเฅเค  เค†เค‡เคŸเคฎ:", + "saving": "เคธเคนเฅ‡เคœเคพ เคœเคพ เคฐเคนเคพ เคนเฅˆ...", "visible": "เคฆเฅƒเคถเฅเคฏ" }, "storage": { @@ -384,7 +412,10 @@ "globalError": "เคเค• เคคเฅเคฐเฅเคŸเคฟ เคนเฅเคˆ เคนเฅˆ, เค…เคธเฅเคตเคฟเคงเคพ เค•เฅ‡ เคฒเคฟเค เค–เฅ‡เคฆ เคนเฅˆเฅค", "cameraPermissionDenied": "เค†เค—เฅ‡ เคฌเคขเคผเคจเฅ‡ เค•เฅ‡ เคฒเคฟเค เค•เฅˆเคฎเคฐเคพ เคเค•เฅเคธเฅ‡เคธ เค†เคตเคถเฅเคฏเค• เคนเฅˆเฅค เค•เฅƒเคชเคฏเคพ เค…เคชเคจเฅ‡ เคฌเฅเคฐเคพเค‰เคœเคผเคฐ เค•เฅ€ เคธเฅ‡เคŸเคฟเค‚เค—เฅเคธ เคฎเฅ‡เค‚ เค•เฅˆเคฎเคฐเคพ เคเค•เฅเคธเฅ‡เคธ เค•เฅ€ เค…เคจเฅเคฎเคคเคฟ เคฆเฅ‡เค‚เฅค", "sendError": "เค†เคฎเค‚เคคเฅเคฐเคฃ เคญเฅ‡เคœเคจเฅ‡ เคฎเฅ‡เค‚ เค…เคธเคฎเคฐเฅเคฅเฅค เค•เฅƒเคชเคฏเคพ เคชเฅเคจเคƒ เคชเฅเคฐเคฏเคพเคธ เค•เคฐเฅ‡เค‚เฅค", - "missingVariableTitles": "เคธเคนเฅ‡เคœเคพ เคจเคนเฅ€เค‚ เคœเคพ เคธเค•เคพ: เค•เฅเค› เคตเฅ‡เคฐเคฟเคเคฌเคฒเฅเคธ เค•เฅ‡ เคถเฅ€เคฐเฅเคทเค• เค—เคพเคฏเคฌ เคนเฅˆเค‚" + "missingVariableTitles": "เคธเคนเฅ‡เคœเคพ เคจเคนเฅ€เค‚ เคœเคพ เคธเค•เคพ: เค•เฅเค› เคตเฅ‡เคฐเคฟเคเคฌเคฒเฅเคธ เค•เฅ‡ เคถเฅ€เคฐเฅเคทเค• เค—เคพเคฏเคฌ เคนเฅˆเค‚", + "failedToLoadAnswers": "Failed to load answers", + "failedToUpdateAnswer": "Failed to update answer", + "failedToSaveAnswerCooperator": "Failed to save cooperator answer" }, "Introduction": { "title": "UX เคฐเคฟเคฎเฅ‹เคŸ เคฒเฅˆเคฌ", @@ -1194,6 +1225,44 @@ "note": "เค†เคชเค•เฅ€ เคญเฅ‚เคฎเคฟเค•เคพ เคธเฅเคตเคฟเคงเคพเคœเคจเค• เคฌเคจเคพเคจเคพ เค”เคฐ เค…เคตเคฒเฅ‹เค•เคจ เค•เคฐเคจเคพ เคนเฅˆเฅค เคชเฅเคฐเคคเคฟเคญเคพเค—เฅ€ เค•เฅ‹ เคธเฅเคตเคคเค‚เคคเฅเคฐ เคฐเฅ‚เคช เคธเฅ‡ เค•เคพเคฐเฅเคฏ เคชเฅ‚เคฐเคพ เค•เคฐเคจเคพ เคšเคพเคนเคฟเค เคœเคฌ เคคเค• เค•เคฟ เคตเฅ‡ เคตเคฟเคถเฅ‡เคท เคฐเฅ‚เคช เคธเฅ‡ เคฎเคฆเคฆ เค•เคพ เค…เคจเฅเคฐเฅ‹เคง เคจ เค•เคฐเฅ‡เค‚เฅค", "startSession": "เคฎเฅ‰เคกเคฐเฅ‡เคŸเฅ‡เคก เคธเฅ‡เคถเคจ เคถเฅเคฐเฅ‚ เค•เคฐเฅ‡เค‚" }, + "VideoCallPanel": { + "toolsPanelTitle": "เคŸเฅ‚เคฒเฅเคธ เคชเฅˆเคจเคฒ", + "sessionControl": "เคธเฅ‡เคถเคจ เคจเคฟเคฏเค‚เคคเฅเคฐเคฃ", + "joinRoomInfo": "เคœเฅ‰เค‡เคจ เคฐเฅ‚เคฎ เคจเคฟเคฏเค‚เคคเฅเคฐเคฃ เค…เคฌ เคŠเคชเคฐ เคฎเฅเค–เฅเคฏ เค‡เค‚เคŸเคฐเคซเฅ‡เคธ เคฎเฅ‡เค‚ เคนเฅˆเค‚", + "proceedNextStep": "เค…เค—เคฒเฅ‡ เคšเคฐเคฃ เคชเคฐ เคœเคพเคเค‚", + "endCall": "เค•เฅ‰เคฒ เคธเคฎเคพเคชเฅเคค เค•เคฐเฅ‡เค‚", + "activeCall": "เค•เฅ‰เคฒ เคธเค•เฅเคฐเคฟเคฏ เคนเฅˆ", + "participants": "เคชเฅเคฐเคคเคฟเคญเคพเค—เฅ€", + "you": "เค†เคช", + "observator": "เคชเคฐเฅเคฏเคตเฅ‡เค•เฅเคทเค•", + "moderator": "เคฎเฅ‰เคกเคฐเฅ‡เคŸเคฐ", + "connected": "เค•เคจเฅ‡เค•เฅเคŸเฅ‡เคก", + "disconnected": "เคกเคฟเคธเฅเค•เคจเฅ‡เค•เฅเคŸเฅ‡เคก", + "camera": "เค•เฅˆเคฎเคฐเคพ", + "noCamera": "เค•เฅˆเคฎเคฐเคพ เคจเคนเฅ€เค‚", + "microphone": "เคฎเคพเค‡เค•เฅเคฐเฅ‹เคซเฅ‹เคจ", + "noMicrophone": "เคฎเคพเค‡เค•เฅเคฐเฅ‹เคซเฅ‹เคจ เคจเคนเฅ€เค‚", + "settings": "เคธเฅ‡เคŸเคฟเค‚เค—เฅเคธ", + "disableCamera": "เค•เฅˆเคฎเคฐเคพ เคฌเค‚เคฆ เค•เคฐเฅ‡เค‚", + "enableCamera": "เค•เฅˆเคฎเคฐเคพ เคšเคพเคฒเฅ‚ เค•เคฐเฅ‡เค‚", + "muteMicrophone": "เคฎเคพเค‡เค•เฅเคฐเฅ‹เคซเฅ‹เคจ เคฎเฅเคฏเฅ‚เคŸ เค•เคฐเฅ‡เค‚", + "unmuteMicrophone": "เคฎเคพเค‡เค•เฅเคฐเฅ‹เคซเฅ‹เคจ เค…เคจเคฎเฅเคฏเฅ‚เคŸ เค•เคฐเฅ‡เค‚", + "stopScreenShare": "เคธเฅเค•เฅเคฐเฅ€เคจ เคถเฅ‡เคฏเคฐ เคฌเค‚เคฆ เค•เคฐเฅ‡เค‚", + "shareScreen": "เคธเฅเค•เฅเคฐเฅ€เคจ เคถเฅ‡เคฏเคฐ เค•เคฐเฅ‡เค‚", + "moderatorOnlySteps": "เค•เฅ‡เคตเคฒ เคฎเฅ‰เคกเคฐเฅ‡เคŸเคฐ เคšเคฐเคฃ เคฌเคฆเคฒ เคธเค•เคคเคพ เคนเฅˆ" + }, + "VideoCall": { + "screenSharingLabel": "เคธเฅเค•เฅเคฐเฅ€เคจ เคธเคพเคเคพ เค•เฅ€ เคœเคพ เคฐเคนเฅ€ เคนเฅˆ", + "cameraOff": "เค•เฅˆเคฎเคฐเคพ เคฌเค‚เคฆ เคนเฅˆ", + "yourVideo": "เค†เคชเค•เคพ เคตเฅ€เคกเคฟเคฏเฅ‹", + "yourPreview": "เค†เคชเค•เคพ เคชเฅเคฐเฅ€เคตเฅเคฏเฅ‚", + "observatorMode": "เค‘เคฌเฅเคœเคฐเฅเคตเคฐ เคฎเฅ‹เคก", + "waitingForModeratorToStartSession": "เคฎเฅ‰เคกเคฐเฅ‡เคŸเคฐ เค•เฅ‡ เคธเฅ‡เคถเคจ เคถเฅเคฐเฅ‚ เค•เคฐเคจเฅ‡ เค•เฅ€ เคชเฅเคฐเคคเฅ€เค•เฅเคทเคพ เคนเฅ‹ เคฐเคนเฅ€ เคนเฅˆ...", + "observeAllFeedsNotice": "เค†เคช เค…เคชเคจเคพ เคตเฅ€เคกเคฟเคฏเฅ‹ เคญเฅ‡เคœเฅ‡ เคฌเคฟเคจเคพ เคธเคญเฅ€ เคตเฅ€เคกเคฟเคฏเฅ‹ เคซเฅ€เคก เคฆเฅ‡เค– เคธเค•เฅ‡เค‚เค—เฅ‡เฅค", + "waitingForParticipants": "เคชเฅเคฐเคคเคฟเคญเคพเค—เคฟเคฏเฅ‹เค‚ เค•เฅ€ เคชเฅเคฐเคคเฅ€เค•เฅเคทเคพ เคนเฅ‹ เคฐเคนเฅ€ เคนเฅˆ...", + "waitingForModerator": "เคฎเฅ‰เคกเคฐเฅ‡เคŸเคฐ เค•เฅ€ เคชเฅเคฐเคคเฅ€เค•เฅเคทเคพ เคนเฅ‹ เคฐเคนเฅ€ เคนเฅˆ...", + "autoStartWhenModeratorOpensRoom": "เคœเคฌ เคฎเฅ‰เคกเคฐเฅ‡เคŸเคฐ เคฐเฅ‚เคฎ เค–เฅ‹เคฒเฅ‡เค—เคพ, เคตเฅ€เคกเคฟเคฏเฅ‹ เค•เฅ‰เคฒ เค…เคชเคจเฅ‡ เค†เคช เคถเฅเคฐเฅ‚ เคนเฅ‹ เคœเคพเคเค—เฅ€เฅค" + }, "WelcomeStep": { "welcome": "RUXAILAB เคฎเฅ‡เค‚ เค†เคชเค•เคพ เคธเฅเคตเคพเค—เคค เคนเฅˆ!", "description": "เค†เคช เคเค• เค‰เคชเคฏเฅ‹เค—เค•เคฐเฅเคคเคพ เคชเคฐเฅ€เค•เฅเคทเคฃ เคฎเฅ‡เค‚ เคญเคพเค— เคฒเฅ‡เค‚เค—เฅ‡ เคœเฅ‹ เค•เคฟเคธเฅ€ เคกเคฟเคœเคฟเคŸเคฒ เคเคชเฅเคฒเคฟเค•เฅ‡เคถเคจ เค•เฅ€ เค‰เคชเคฏเฅ‹เค—เคฟเคคเคพ เค”เคฐ เคธเคฎเคเคจเฅ‡ เค•เฅ€ เค•เฅเคทเคฎเคคเคพ เค•เคพ เคฎเฅ‚เคฒเฅเคฏเคพเค‚เค•เคจ เค•เคฐเคคเคพ เคนเฅˆเฅค เคฏเคน เคชเคฐเฅ€เค•เฅเคทเคฃ เคธเค‚เคญเคพเคตเคฟเคค เคคเค•เคจเฅ€เค•เฅ€ เคฌเคพเคงเคพเค“เค‚ เค•เคพ เคชเคคเคพ เคฒเค—เคพเคจเฅ‡ เค”เคฐ เค…เคจเฅเคญเคต เค•เฅ‹ เคฌเฅ‡เคนเคคเคฐ เคฌเคจเคพเคจเฅ‡ เคฎเฅ‡เค‚ เคฎเคฆเคฆ เค•เคฐเคคเคพ เคนเฅˆเฅค", diff --git a/src/app/plugins/locales/ja.json b/src/app/plugins/locales/ja.json index b134de620..b29d525b9 100644 --- a/src/app/plugins/locales/ja.json +++ b/src/app/plugins/locales/ja.json @@ -43,7 +43,34 @@ "fair": "ๆ™ฎ้€š", "good": "่‰ฏใ„", "strong": "ๅผทใ„" - } + }, + "emailNotVerified": "ใƒกใƒผใƒซใŒ็ขบ่ชใ•ใ‚Œใฆใ„ใพใ›ใ‚“", + "verificationEmailSent": "็ขบ่ชใƒกใƒผใƒซใŒๆญฃๅธธใซ้€ไฟกใ•ใ‚Œใพใ—ใŸ๏ผ", + "errorSendingVerification": "็ขบ่ชใƒกใƒผใƒซ้€ไฟกใ‚จใƒฉใƒผ", + "verifyEmailTitle": "ใƒกใƒผใƒซใ‚’็ขบ่ชใ—ใฆใใ ใ•ใ„", + "verifyEmailSubtitle": "็ขบ่ชใƒชใƒณใ‚ฏใ‚’ใƒกใƒผใƒซใซ้€ไฟกใ—ใพใ—ใŸ", + "emailLabel": "ใƒกใƒผใƒซ", + "verifyingEmail": "็ขบ่ชไธญ...", + "emailAlreadyVerified": "ใƒกใƒผใƒซใฏใ™ใงใซ็ขบ่ชใ•ใ‚Œใฆใ„ใพใ™ใ‹๏ผŸ", + "emailVerified": "ใƒกใƒผใƒซใŒๆญฃๅธธใซ็ขบ่ชใ•ใ‚Œใพใ—ใŸ๏ผ", + "checkEmailTitle": "ใƒกใƒผใƒซใ‚’ใ”็ขบ่ชใใ ใ•ใ„", + "checkEmailStep1": "็ขบ่ชใƒกใƒผใƒซใ‚’ๅ—ใ‘ๅ–ใ‚‹ใŸใ‚ใซใ‚คใƒณใƒœใƒƒใ‚ฏใ‚นใ‚’ใƒใ‚งใƒƒใ‚ฏ", + "checkEmailStep2": "ใƒกใƒผใƒซๅ†…ใฎใƒชใƒณใ‚ฏใ‚’ใ‚ฏใƒชใƒƒใ‚ฏใ—ใฆใ€ใ‚ขใ‚ซใ‚ฆใƒณใƒˆใ‚’็ขบ่ช", + "checkEmailStep3": "ใ“ใ“ใซๆˆปใ‚‹ใจใ‚ขใ‚ซใ‚ฆใƒณใƒˆใŒ็ขบ่ชใ•ใ‚Œใพใ™", + "checkSpamFolder": "่ฆ‹ๅฝ“ใŸใ‚Šใพใ›ใ‚“ใ‹๏ผŸ ใ‚นใƒ‘ใƒ ใƒ•ใ‚ฉใƒซใƒ€ใ‚’็ขบ่ช", + "resending": "ๅ†้€ไฟกไธญ...", + "resendEmail": "ใƒกใƒผใƒซใ‚’ๅ†้€ไฟก", + "changeEmail": "ใƒกใƒผใƒซใ‚ขใƒ‰ใƒฌใ‚นใ‚’ๅค‰ๆ›ด", + "continueToDashboard": "ใƒ€ใƒƒใ‚ทใƒฅใƒœใƒผใƒ‰ใซ้€ฒใ‚€", + "signOut": "ใ‚ตใ‚คใƒณใ‚ขใ‚ฆใƒˆ", + "newEmail": "ๆ–ฐใ—ใ„ใƒกใƒผใƒซ", + "enterNewEmail": "ๆ–ฐใ—ใ„ใƒกใƒผใƒซใ‚ขใƒ‰ใƒฌใ‚นใ‚’ๅ…ฅๅŠ›ใ—ใฆใใ ใ•ใ„", + "saving": "ไฟๅญ˜ไธญ...", + "errorCheckingVerification": "็ขบ่ช็Šถๆ…‹ใฎ็ขบ่ชใ‚จใƒฉใƒผ", + "errorResendingEmail": "็ขบ่ชใƒกใƒผใƒซๅ†้€ไฟกใ‚จใƒฉใƒผ", + "invalidEmail": "็„กๅŠนใชใƒกใƒผใƒซใ‚ขใƒ‰ใƒฌใ‚น", + "emailUpdatedVerification": "ใƒกใƒผใƒซใŒๆ›ดๆ–ฐใ•ใ‚Œใพใ—ใŸ๏ผๆ–ฐใ—ใ„ใƒกใƒผใƒซใ‚ขใƒ‰ใƒฌใ‚นใซ็ขบ่ชใƒชใƒณใ‚ฏใ‚’้€ไฟกใ—ใพใ—ใŸใ€‚", + "errorUpdatingEmail": "ใƒกใƒผใƒซๆ›ดๆ–ฐใ‚จใƒฉใƒผ" }, "common": { "enterTextHere": "ใ“ใ“ใซใƒ†ใ‚ญใ‚นใƒˆใ‚’ๅ…ฅๅŠ›...", @@ -112,6 +139,7 @@ "user": "ใƒฆใƒผใ‚ถใƒผ", "id": "ID", "itemsPerPage": "ใƒšใƒผใ‚ธใ”ใจใฎ้ …็›ฎๆ•ฐ๏ผš", + "saving": "ไฟๅญ˜ไธญ...", "visible": "่กจ็คบ" }, "storage": { @@ -384,7 +412,10 @@ "globalError": "ใ‚จใƒฉใƒผใŒ็™บ็”Ÿใ—ใพใ—ใŸใ€‚ใ”ไธไพฟใ‚’ใŠใ‹ใ‘ใ—ใฆ็”ณใ—่จณใ‚ใ‚Šใพใ›ใ‚“ใ€‚", "cameraPermissionDenied": "ใ‚ซใƒกใƒฉใธใฎใ‚ขใ‚ฏใ‚ปใ‚นใŒๆ‹’ๅฆใ•ใ‚Œใพใ—ใŸใ€‚ใƒ–ใƒฉใ‚ฆใ‚ถใฎ่จญๅฎšใงใ‚ซใƒกใƒฉใธใฎใ‚ขใ‚ฏใ‚ปใ‚นใ‚’่จฑๅฏใ—ใฆใใ ใ•ใ„ใ€‚", "sendError": "ๆ‹›ๅพ…ใฎ้€ไฟกใซๅคฑๆ•—ใ—ใพใ—ใŸใ€‚ๅ†่ฉฆ่กŒใ—ใฆใใ ใ•ใ„", - "missingVariableTitles": "ไฟๅญ˜ใงใใพใ›ใ‚“๏ผšใ„ใใคใ‹ใฎๅค‰ๆ•ฐใซใ‚ฟใ‚คใƒˆใƒซใŒใ‚ใ‚Šใพใ›ใ‚“" + "missingVariableTitles": "ไฟๅญ˜ใงใใพใ›ใ‚“๏ผšใ„ใใคใ‹ใฎๅค‰ๆ•ฐใซใ‚ฟใ‚คใƒˆใƒซใŒใ‚ใ‚Šใพใ›ใ‚“", + "failedToLoadAnswers": "Failed to load answers", + "failedToUpdateAnswer": "Failed to update answer", + "failedToSaveAnswerCooperator": "Failed to save cooperator answer" }, "Introduction": { "title": "UXใƒชใƒขใƒผใƒˆใƒฉใƒœ", @@ -1194,6 +1225,44 @@ "note": "ใ‚ใชใŸใฎๅฝนๅ‰ฒใฏ้€ฒ่กŒใฎใ‚ตใƒใƒผใƒˆใจ่ฆณๅฏŸใงใ™ใ€‚ๅ‚ๅŠ ่€…ใŒ็‰นใซๅŠฉใ‘ใ‚’ๆฑ‚ใ‚ใชใ„้™ใ‚Šใ€ใ‚ฟใ‚นใ‚ฏใฏๅ‚ๅŠ ่€…่‡ช่บซใงๅฎŒไบ†ใ•ใ›ใฆใใ ใ•ใ„ใ€‚", "startSession": "ใƒขใƒ‡ใƒฌใƒผใƒˆใ‚ปใƒƒใ‚ทใƒงใƒณใ‚’้–‹ๅง‹" }, + "VideoCallPanel": { + "toolsPanelTitle": "ใƒ„ใƒผใƒซใƒ‘ใƒใƒซ", + "sessionControl": "ใ‚ปใƒƒใ‚ทใƒงใƒณ็ฎก็†", + "joinRoomInfo": "ใƒซใƒผใƒ ๅ‚ๅŠ ใฎๆ“ไฝœใฏไธŠ้ƒจใฎใƒกใ‚คใƒณ็”ป้ขใซ็งปๅ‹•ใ—ใพใ—ใŸ", + "proceedNextStep": "ๆฌกใฎใ‚นใƒ†ใƒƒใƒ—ใธ้€ฒใ‚€", + "endCall": "้€š่ฉฑใ‚’็ต‚ไบ†", + "activeCall": "้€š่ฉฑไธญ", + "participants": "ๅ‚ๅŠ ่€…", + "you": "ใ‚ใชใŸ", + "observator": "ใ‚ชใƒ–ใ‚ถใƒผใƒใƒผ", + "moderator": "ใƒขใƒ‡ใƒฌใƒผใ‚ฟใƒผ", + "connected": "ๆŽฅ็ถšไธญ", + "disconnected": "ๅˆ‡ๆ–ญ", + "camera": "ใ‚ซใƒกใƒฉ", + "noCamera": "ใ‚ซใƒกใƒฉใชใ—", + "microphone": "ใƒžใ‚คใ‚ฏ", + "noMicrophone": "ใƒžใ‚คใ‚ฏใชใ—", + "settings": "่จญๅฎš", + "disableCamera": "ใ‚ซใƒกใƒฉใ‚’ใ‚ชใƒ•", + "enableCamera": "ใ‚ซใƒกใƒฉใ‚’ใ‚ชใƒณ", + "muteMicrophone": "ใƒžใ‚คใ‚ฏใ‚’ใƒŸใƒฅใƒผใƒˆ", + "unmuteMicrophone": "ใƒžใ‚คใ‚ฏใฎใƒŸใƒฅใƒผใƒˆ่งฃ้™ค", + "stopScreenShare": "็”ป้ขๅ…ฑๆœ‰ใ‚’ๅœๆญข", + "shareScreen": "็”ป้ขใ‚’ๅ…ฑๆœ‰", + "moderatorOnlySteps": "ใ‚นใƒ†ใƒƒใƒ—ใ‚’ๅค‰ๆ›ดใงใใ‚‹ใฎใฏใƒขใƒ‡ใƒฌใƒผใ‚ฟใƒผใฎใฟใงใ™" + }, + "VideoCall": { + "screenSharingLabel": "็”ป้ขๅ…ฑๆœ‰ไธญ", + "cameraOff": "ใ‚ซใƒกใƒฉใฏใ‚ชใƒ•ใงใ™", + "yourVideo": "ใ‚ใชใŸใฎใƒ“ใƒ‡ใ‚ช", + "yourPreview": "ใ‚ใชใŸใฎใƒ—ใƒฌใƒ“ใƒฅใƒผ", + "observatorMode": "ใ‚ชใƒ–ใ‚ถใƒผใƒใƒผใƒขใƒผใƒ‰", + "waitingForModeratorToStartSession": "ใƒขใƒ‡ใƒฌใƒผใ‚ฟใƒผใŒใ‚ปใƒƒใ‚ทใƒงใƒณใ‚’้–‹ๅง‹ใ™ใ‚‹ใฎใ‚’ๅพ…ใฃใฆใ„ใพใ™...", + "observeAllFeedsNotice": "่‡ชๅˆ†ใฎๆ˜ ๅƒใ‚’้€ไฟกใ›ใšใซใ€ใ™ในใฆใฎใƒ“ใƒ‡ใ‚ชใƒ•ใ‚ฃใƒผใƒ‰ใ‚’่ฆณๅฏŸใงใใพใ™ใ€‚", + "waitingForParticipants": "ๅ‚ๅŠ ่€…ใ‚’ๅพ…ใฃใฆใ„ใพใ™...", + "waitingForModerator": "ใƒขใƒ‡ใƒฌใƒผใ‚ฟใƒผใ‚’ๅพ…ใฃใฆใ„ใพใ™...", + "autoStartWhenModeratorOpensRoom": "ใƒขใƒ‡ใƒฌใƒผใ‚ฟใƒผใŒใƒซใƒผใƒ ใ‚’้–‹ใใจใ€ใƒ“ใƒ‡ใ‚ช้€š่ฉฑใฏ่‡ชๅ‹•็š„ใซ้–‹ๅง‹ใ•ใ‚Œใพใ™ใ€‚" + }, "WelcomeStep": { "welcome": "RUXAILABใธใ‚ˆใ†ใ“ใ๏ผ", "description": "ใ‚ใชใŸใฏใƒ‡ใ‚ธใ‚ฟใƒซใ‚ขใƒ—ใƒชใ‚ฑใƒผใ‚ทใƒงใƒณใฎไฝฟใ„ใ‚„ใ™ใ•ใจ็†่งฃๅบฆใ‚’่ฉ•ไพกใ™ใ‚‹ใŸใ‚ใฎใƒฆใƒผใ‚ถใƒผใƒ†ใ‚นใƒˆใซๅ‚ๅŠ ใ—ใพใ™ใ€‚ใ“ใฎ็จฎใฎใƒ†ใ‚นใƒˆใฏใ€ๆŠ€่ก“็š„ใช้šœๅฃใ‚’ๆคœๅ‡บใ—ใ€ใ™ในใฆใฎไบบใฎใŸใ‚ใซไฝ“้จ“ใ‚’ๆ”นๅ–„ใ™ใ‚‹ใฎใซๅฝน็ซ‹ใกใพใ™ใ€‚", @@ -1584,11 +1653,7 @@ "SelfTest": { "title": "ใ‚ปใƒซใƒ•ใƒ†ใ‚นใƒˆ", "type": "้žใƒขใƒ‡ใƒฌใƒผใƒˆ", - "text": [ - "่‡ชๅˆ†ใฎๆ™‚้–“ใซๅ›ž็ญ”", - "้ซ˜ๅบฆใชๅ›ž็ญ”ๅˆ†ๆž", - "ใ‚ฟใ‚นใ‚ฏใฎใ‚ซใ‚นใ‚ฟใƒžใ‚คใ‚บ" - ] + "text": ["่‡ชๅˆ†ใฎๆ™‚้–“ใซๅ›ž็ญ”", "้ซ˜ๅบฆใชๅ›ž็ญ”ๅˆ†ๆž", "ใ‚ฟใ‚นใ‚ฏใฎใ‚ซใ‚นใ‚ฟใƒžใ‚คใ‚บ"] }, "LiveTest": { "title": "ใƒฉใ‚คใƒ–ใƒ†ใ‚นใƒˆ", @@ -1870,11 +1935,7 @@ "blank": { "title": "็ฉบ็™ฝใฎ็ ”็ฉถใ‹ใ‚‰้–‹ๅง‹", "description": "ๅฎŒๅ…จใชใ‚ซใ‚นใ‚ฟใƒžใ‚คใ‚บใงใ‚ผใƒญใ‹ใ‚‰็ ”็ฉถใ‚’ไฝœๆˆใ—ใพใ™", - "features": [ - "ๅฎŒๅ…จใชใ‚ซใ‚นใ‚ฟใƒžใ‚คใ‚บ", - "ใ‚ผใƒญใ‹ใ‚‰ๆง‹็ฏ‰", - "่จญๅฎšใ‚’ๅฎŒๅ…จใซๅˆถๅพก" - ] + "features": ["ๅฎŒๅ…จใชใ‚ซใ‚นใ‚ฟใƒžใ‚คใ‚บ", "ใ‚ผใƒญใ‹ใ‚‰ๆง‹็ฏ‰", "่จญๅฎšใ‚’ๅฎŒๅ…จใซๅˆถๅพก"] }, "template": { "title": "ใƒ†ใƒณใƒ—ใƒฌใƒผใƒˆใ‹ใ‚‰ไฝœๆˆ", diff --git a/src/app/plugins/locales/pt_br.json b/src/app/plugins/locales/pt_br.json index d1e001eeb..de95fb1db 100644 --- a/src/app/plugins/locales/pt_br.json +++ b/src/app/plugins/locales/pt_br.json @@ -43,7 +43,34 @@ "fair": "Regular", "good": "Boa", "strong": "Forte" - } + }, + "emailNotVerified": "E-mail nรฃo verificado", + "verificationEmailSent": "E-mail de verificaรงรฃo enviado com sucesso!", + "errorSendingVerification": "Erro ao enviar e-mail de verificaรงรฃo", + "verifyEmailTitle": "Verifique seu e-mail", + "verifyEmailSubtitle": "Enviamos um link de verificaรงรฃo para seu e-mail", + "emailLabel": "E-mail", + "verifyingEmail": "Verificando...", + "emailAlreadyVerified": "Email jรก verificado?", + "emailVerified": "E-mail verificado com sucesso!", + "checkEmailTitle": "Verifique seu e-mail", + "checkEmailStep1": "Verifique sua caixa de entrada para um e-mail de verificaรงรฃo", + "checkEmailStep2": "Clique no link do e-mail para verificar sua conta", + "checkEmailStep3": "Volte aqui e sua conta serรก verificada", + "checkSpamFolder": "Nรฃo vรช? Verifique sua pasta de spam", + "resending": "Reenviando...", + "resendEmail": "Reenviar e-mail", + "changeEmail": "Alterar e-mail", + "continueToDashboard": "Continuar para o painel", + "signOut": "Sair", + "newEmail": "Novo e-mail", + "enterNewEmail": "Insira seu novo endereรงo de e-mail", + "saving": "Salvando...", + "errorCheckingVerification": "Erro ao verificar o status de verificaรงรฃo", + "errorResendingEmail": "Erro ao reenviar e-mail de verificaรงรฃo", + "invalidEmail": "Endereรงo de e-mail invรกlido", + "emailUpdatedVerification": "E-mail atualizado! Enviamos um link de verificaรงรฃo para seu novo e-mail.", + "errorUpdatingEmail": "Erro ao atualizar e-mail" }, "common": { "enterTextHere": "Digite o texto aqui...", @@ -112,6 +139,7 @@ "user": "Usuรกrio", "id": "ID", "itemsPerPage": "Itens por pรกgina:", + "saving": "Salvando...", "visible": "Visรญvel" }, "storage": { @@ -384,7 +412,10 @@ "globalError": "Ocorreu um erro, desculpe-nos pelo transtorno.", "cameraPermissionDenied": "O acesso ร  cรขmera รฉ necessรกrio para continuar. Por favor, permita o acesso ร  cรขmera nas configuraรงรตes do navegador.", "sendError": "Nรฃo foi possรญvel enviar o convite. Por favor, tente novamente", - "missingVariableTitles": "Nรฃo รฉ possรญvel salvar: Algumas variรกveis estรฃo sem tรญtulos" + "missingVariableTitles": "Nรฃo รฉ possรญvel salvar: Algumas variรกveis estรฃo sem tรญtulos", + "failedToLoadAnswers": "Falha ao carregar respostas", + "failedToUpdateAnswer": "Falha ao atualizar resposta", + "failedToSaveAnswerCooperator": "Falha ao salvar resposta do cooperador" }, "Introduction": { "title": "UX LAB Remoto", @@ -1194,6 +1225,44 @@ "note": "Seu papel รฉ facilitar e observar. O participante deve concluir as tarefas de forma independente, a menos que solicite ajuda especificamente.", "startSession": "Iniciar Sessรฃo Moderada" }, + "VideoCallPanel": { + "toolsPanelTitle": "Painel de ferramentas", + "sessionControl": "Controle da sessao", + "joinRoomInfo": "Os controles para entrar na sala agora estao na interface principal acima", + "proceedNextStep": "Prosseguir para o proximo passo", + "endCall": "Encerrar chamada", + "activeCall": "Chamada ativa", + "participants": "Participantes", + "you": "Voce", + "observator": "Observador", + "moderator": "Moderador", + "connected": "Conectado", + "disconnected": "Desconectado", + "camera": "Camera", + "noCamera": "Sem camera", + "microphone": "Microfone", + "noMicrophone": "Sem microfone", + "settings": "Configuracao", + "disableCamera": "Desativar camera", + "enableCamera": "Ativar camera", + "muteMicrophone": "Silenciar microfone", + "unmuteMicrophone": "Ativar microfone", + "stopScreenShare": "Parar compartilhamento de tela", + "shareScreen": "Compartilhar tela", + "moderatorOnlySteps": "Somente o moderador pode alterar as etapas" + }, + "VideoCall": { + "screenSharingLabel": "Compartilhando tela", + "cameraOff": "A camera esta desligada", + "yourVideo": "Seu video", + "yourPreview": "Sua visualizacao", + "observatorMode": "Modo observador", + "waitingForModeratorToStartSession": "Aguardando o moderador iniciar a sessao...", + "observeAllFeedsNotice": "Voce podera observar todos os videos sem enviar o seu.", + "waitingForParticipants": "Aguardando participantes...", + "waitingForModerator": "Aguardando moderador...", + "autoStartWhenModeratorOpensRoom": "A chamada de video iniciara automaticamente quando o moderador abrir a sala." + }, "WelcomeStep": { "welcome": "Bem-vindo ao RUXAILAB!", "description": "Vocรช estรก prestes a participar de um teste de usuรกrio que visa avaliar a facilidade de uso e a compreensรฃo de uma aplicaรงรฃo digital. Esse tipo de teste nos ajuda a identificar possรญveis barreiras tecnolรณgicas e melhorar a experiรชncia para todos.", diff --git a/src/app/plugins/locales/ru.json b/src/app/plugins/locales/ru.json index 95ec376ce..3824ed076 100644 --- a/src/app/plugins/locales/ru.json +++ b/src/app/plugins/locales/ru.json @@ -43,7 +43,34 @@ "fair": "ะกั€ะตะดะฝะธะน", "good": "ะฅะพั€ะพัˆะธะน", "strong": "ะกะธะปัŒะฝั‹ะน" - } + }, + "emailNotVerified": "ะญะปะตะบั‚ั€ะพะฝะฝะฐั ะฟะพั‡ั‚ะฐ ะฝะต ะฟะพะดั‚ะฒะตั€ะถะดะตะฝะฐ", + "verificationEmailSent": "ะŸะธััŒะผะพ ะฟะพะดั‚ะฒะตั€ะถะดะตะฝะธั ะพั‚ะฟั€ะฐะฒะปะตะฝะพ ัƒัะฟะตัˆะฝะพ!", + "errorSendingVerification": "ะžัˆะธะฑะบะฐ ะฟั€ะธ ะพั‚ะฟั€ะฐะฒะบะต ะฟะธััŒะผะฐ ะฟะพะดั‚ะฒะตั€ะถะดะตะฝะธั", + "verifyEmailTitle": "ะŸะพะดั‚ะฒะตั€ะดะธั‚ะต ัะฒะพัŽ ัะปะตะบั‚ั€ะพะฝะฝัƒัŽ ะฟะพั‡ั‚ัƒ", + "verifyEmailSubtitle": "ะœั‹ ะพั‚ะฟั€ะฐะฒะธะปะธ ััั‹ะปะบัƒ ะฟะพะดั‚ะฒะตั€ะถะดะตะฝะธั ะฝะฐ ะฒะฐัˆัƒ ัะปะตะบั‚ั€ะพะฝะฝัƒัŽ ะฟะพั‡ั‚ัƒ", + "emailLabel": "ะญะปะตะบั‚ั€ะพะฝะฝะฐั ะฟะพั‡ั‚ะฐ", + "verifyingEmail": "ะŸะพะดั‚ะฒะตั€ะถะดะตะฝะธะต...", + "emailAlreadyVerified": "Imeili yamaze kwemezwa?", + "emailVerified": "ะญะปะตะบั‚ั€ะพะฝะฝะฐั ะฟะพั‡ั‚ะฐ ัƒัะฟะตัˆะฝะพ ะฟะพะดั‚ะฒะตั€ะถะดะตะฝะฐ!", + "checkEmailTitle": "ะŸั€ะพะฒะตั€ัŒั‚ะต ัะฒะพัŽ ัะปะตะบั‚ั€ะพะฝะฝัƒัŽ ะฟะพั‡ั‚ัƒ", + "checkEmailStep1": "ะŸั€ะพะฒะตั€ัŒั‚ะต ะฒั…ะพะดัั‰ะธะต ัะพะพะฑั‰ะตะฝะธั ะฝะฐ ะฟะธััŒะผะพ ะฟะพะดั‚ะฒะตั€ะถะดะตะฝะธั", + "checkEmailStep2": "ะะฐะถะผะธั‚ะต ะฝะฐ ััั‹ะปะบัƒ ะฒ ะฟะธััŒะผะต, ั‡ั‚ะพะฑั‹ ะฟะพะดั‚ะฒะตั€ะดะธั‚ัŒ ัะฒะพะน ะฐะบะบะฐัƒะฝั‚", + "checkEmailStep3": "ะ’ะตั€ะฝะธั‚ะตััŒ ััŽะดะฐ, ะธ ะฒะฐัˆ ะฐะบะบะฐัƒะฝั‚ ะฑัƒะดะตั‚ ะฟะพะดั‚ะฒะตั€ะถะดะตะฝ", + "checkSpamFolder": "ะะต ะฒะธะดะธั‚ะต? ะŸั€ะพะฒะตั€ัŒั‚ะต ะฟะฐะฟะบัƒ ัะฟะฐะผะฐ", + "resending": "ะŸะพะฒั‚ะพั€ะฝะฐั ะพั‚ะฟั€ะฐะฒะบะฐ...", + "resendEmail": "ะžั‚ะฟั€ะฐะฒะธั‚ัŒ ะฟะธััŒะผะพ ะฟะพะฒั‚ะพั€ะฝะพ", + "changeEmail": "ะ˜ะทะผะตะฝะธั‚ัŒ ัะปะตะบั‚ั€ะพะฝะฝัƒัŽ ะฟะพั‡ั‚ัƒ", + "continueToDashboard": "ะŸะตั€ะตะนั‚ะธ ะบ ะฟะฐะฝะตะปะธ ัƒะฟั€ะฐะฒะปะตะฝะธั", + "signOut": "ะ’ั‹ั…ะพะด", + "newEmail": "ะะพะฒะฐั ัะปะตะบั‚ั€ะพะฝะฝะฐั ะฟะพั‡ั‚ะฐ", + "enterNewEmail": "ะ’ะฒะตะดะธั‚ะต ะฝะพะฒั‹ะน ะฐะดั€ะตั ัะปะตะบั‚ั€ะพะฝะฝะพะน ะฟะพั‡ั‚ั‹", + "saving": "ะกะพั…ั€ะฐะฝะตะฝะธะต...", + "errorCheckingVerification": "ะžัˆะธะฑะบะฐ ะฟั€ะธ ะฟั€ะพะฒะตั€ะบะต ัั‚ะฐั‚ัƒัะฐ ะฟะพะดั‚ะฒะตั€ะถะดะตะฝะธั", + "errorResendingEmail": "ะžัˆะธะฑะบะฐ ะฟั€ะธ ะฟะพะฒั‚ะพั€ะฝะพะน ะพั‚ะฟั€ะฐะฒะบะต ะฟะธััŒะผะฐ ะฟะพะดั‚ะฒะตั€ะถะดะตะฝะธั", + "invalidEmail": "ะะตะฒะตั€ะฝั‹ะน ะฐะดั€ะตั ัะปะตะบั‚ั€ะพะฝะฝะพะน ะฟะพั‡ั‚ั‹", + "emailUpdatedVerification": "ะญะปะตะบั‚ั€ะพะฝะฝะฐั ะฟะพั‡ั‚ะฐ ะพะฑะฝะพะฒะปะตะฝะฐ! ะœั‹ ะพั‚ะฟั€ะฐะฒะธะปะธ ััั‹ะปะบัƒ ะฟะพะดั‚ะฒะตั€ะถะดะตะฝะธั ะฝะฐ ะฒะฐัˆัƒ ะฝะพะฒัƒัŽ ัะปะตะบั‚ั€ะพะฝะฝัƒัŽ ะฟะพั‡ั‚ัƒ.", + "errorUpdatingEmail": "ะžัˆะธะฑะบะฐ ะฟั€ะธ ะพะฑะฝะพะฒะปะตะฝะธะธ ัะปะตะบั‚ั€ะพะฝะฝะพะน ะฟะพั‡ั‚ั‹" }, "common": { "enterTextHere": "ะ’ะฒะตะดะธั‚ะต ั‚ะตะบัั‚ ะทะดะตััŒ...", @@ -112,6 +139,7 @@ "user": "ะŸะพะปัŒะทะพะฒะฐั‚ะตะปัŒ", "id": "ID", "itemsPerPage": "ะญะปะตะผะตะฝั‚ะพะฒ ะฝะฐ ัั‚ั€ะฐะฝะธั†ะต:", + "saving": "ะกะพั…ั€ะฐะฝะตะฝะธะต...", "visible": "ะ’ะธะดะธะผั‹ะน" }, "storage": { @@ -384,7 +412,10 @@ "globalError": "ะŸั€ะพะธะทะพัˆะปะฐ ะพัˆะธะฑะบะฐ, ะธะทะฒะธะฝะธั‚ะต ะทะฐ ะฝะตัƒะดะพะฑัั‚ะฒะฐ.", "cameraPermissionDenied": "ะ”ะปั ะฟั€ะพะดะพะปะถะตะฝะธั ั‚ั€ะตะฑัƒะตั‚ัั ะดะพัั‚ัƒะฟ ะบ ะบะฐะผะตั€ะต. ะŸะพะถะฐะปัƒะนัั‚ะฐ, ั€ะฐะทั€ะตัˆะธั‚ะต ะดะพัั‚ัƒะฟ ะบ ะบะฐะผะตั€ะต ะฒ ะฝะฐัั‚ั€ะพะนะบะฐั… ะฑั€ะฐัƒะทะตั€ะฐ.", "sendError": "ะะต ัƒะดะฐะปะพััŒ ะพั‚ะฟั€ะฐะฒะธั‚ัŒ ะฟั€ะธะณะปะฐัˆะตะฝะธะต. ะŸะพะถะฐะปัƒะนัั‚ะฐ, ะฟะพะฟั€ะพะฑัƒะนั‚ะต ะตั‰ะต ั€ะฐะท", - "missingVariableTitles": "ะะต ัƒะดะฐะตั‚ัั ัะพั…ั€ะฐะฝะธั‚ัŒ: ะะตะบะพั‚ะพั€ั‹ะผ ะฟะตั€ะตะผะตะฝะฝั‹ะผ ะฝะต ั…ะฒะฐั‚ะฐะตั‚ ะทะฐะณะพะปะพะฒะบะพะฒ" + "missingVariableTitles": "ะะต ัƒะดะฐะตั‚ัั ัะพั…ั€ะฐะฝะธั‚ัŒ: ะะตะบะพั‚ะพั€ั‹ะผ ะฟะตั€ะตะผะตะฝะฝั‹ะผ ะฝะต ั…ะฒะฐั‚ะฐะตั‚ ะทะฐะณะพะปะพะฒะบะพะฒ", + "failedToLoadAnswers": "Failed to load answers", + "failedToUpdateAnswer": "Failed to update answer", + "failedToSaveAnswerCooperator": "Failed to save cooperator answer" }, "Introduction": { "title": "ะฃะดะฐะปะตะฝะฝะฐั ะปะฐะฑะพั€ะฐั‚ะพั€ะธั UX", @@ -1194,6 +1225,44 @@ "note": "ะ’ะฐัˆะฐ ั€ะพะปัŒ โ€” ัะพะดะตะนัั‚ะฒะพะฒะฐั‚ัŒ ะธ ะฝะฐะฑะปัŽะดะฐั‚ัŒ. ะฃั‡ะฐัั‚ะฝะธะบ ะดะพะปะถะตะฝ ะฒั‹ะฟะพะปะฝัั‚ัŒ ะทะฐะดะฐั‡ะธ ัะฐะผะพัั‚ะพัั‚ะตะปัŒะฝะพ, ะตัะปะธ ั‚ะพะปัŒะบะพ ะพะฝ ัะฟะตั†ะธะฐะปัŒะฝะพ ะฝะต ะฟะพะฟั€ะพัะธั‚ ะพ ะฟะพะผะพั‰ะธ.", "startSession": "ะะฐั‡ะฐั‚ัŒ ะผะพะดะตั€ะธั€ัƒะตะผัƒัŽ ัะตััะธัŽ" }, + "VideoCallPanel": { + "toolsPanelTitle": "ะŸะฐะฝะตะปัŒ ะธะฝัั‚ั€ัƒะผะตะฝั‚ะพะฒ", + "sessionControl": "ะฃะฟั€ะฐะฒะปะตะฝะธะต ัะตััะธะตะน", + "joinRoomInfo": "ะšะฝะพะฟะบะธ ะฒั…ะพะดะฐ ะฒ ะบะพะผะฝะฐั‚ัƒ ั‚ะตะฟะตั€ัŒ ะฝะฐั…ะพะดัั‚ัั ะฒ ะพัะฝะพะฒะฝะพะผ ะธะฝั‚ะตั€ั„ะตะนัะต ะฒั‹ัˆะต", + "proceedNextStep": "ะŸะตั€ะตะนั‚ะธ ะบ ัะปะตะดัƒัŽั‰ะตะผัƒ ัˆะฐะณัƒ", + "endCall": "ะ—ะฐะฒะตั€ัˆะธั‚ัŒ ะทะฒะพะฝะพะบ", + "activeCall": "ะะบั‚ะธะฒะฝั‹ะน ะทะฒะพะฝะพะบ", + "participants": "ะฃั‡ะฐัั‚ะฝะธะบะธ", + "you": "ะ’ั‹", + "observator": "ะะฐะฑะปัŽะดะฐั‚ะตะปัŒ", + "moderator": "ะœะพะดะตั€ะฐั‚ะพั€", + "connected": "ะŸะพะดะบะปัŽั‡ะตะฝ", + "disconnected": "ะžั‚ะบะปัŽั‡ะตะฝ", + "camera": "ะšะฐะผะตั€ะฐ", + "noCamera": "ะ‘ะตะท ะบะฐะผะตั€ั‹", + "microphone": "ะœะธะบั€ะพั„ะพะฝ", + "noMicrophone": "ะ‘ะตะท ะผะธะบั€ะพั„ะพะฝะฐ", + "settings": "ะะฐัั‚ั€ะพะนะบะธ", + "disableCamera": "ะžั‚ะบะปัŽั‡ะธั‚ัŒ ะบะฐะผะตั€ัƒ", + "enableCamera": "ะ’ะบะปัŽั‡ะธั‚ัŒ ะบะฐะผะตั€ัƒ", + "muteMicrophone": "ะ’ั‹ะบะปัŽั‡ะธั‚ัŒ ะผะธะบั€ะพั„ะพะฝ", + "unmuteMicrophone": "ะ’ะบะปัŽั‡ะธั‚ัŒ ะผะธะบั€ะพั„ะพะฝ", + "stopScreenShare": "ะžัั‚ะฐะฝะพะฒะธั‚ัŒ ะดะตะผะพะฝัั‚ั€ะฐั†ะธัŽ ัะบั€ะฐะฝะฐ", + "shareScreen": "ะ”ะตะผะพะฝัั‚ั€ะธั€ะพะฒะฐั‚ัŒ ัะบั€ะฐะฝ", + "moderatorOnlySteps": "ะขะพะปัŒะบะพ ะผะพะดะตั€ะฐั‚ะพั€ ะผะพะถะตั‚ ะผะตะฝัั‚ัŒ ัˆะฐะณะธ" + }, + "VideoCall": { + "screenSharingLabel": "ะ”ะตะผะพะฝัั‚ั€ะฐั†ะธั ัะบั€ะฐะฝะฐ", + "cameraOff": "ะšะฐะผะตั€ะฐ ะฒั‹ะบะปัŽั‡ะตะฝะฐ", + "yourVideo": "ะ’ะฐัˆะต ะฒะธะดะตะพ", + "yourPreview": "ะ’ะฐัˆ ะฟั€ะตะดะฟั€ะพัะผะพั‚ั€", + "observatorMode": "ะ ะตะถะธะผ ะฝะฐะฑะปัŽะดะฐั‚ะตะปั", + "waitingForModeratorToStartSession": "ะžะถะธะดะฐะฝะธะต, ะฟะพะบะฐ ะผะพะดะตั€ะฐั‚ะพั€ ะฝะฐั‡ะฝะตั‚ ัะตััะธัŽ...", + "observeAllFeedsNotice": "ะ’ั‹ ัะผะพะถะตั‚ะต ะฝะฐะฑะปัŽะดะฐั‚ัŒ ะฒัะต ะฒะธะดะตะพะฟะพั‚ะพะบะธ, ะฝะต ะพั‚ะฟั€ะฐะฒะปัั ัะฒะพะน.", + "waitingForParticipants": "ะžะถะธะดะฐะฝะธะต ัƒั‡ะฐัั‚ะฝะธะบะพะฒ...", + "waitingForModerator": "ะžะถะธะดะฐะฝะธะต ะผะพะดะตั€ะฐั‚ะพั€ะฐ...", + "autoStartWhenModeratorOpensRoom": "ะ’ะธะดะตะพะทะฒะพะฝะพะบ ะฝะฐั‡ะฝะตั‚ัั ะฐะฒั‚ะพะผะฐั‚ะธั‡ะตัะบะธ, ะบะพะณะดะฐ ะผะพะดะตั€ะฐั‚ะพั€ ะพั‚ะบั€ะพะตั‚ ะบะพะผะฝะฐั‚ัƒ." + }, "WelcomeStep": { "welcome": "ะ”ะพะฑั€ะพ ะฟะพะถะฐะปะพะฒะฐั‚ัŒ ะฒ RUXAILAB!", "description": "ะ’ั‹ ัะพะฑะธั€ะฐะตั‚ะตััŒ ะฟั€ะธะฝัั‚ัŒ ัƒั‡ะฐัั‚ะธะต ะฒ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปัŒัะบะพะผ ั‚ะตัั‚ะต, ะฝะฐะฟั€ะฐะฒะปะตะฝะฝะพะผ ะฝะฐ ะพั†ะตะฝะบัƒ ัƒะดะพะฑัั‚ะฒะฐ ะธัะฟะพะปัŒะทะพะฒะฐะฝะธั ะธ ะฟะพะฝะธะผะฐะฝะธั ั†ะธั„ั€ะพะฒะพะณะพ ะฟั€ะธะปะพะถะตะฝะธั. ะญั‚ะพั‚ ั‚ะธะฟ ั‚ะตัั‚ะฐ ะฟะพะผะพะณะฐะตั‚ ะฝะฐะผ ะฒั‹ัะฒะธั‚ัŒ ะฒะพะทะผะพะถะฝั‹ะต ั‚ะตั…ะฝะพะปะพะณะธั‡ะตัะบะธะต ะฑะฐั€ัŒะตั€ั‹ ะธ ัƒะปัƒั‡ัˆะธั‚ัŒ ะพะฟั‹ั‚ ะดะปั ะฒัะตั….", diff --git a/src/app/plugins/locales/zh.json b/src/app/plugins/locales/zh.json index 9992f52b5..732db4ec6 100644 --- a/src/app/plugins/locales/zh.json +++ b/src/app/plugins/locales/zh.json @@ -43,7 +43,34 @@ "fair": "ไธ€่ˆฌ", "good": "ๅฅฝ", "strong": "ๅผบ" - } + }, + "emailNotVerified": "็”ตๅญ้‚ฎไปถๆœช้ชŒ่ฏ", + "verificationEmailSent": "้ชŒ่ฏ็”ตๅญ้‚ฎไปถๅ‘้€ๆˆๅŠŸ๏ผ", + "errorSendingVerification": "ๅ‘้€้ชŒ่ฏ็”ตๅญ้‚ฎไปถๆ—ถๅ‡บ้”™", + "verifyEmailTitle": "้ชŒ่ฏๆ‚จ็š„็”ตๅญ้‚ฎไปถ", + "verifyEmailSubtitle": "ๆˆ‘ไปฌๅทฒๅ‘ๆ‚จ็š„็”ตๅญ้‚ฎไปถๅ‘้€ไบ†้ชŒ่ฏ้“พๆŽฅ", + "emailLabel": "็”ตๅญ้‚ฎไปถ", + "verifyingEmail": "้ชŒ่ฏไธญ...", + "emailAlreadyVerified": "็”ตๅญ้‚ฎไปถๅทฒ็ป้ชŒ่ฏไบ†ๅ—๏ผŸ", + "emailVerified": "็”ตๅญ้‚ฎไปถ้ชŒ่ฏๆˆๅŠŸ๏ผ", + "checkEmailTitle": "ๆฃ€ๆŸฅๆ‚จ็š„็”ตๅญ้‚ฎไปถ", + "checkEmailStep1": "ๆฃ€ๆŸฅๆ‚จ็š„ๆ”ถไปถ็ฎฑไปฅ่Žทๅ–้ชŒ่ฏ็”ตๅญ้‚ฎไปถ", + "checkEmailStep2": "ๅ•ๅ‡ป็”ตๅญ้‚ฎไปถไธญ็š„้“พๆŽฅไปฅ้ชŒ่ฏๆ‚จ็š„ๅธๆˆท", + "checkEmailStep3": "่ฟ”ๅ›žๆญคๅค„๏ผŒๆ‚จ็š„ๅธๆˆทๅฐ†่ขซ้ชŒ่ฏ", + "checkSpamFolder": "ๆฒก็œ‹ๅˆฐ๏ผŸๆฃ€ๆŸฅๆ‚จ็š„ๅžƒๅœพ้‚ฎไปถๆ–‡ไปถๅคน", + "resending": "้‡ๆ–ฐๅ‘้€ไธญ...", + "resendEmail": "้‡ๆ–ฐๅ‘้€็”ตๅญ้‚ฎไปถ", + "changeEmail": "ๆ›ดๆ”น็”ตๅญ้‚ฎไปถ", + "continueToDashboard": "็ปง็ปญๅˆฐไปช่กจๆฟ", + "signOut": "้€€ๅ‡บ็™ปๅฝ•", + "newEmail": "ๆ–ฐ็”ตๅญ้‚ฎไปถ", + "enterNewEmail": "่พ“ๅ…ฅๆ‚จ็š„ๆ–ฐ็”ตๅญ้‚ฎไปถๅœฐๅ€", + "saving": "ไฟๅญ˜ไธญ...", + "errorCheckingVerification": "ๆฃ€ๆŸฅ้ชŒ่ฏ็Šถๆ€ๆ—ถๅ‡บ้”™", + "errorResendingEmail": "้‡ๆ–ฐๅ‘้€้ชŒ่ฏ็”ตๅญ้‚ฎไปถๆ—ถๅ‡บ้”™", + "invalidEmail": "ๆ— ๆ•ˆ็š„็”ตๅญ้‚ฎไปถๅœฐๅ€", + "emailUpdatedVerification": "็”ตๅญ้‚ฎไปถๅทฒๆ›ดๆ–ฐ๏ผๆˆ‘ไปฌๅทฒๅ‘ๆ‚จ็š„ๆ–ฐ็”ตๅญ้‚ฎไปถๅ‘้€ไบ†้ชŒ่ฏ้“พๆŽฅใ€‚", + "errorUpdatingEmail": "ๆ›ดๆ–ฐ็”ตๅญ้‚ฎไปถๆ—ถๅ‡บ้”™" }, "common": { "enterTextHere": "ๅœจๆญค่พ“ๅ…ฅๆ–‡ๆœฌ...", @@ -112,6 +139,7 @@ "user": "็”จๆˆท", "id": "ID", "itemsPerPage": "ๆฏ้กต้กน็›ฎๆ•ฐ๏ผš", + "saving": "ไฟๅญ˜ไธญ...", "visible": "ๅฏ่ง" }, "storage": { @@ -384,7 +412,10 @@ "globalError": "ๅ‘็”Ÿ้”™่ฏฏ๏ผŒ็ป™ๆ‚จๅธฆๆฅไธไพฟ๏ผŒๆ•ฌ่ฏท่ฐ…่งฃใ€‚", "cameraPermissionDenied": "็ปง็ปญๆ“ไฝœ้œ€่ฆ่ฎฟ้—ฎๆ‘„ๅƒๅคดใ€‚่ฏทๅœจๆต่งˆๅ™จ่ฎพ็ฝฎไธญๅ…่ฎธๆ‘„ๅƒๅคด่ฎฟ้—ฎใ€‚", "sendError": "ๅ‘้€้‚€่ฏทๅคฑ่ดฅ๏ผŒ่ฏท้‡่ฏ•", - "missingVariableTitles": "ๆ— ๆณ•ไฟๅญ˜๏ผšไธ€ไบ›ๅ˜้‡็ผบๅฐ‘ๆ ‡้ข˜" + "missingVariableTitles": "ๆ— ๆณ•ไฟๅญ˜๏ผšไธ€ไบ›ๅ˜้‡็ผบๅฐ‘ๆ ‡้ข˜", + "failedToLoadAnswers": "Failed to load answers", + "failedToUpdateAnswer": "Failed to update answer", + "failedToSaveAnswerCooperator": "Failed to save cooperator answer" }, "Introduction": { "title": "UX่ฟœ็จ‹ๅฎž้ชŒๅฎค", @@ -1194,6 +1225,44 @@ "note": "ๆ‚จ็š„่ง’่‰ฒๆ˜ฏๆไพ›ไพฟๅˆฉๅ’Œ่ง‚ๅฏŸใ€‚ๅ‚ไธŽ่€…ๅบ”็‹ฌ็ซ‹ๅฎŒๆˆไปปๅŠก๏ผŒ้™ค้žไป–ไปฌ็‰นๅˆซ่ฆๆฑ‚ๅธฎๅŠฉใ€‚", "startSession": "ๅฏๅŠจไธปๆŒๅผ็Žฏ่Š‚" }, + "VideoCallPanel": { + "toolsPanelTitle": "ๅทฅๅ…ท้ขๆฟ", + "sessionControl": "ไผš่ฏๆŽงๅˆถ", + "joinRoomInfo": "ๅŠ ๅ…ฅๆˆฟ้—ด็š„ๆŽงๅˆถ็ŽฐๅœจไฝไบŽไธŠๆ–นไธป็•Œ้ข", + "proceedNextStep": "่ฟ›ๅ…ฅไธ‹ไธ€ๆญฅ", + "endCall": "็ป“ๆŸ้€š่ฏ", + "activeCall": "้€š่ฏ่ฟ›่กŒไธญ", + "participants": "ๅ‚ไธŽ่€…", + "you": "ไฝ ", + "observator": "่ง‚ๅฏŸ่€…", + "moderator": "ไธปๆŒไบบ", + "connected": "ๅทฒ่ฟžๆŽฅ", + "disconnected": "ๅทฒๆ–ญๅผ€", + "camera": "ๆ‘„ๅƒๅคด", + "noCamera": "ๆ— ๆ‘„ๅƒๅคด", + "microphone": "้บฆๅ…‹้ฃŽ", + "noMicrophone": "ๆ— ้บฆๅ…‹้ฃŽ", + "settings": "่ฎพ็ฝฎ", + "disableCamera": "ๅ…ณ้—ญๆ‘„ๅƒๅคด", + "enableCamera": "ๅผ€ๅฏๆ‘„ๅƒๅคด", + "muteMicrophone": "้บฆๅ…‹้ฃŽ้™้Ÿณ", + "unmuteMicrophone": "ๅ–ๆถˆ้บฆๅ…‹้ฃŽ้™้Ÿณ", + "stopScreenShare": "ๅœๆญขๅฑๅน•ๅ…ฑไบซ", + "shareScreen": "ๅ…ฑไบซๅฑๅน•", + "moderatorOnlySteps": "ๅชๆœ‰ไธปๆŒไบบๅฏไปฅๆ›ดๆ”นๆญฅ้ชค" + }, + "VideoCall": { + "screenSharingLabel": "ๆญฃๅœจๅ…ฑไบซๅฑๅน•", + "cameraOff": "ๆ‘„ๅƒๅคดๅทฒๅ…ณ้—ญ", + "yourVideo": "ไฝ ็š„่ง†้ข‘", + "yourPreview": "ไฝ ็š„้ข„่งˆ", + "observatorMode": "่ง‚ๅฏŸ่€…ๆจกๅผ", + "waitingForModeratorToStartSession": "ๆญฃๅœจ็ญ‰ๅพ…ไธปๆŒไบบๅผ€ๅง‹ไผš่ฏ...", + "observeAllFeedsNotice": "ไฝ ๅฏไปฅๅœจไธๅ‘้€่‡ชๅทฑ่ง†้ข‘็š„ๆƒ…ๅ†ตไธ‹่ง‚ๅฏŸๆ‰€ๆœ‰่ง†้ข‘ๆตใ€‚", + "waitingForParticipants": "ๆญฃๅœจ็ญ‰ๅพ…ๅ‚ไธŽ่€…...", + "waitingForModerator": "ๆญฃๅœจ็ญ‰ๅพ…ไธปๆŒไบบ...", + "autoStartWhenModeratorOpensRoom": "ๅฝ“ไธปๆŒไบบๆ‰“ๅผ€ๆˆฟ้—ดๆ—ถ๏ผŒ่ง†้ข‘้€š่ฏๅฐ†่‡ชๅŠจๅผ€ๅง‹ใ€‚" + }, "WelcomeStep": { "welcome": "ๆฌข่ฟŽๆฅๅˆฐRUXAILAB๏ผ", "description": "ๆ‚จๅฐ†ๅ‚ไธŽไธ€้กนๆ—จๅœจ่ฏ„ไผฐๆ•ฐๅญ—ๅบ”็”จ็จ‹ๅบๆ˜“็”จๆ€งๅ’Œ็†่งฃ็จ‹ๅบฆ็š„็”จๆˆทๆต‹่ฏ•ใ€‚่ฟ™็ง็ฑปๅž‹็š„ๆต‹่ฏ•ๅธฎๅŠฉๆˆ‘ไปฌๆฃ€ๆต‹ๅฏ่ƒฝ็š„ๆŠ€ๆœฏ้šœ็ขๅนถๆ”นๅ–„ๆฏไธชไบบ็š„ไฝ“้ชŒใ€‚", @@ -1566,11 +1635,7 @@ "test": "ๆต‹่ฏ•", "testType_1": { "testTitle": "ๅฏ็”จๆ€งๅฏๅ‘ๅผ", - "text": [ - "ๅฏ็”จๆ€ง็™พๅˆ†ๆฏ”", - "ๆœ€็ปˆๆŠฅๅ‘ŠPDF", - "้‚€่ฏทไธ“ๅฎถ่ฏ„ไผฐๆ‚จ็š„ๅบ”็”จ็จ‹ๅบ" - ] + "text": ["ๅฏ็”จๆ€ง็™พๅˆ†ๆฏ”", "ๆœ€็ปˆๆŠฅๅ‘ŠPDF", "้‚€่ฏทไธ“ๅฎถ่ฏ„ไผฐๆ‚จ็š„ๅบ”็”จ็จ‹ๅบ"] }, "testType_2": { "testTitle": "็”จๆˆทๅฏ็”จๆ€ง", @@ -1584,20 +1649,12 @@ "SelfTest": { "title": "่‡ชไธปๆต‹่ฏ•", "type": "ๆ— ไธปๆŒ", - "text": [ - "ๅœจ็ฉบ้—ฒๆ—ถ้—ดๅ›ž็ญ”", - "ๅขžๅผบ็š„็ญ”ๆกˆๅˆ†ๆž", - "ไปปๅŠก่‡ชๅฎšไน‰" - ] + "text": ["ๅœจ็ฉบ้—ฒๆ—ถ้—ดๅ›ž็ญ”", "ๅขžๅผบ็š„็ญ”ๆกˆๅˆ†ๆž", "ไปปๅŠก่‡ชๅฎšไน‰"] }, "LiveTest": { "title": "ๅฎžๆ—ถๆต‹่ฏ•", "type": "ๆœ‰ไธปๆŒ", - "text": [ - "ๆ‘„ๅƒๅคดใ€้Ÿณ้ข‘ๅ’Œๅฑๅน•ๅฝ•ๅˆถ", - "ๅขžๅผบ็š„็ญ”ๆกˆๅˆ†ๆž", - "ๆœ‰ไธปๆŒ็š„ๅฎžๆ—ถๆต‹่ฏ•" - ] + "text": ["ๆ‘„ๅƒๅคดใ€้Ÿณ้ข‘ๅ’Œๅฑๅน•ๅฝ•ๅˆถ", "ๅขžๅผบ็š„็ญ”ๆกˆๅˆ†ๆž", "ๆœ‰ไธปๆŒ็š„ๅฎžๆ—ถๆต‹่ฏ•"] } } }, @@ -1870,20 +1927,12 @@ "blank": { "title": "ไปŽ็ฉบ็™ฝ็ ”็ฉถๅผ€ๅง‹", "description": "ไปŽๅคดๅผ€ๅง‹ๅˆ›ๅปบ็ ”็ฉถ๏ผŒๅฎŒๅ…จๅฏๅฎšๅˆถ", - "features": [ - "ๅฎŒๅ…จๅฏๅฎšๅˆถ", - "ไปŽๅคดๆž„ๅปบ", - "ๅฏน่ฎพ็ฝฎ็š„ๅฎŒๅ…จๆŽงๅˆถ" - ] + "features": ["ๅฎŒๅ…จๅฏๅฎšๅˆถ", "ไปŽๅคดๆž„ๅปบ", "ๅฏน่ฎพ็ฝฎ็š„ๅฎŒๅ…จๆŽงๅˆถ"] }, "template": { "title": "ไปŽๆจกๆฟๅˆ›ๅปบ", "description": "ไฝฟ็”จ้ข„ๆž„ๅปบ็š„ๆจกๆฟๅฟซ้€Ÿๅผ€ๅง‹", - "features": [ - "ๅฟซ้€Ÿ่ฎพ็ฝฎ", - "้ข„้…็ฝฎ่ฎพ็ฝฎ", - "ๅŒ…ๅซๆœ€ไฝณๅฎž่ทต" - ] + "features": ["ๅฟซ้€Ÿ่ฎพ็ฝฎ", "้ข„้…็ฝฎ่ฎพ็ฝฎ", "ๅŒ…ๅซๆœ€ไฝณๅฎž่ทต"] } }, "details": { diff --git a/src/app/router/index.js b/src/app/router/index.js index cbf4e6f6c..41b15dee2 100644 --- a/src/app/router/index.js +++ b/src/app/router/index.js @@ -7,7 +7,6 @@ import HeuristicRoutes from '@/ux/Heuristic/router.js' import accessibilityRoutes from '@/ux/accessibility/router.js' import UserTestRoutes from '@/ux/UserTest/router.js' import store from '@/store' -import { getTemplateManagerPath } from '@/shared/utils/templateRouting' const routes = [ ...Public, @@ -36,10 +35,20 @@ router.beforeEach(async (to, from, next) => { return next() // Allow immediate access without any checks } + // Allow access to public pages even if user is logged in but email not verified + const publicPages = ['/signin', '/signup', '/verify-email', '/forgot-password'] + if (publicPages.includes(to.path)) { + return next() + } + if (!user) { - await store.dispatch('autoSignIn') + const authUser = await store.dispatch('autoSignIn') user = store.state.Auth.user + // If user is logged in but email not verified, redirect to verify-email + if (authUser && authUser.emailVerified === false && !publicPages.includes(to.path)) { + return next('/verify-email') } +} if (to.path === '/') return next(redirect()) @@ -49,49 +58,6 @@ router.beforeEach(async (to, from, next) => { } } - if (to.meta?.templateAccess && to.params?.id) { - const template = await store.dispatch('getTemplateById', to.params.id) - - if (!template) { - store.commit('SET_TOAST', { - type: 'error', - message: 'Template not found.', - }) - return next('/admin') - } - - const ownerId = template.header?.templateAuthor?.userDocId - const isOwner = ownerId && user?.id === ownerId - const isPublic = Boolean(template.header?.isTemplatePublic) - - if (!isOwner && !isPublic) { - store.commit('SET_TOAST', { - type: 'error', - message: 'You do not have permission to access this template.', - }) - return next('/admin') - } - - const canonicalPath = getTemplateManagerPath(template) - if (canonicalPath && canonicalPath !== to.path) { - if (to.meta?.templateSection === 'manager') { - return next({ path: canonicalPath, query: to.query, replace: true }) - } - } - - if (to.meta?.templateOwnerOnly && !isOwner) { - store.commit('SET_TOAST', { - type: 'error', - message: 'Only template owner can access configuration.', - }) - return next(canonicalPath || '/admin') - } - } - - if (user && ['/signin', '/signup'].includes(to.path)) { - return next(redirect()) - } - next() }) diff --git a/src/controllers/StudyController.js b/src/controllers/StudyController.js index 0fc3e88c0..6b43c18ac 100644 --- a/src/controllers/StudyController.js +++ b/src/controllers/StudyController.js @@ -59,10 +59,10 @@ export default class StudyController extends Controller { promises.push( userController.removeTestFromUser(cooperator.userDocId, payload.id), ) - promises.push( - userController.removeNotificationsForTest(payload.id, cooperators), - ) } + promises.push( + userController.removeNotificationsForTest(payload.id, cooperators), + ) await Promise.all(promises) } await super.update('users', payload.testAdmin.userDocId, payload.auxUser) diff --git a/src/features/auth/controllers/AuthController.js b/src/features/auth/controllers/AuthController.js index 737938b38..f41831312 100644 --- a/src/features/auth/controllers/AuthController.js +++ b/src/features/auth/controllers/AuthController.js @@ -150,4 +150,20 @@ export default class AuthController { throw err } } + + async sendVerificationEmail(email, userName) { + try { + const emailController = new EmailController() + await emailController.send({ + to: email, + subject: 'Verify Your Email Address', + template: 'emailVerification', + data: { + userName: userName || email, + }, + }) + } catch (err) { + throw err + } + } } diff --git a/src/features/auth/store/Auth.js b/src/features/auth/store/Auth.js index a38b3b608..4ea03d3a4 100644 --- a/src/features/auth/store/Auth.js +++ b/src/features/auth/store/Auth.js @@ -63,11 +63,21 @@ export default { */ async signup({ commit }, payload) { try { + const normalizedEmail = payload.email?.trim().toLowerCase() const { user } = await authController.signUp( - payload.email, + normalizedEmail, payload.password, ) - await userController.create({ id: user.uid, email: user.email }) + await userController.create({ + id: user.uid, + email: user.email || normalizedEmail, + }) + + // Send verification email + try { + await authController.sendVerificationEmail(user.email, user.email) + } catch {} + commit('SET_TOAST', { message: i18n.global.t('auth.signupSuccess'), type: 'success', @@ -87,12 +97,22 @@ export default { commit('setLoading', true) try { + const normalizedEmail = payload.email?.trim().toLowerCase() const { user } = await authController.signIn( - payload.email, + normalizedEmail, payload.password, payload.rememberMe, ) + // Check if email is verified + if (!user.emailVerified) { + commit('SET_TOAST', { + message: i18n.global.t('auth.emailNotVerified'), + type: 'warning', + }) + throw new Error('EMAIL_NOT_VERIFIED') + } + const dbUser = await userController.getById(user.uid) commit('SET_USER', dbUser) @@ -102,6 +122,9 @@ export default { type: 'success', }) } catch (err) { + if (err.message === 'EMAIL_NOT_VERIFIED') { + throw err + } showError('errors.incorrectCredential') return err } finally { @@ -124,9 +147,8 @@ export default { let dbUser = null try { dbUser = await userController.getById(user.uid) - } catch (error) { + } catch { // User doesn't exist in DB, will be created below - return error } // Create user if they don't exist yet @@ -148,6 +170,9 @@ export default { type: 'success', }) } catch (err) { + if (err.message === 'EMAIL_NOT_VERIFIED') { + throw err + } commit('SET_TOAST', { message: i18n.global.t('errors.globalError'), type: 'error', @@ -185,6 +210,13 @@ export default { const user = await authController.autoSignIn() if (!user) return + // Check if email is verified + if (!user.emailVerified) { + // User is logged in but email not verified + // Don't set them as fully authenticated, but allow access to verify-email page + return user + } + const dbUser = await userController.getById(user.uid) commit('SET_USER', dbUser) } catch (e) { @@ -231,5 +263,21 @@ export default { commit('setLoading', false) } }, + + async sendVerificationEmail({ commit }, { email, userName }) { + try { + await authController.sendVerificationEmail(email, userName) + commit('SET_TOAST', { + message: i18n.global.t('auth.verificationEmailSent'), + type: 'success', + }) + } catch (err) { + commit('SET_TOAST', { + message: i18n.global.t('auth.errorSendingVerification'), + type: 'error', + }) + throw err + } + }, }, } diff --git a/src/features/auth/views/SignInView.vue b/src/features/auth/views/SignInView.vue index e3bdcb9eb..e1e1a0d56 100644 --- a/src/features/auth/views/SignInView.vue +++ b/src/features/auth/views/SignInView.vue @@ -1,13 +1,19 @@ + + + + + diff --git a/src/ux/UserTest/components/manager/ParticipantsInfo.vue b/src/ux/UserTest/components/manager/ParticipantsInfo.vue index 03829717c..2e49e0c87 100644 --- a/src/ux/UserTest/components/manager/ParticipantsInfo.vue +++ b/src/ux/UserTest/components/manager/ParticipantsInfo.vue @@ -107,6 +107,7 @@ import { computed } from 'vue' import { useRouter } from 'vue-router' import { useI18n } from 'vue-i18n' +import { useStore } from 'vuex' const props = defineProps({ test: { @@ -116,17 +117,15 @@ const props = defineProps({ }) const router = useRouter() +const store = useStore() const { t } = useI18n() +// Read answers from the centralized Answer Vuex store getter +const allAnswers = computed(() => store.getters.allAnswersList) + // Get completed participants from answers const completedAnswers = computed(() => { - const testAnswers = props.test?.answers || [] - if (Array.isArray(testAnswers)) { - return testAnswers.filter((answer) => answer.submitted) - } else if (typeof testAnswers === 'object' && testAnswers !== null) { - return Object.values(testAnswers).filter((answer) => answer.submitted) - } - return [] + return allAnswers.value.filter((answer) => answer.submitted) }) // Get cooperators @@ -146,18 +145,18 @@ const pendingInvitations = computed(() => { }) const totalParticipants = computed(() => { - // Total = accepted cooperators (who can participate) - return acceptedCooperators.value.length + // Use the larger of: accepted cooperators or total answers + // This ensures admin test-taking is counted even without being a cooperator + return Math.max(acceptedCooperators.value.length, allAnswers.value.length) }) const completedParticipants = computed(() => { - // Count completed answers return completedAnswers.value.length }) const notStartedParticipants = computed(() => { - // Accepted cooperators who haven't completed yet - return acceptedCooperators.value.length - completedAnswers.value.length + // Participants who haven't completed: total minus completed, floored at 0 + return Math.max(0, totalParticipants.value - completedAnswers.value.length) }) const completionPercentage = computed(() => { diff --git a/src/ux/UserTest/components/manager/StorageInfo.vue b/src/ux/UserTest/components/manager/StorageInfo.vue index 15f9ff5df..0a561645a 100644 --- a/src/ux/UserTest/components/manager/StorageInfo.vue +++ b/src/ux/UserTest/components/manager/StorageInfo.vue @@ -138,6 +138,7 @@ import { computed } from 'vue' import { useRouter } from 'vue-router' import { formatBytes } from '@/shared/utils/formatUtils' import { useI18n } from 'vue-i18n' +import { useStore } from 'vuex' const props = defineProps({ test: { @@ -147,15 +148,14 @@ const props = defineProps({ }) const router = useRouter() +const store = useStore() const { t } = useI18n() // Storage quota (in bytes) - you can make this configurable const STORAGE_QUOTA = 5 * 1024 * 1024 * 1024 // 5GB default -const answers = computed(() => { - const testAnswers = props.test?.answers || [] - return Array.isArray(testAnswers) ? testAnswers : Object.values(testAnswers) -}) +// Read answers from the centralized Answer Vuex store getter +const answers = computed(() => store.getters.allAnswersList) const totalMediaFiles = computed(() => { let count = 0 diff --git a/src/ux/UserTest/components/manager/StudyOverview.vue b/src/ux/UserTest/components/manager/StudyOverview.vue index 9a0cb0c30..79d12ca46 100644 --- a/src/ux/UserTest/components/manager/StudyOverview.vue +++ b/src/ux/UserTest/components/manager/StudyOverview.vue @@ -100,8 +100,10 @@ + + diff --git a/src/ux/UserTest/components/steps/PreTasksStep.vue b/src/ux/UserTest/components/steps/PreTasksStep.vue index 3a9a98cd3..8b2833d3a 100644 --- a/src/ux/UserTest/components/steps/PreTasksStep.vue +++ b/src/ux/UserTest/components/steps/PreTasksStep.vue @@ -9,15 +9,14 @@

-
-

{{ $t('UserTestView.PreTasksStep.recordingTitle') }}

@@ -25,9 +24,9 @@

{{ $t('UserTestView.PreTasksStep.recordingDescription') }}

- + -
+
diff --git a/src/ux/UserTest/components/steps/PreTestStep.vue b/src/ux/UserTest/components/steps/PreTestStep.vue index e05299c5b..7ff820c4f 100644 --- a/src/ux/UserTest/components/steps/PreTestStep.vue +++ b/src/ux/UserTest/components/steps/PreTestStep.vue @@ -1,100 +1,178 @@ + + diff --git a/src/ux/UserTest/components/steps/WelcomeStep.vue b/src/ux/UserTest/components/steps/WelcomeStep.vue index 82e436126..268b8eabd 100644 --- a/src/ux/UserTest/components/steps/WelcomeStep.vue +++ b/src/ux/UserTest/components/steps/WelcomeStep.vue @@ -34,12 +34,13 @@ :title="$t('UserTestView.WelcomeStep.steps.consent')" /> - - - + + - - @@ -95,12 +106,15 @@ import ShowInfo from '@/shared/components/ShowInfo.vue' import { VStepperVertical } from 'vuetify/labs/VStepperVertical' import { useDisplay } from 'vuetify' -const props = defineProps({ + +defineProps({ stepperValue: { type: Number, required: true }, welcomeMessage: { type: String, default: '' }, hasEyeTracking: { type: Boolean, default: false }, + hasPreTest: { type: Boolean, default: true }, + hasPostTest: { type: Boolean, default: true }, }) -const emit = defineEmits(['start']) +defineEmits(['start']) const { smAndDown } = useDisplay() diff --git a/src/ux/UserTest/views/ModeratedTestView.vue b/src/ux/UserTest/views/ModeratedTestView.vue index a33a60f59..821f4e8f0 100644 --- a/src/ux/UserTest/views/ModeratedTestView.vue +++ b/src/ux/UserTest/views/ModeratedTestView.vue @@ -786,6 +786,10 @@ watch( // Methods const proceedToNextStep = async () => { if (!isUserTestAdmin.value) return + + // Increment globalIndex before updating Firebase + globalIndex.value = globalIndex.value + 1 + const roomRef = dbRef(database, `rooms/${roomId.value}`) await update(roomRef, { globalIndex: globalIndex.value, @@ -959,6 +963,9 @@ const startTest = async () => { if (data.showVideoCall !== undefined) { displayVideoCallComponent.value = data.showVideoCall } + } else { + // Admin always stays in video call during session + displayVideoCallComponent.value = true } }) diff --git a/src/ux/UserTest/views/UserTestView.vue b/src/ux/UserTest/views/UserTestView.vue index caf88fd6f..99f24334d 100644 --- a/src/ux/UserTest/views/UserTestView.vue +++ b/src/ux/UserTest/views/UserTestView.vue @@ -222,14 +222,16 @@ complete-icon="mdi-check" /> - - + + - - @@ -353,7 +367,11 @@ /> test.value?.testStructure?.userTasks?.some((task) => task.hasEye), ) +const hasPreTest = computed(() => { + return ( + test.value?.testStructure?.preTest != null && + test.value?.testStructure?.preTest.length > 0 + ) +}) + +const hasPostTest = computed(() => { + return ( + test.value?.testStructure?.postTest != null && + test.value?.testStructure?.postTest.length > 0 + ) +}) + const isUserTestAdmin = computed(() => { return test.value.testAdmin.userDocId === user.value?.id }) @@ -797,18 +830,15 @@ const attachMediaToTasks = (answer, mediaUrls) => { for (const type in medias) { if (type === 'sizes') { const sizes = medias[type] - console.log(`Found sizes for Task ${taskIndex}:`, sizes) + if (sizes.screenRecordURL) { task.screenSize = sizes.screenRecordURL - console.log('Set screenSize:', task.screenSize) } if (sizes.audioRecordURL) { task.audioSize = sizes.audioRecordURL - console.log('Set audioSize:', task.audioSize) } if (sizes.webcamRecordURL) { task.webcamSize = sizes.webcamRecordURL - console.log('Set webcamSize:', task.webcamSize) } continue } @@ -919,7 +949,12 @@ const completeStep = (id, type, userCompleted = true) => { try { if (type === 'consent') { localTestAnswer.consentCompleted = true - globalIndex.value = 2 // PreTest + if (hasPreTest.value) { + globalIndex.value = 2 // PreTest + } else { + globalIndex.value = 4 // Tasks + localTestAnswer.preTestCompleted = true + } savePartialAnswer() } @@ -960,8 +995,12 @@ const completeStep = (id, type, userCompleted = true) => { } else { if (allTasksCompleted.value) { taskIndex.value = id + 1 // to help saving methods - globalIndex.value = hasEyeTracking.value ? 6 : 5 // PostTest - } else { + if (hasPostTest.value) { + globalIndex.value = hasEyeTracking.value ? 6 : 5 // PostTest + } else { + globalIndex.value = hasEyeTracking.value ? 7 : 6 // Finish + localTestAnswer.postTestCompleted = true + } } } //TODO: Show proper toast not the following one diff --git a/src/ux/accessibility/view/automatic/Answers.vue b/src/ux/accessibility/view/automatic/Answers.vue index 5d3ee3d89..829f8e8f0 100644 --- a/src/ux/accessibility/view/automatic/Answers.vue +++ b/src/ux/accessibility/view/automatic/Answers.vue @@ -547,6 +547,7 @@