From 767e1670f930aa486df170330e33f55e9da02cfa Mon Sep 17 00:00:00 2001
From: Fides
Date: Fri, 20 Feb 2026 21:12:15 +0200
Subject: [PATCH] feat(i18n): centralize translations in Google Sheets and
document workflow
---
add-a-new-language.md | 144 ++++++-----------
backend/.env.example | 4 +
backend/app/i18n/locales/en-GB/messages.json | 2 +-
backend/app/i18n/locales/en-US/messages.json | 2 +-
.../import_translations_from_sheets.py | 147 ++++++++++++++++++
backend/scripts/locale_registration.py | 147 ++++++++++++++++++
frontend-new/src/auth/pages/Login/Login.tsx | 2 +-
.../Login/__snapshots__/Login.test.tsx.snap | 15 +-
.../components/timestamp/Timestamp.tsx | 3 +-
.../__snapshots__/Timestamp.test.tsx.snap | 3 +-
.../SkillReportPDF.test.tsx.snap | 8 +-
.../ExperiencesReportContent.test.tsx.snap | 2 +-
.../rotateToSolve/RotateToSolvePuzzle.tsx | 2 +-
.../RotateToSolvePuzzle.test.tsx.snap | 3 +-
.../SkillsRankingJobSeekerDisclosure.tsx | 4 +-
...lsRankingJobSeekerDisclosure.test.tsx.snap | 9 +-
.../SkillsRankingJobMarketDisclosure.tsx | 6 +-
...lsRankingJobMarketDisclosure.test.tsx.snap | 10 +-
.../SkillsRankingProofOfValue.test.tsx.snap | 3 +-
.../SkillsRankingRetypedRank.tsx | 4 +-
.../SkillsRankingRetypedRank.test.tsx.snap | 9 +-
.../src/i18n/locales/en-GB/translation.json | 26 ++--
.../src/i18n/locales/en-US/translation.json | 26 ++--
.../src/i18n/locales/es-AR/translation.json | 22 +--
.../src/i18n/locales/es-ES/translation.json | 22 +--
25 files changed, 449 insertions(+), 176 deletions(-)
create mode 100644 backend/scripts/import_translations_from_sheets.py
create mode 100644 backend/scripts/locale_registration.py
diff --git a/add-a-new-language.md b/add-a-new-language.md
index 8310b44a2..6c3192d11 100644
--- a/add-a-new-language.md
+++ b/add-a-new-language.md
@@ -2,103 +2,78 @@
This guide is divided into two main parts:
-1. **[Adding a New Language](#adding-a-new-language)**: Instructions for introducing a new locale across the backend and frontend.
+1. **[Adding a New Language](#adding-a-new-language)**: Instructions for introducing a new locale using the automated Google Sheets workflow.
2. **[Switching or Using Supported Languages](#switching-or-using-supported-languages)** : Guidance for configuring or switching between languages that are already supported.
## Adding a New Language
-This section shows how to introduce a new locale end-to-end across the backend and the frontend. It includes what files to add, the expected templates, which configs to update, and how to verify everything.
+Compass uses an automated translation workflow based on Google Sheets. This allows non-technical users to add translations from a familiar spreadsheet, while developers simply run an import script to automatically apply them to the codebase.
-Locale identifiers follow [IETF BCP 47](https://www.ietf.org/rfc/bcp/bcp47.txt) (e.g., `en-GB`, `es-AR`). Keep language in lowercase and region in uppercase. Keep codes consistent across config and folders.
+Locale identifiers must follow [IETF BCP 47](https://www.ietf.org/rfc/bcp/bcp47.txt) (e.g., `en-GB`, `es-AR`, `fr-FR`). Keep language codes in lowercase and region codes in uppercase.
-Notes
+### 1. For Translators (Non-Technical)
-* Use BCP‑47 language tags (e.g., `en-GB`, `en-US`, `es-ES`, `es-AR`). Keep codes consistent across config and folders.
-* Backend uses locale directories under `backend/app/i18n/locales//` with domain JSON files (e.g., `messages.json`).
-* Frontend-new uses `frontend-new/src/i18n/locales//translation.json` and maps them in [`frontend-new/src/i18n/i18n.ts`](frontend-new/src/i18n/i18n.ts).
-* Supported languages are enabled via environment variables on both backend and frontend.
+1. **Open the Compass Translation Sheet:** Access the shared Google Sheet provided by the development team.
+2. **Add a New Language Column:** Find the first empty column header after the existing language codes. Type the BCP-47 language code for your new language (e.g., `fr-FR` for French) and press Enter.
+ > **Note:** Do NOT modify the `Platform` or `Key` columns, as these are strict technical identifiers required for the application to function and **must only be edited by developers or other authorised maintainers**.
+3. **Fill in the Translations:** Scroll through the rows. Use the English (`en-GB` or `en-US`) columns as a reference. Type your translated text into your new column. If you leave a cell blank, the application will automatically fall back to English for that specific text.
+4. **Notify the Team:** Once translations are complete, notify the development team to run the import script.
-### Prerequisites
+### 2. For Developers (Technical)
-* Decide the language code(s) you want to support (e.g., `es-AR`).
-* Pick a reference language to copy from (English is recommended):
- - Backend reference: [`backend/app/i18n/locales/en-US/messages.json`](backend/app/i18n/locales/en-US/messages.json)
- - Frontend-new reference: [`frontend-new/src/i18n/locales/en-GB/translation.json`](frontend-new/src/i18n/locales/en-GB/translation.json)
+Once translations or new keys have been added to the Google Sheet, follow these steps to pull them into the codebase.
-### 1. Backend
-
-The backend uses `LocaleProvider` to determine the current locale. The default locale in `BACKEND_LANGUAGE_CONFIG` configuration is used as the default.
-
-#### 1.1 Create Locale directory and messages file
-
-* Create a folder for your new locale under `backend/app/i18n/locales//` and add a `messages.json` file with the same keys as English.
-* Translate backend-facing strings in `backend/app/i18n/locales//messages.json`. Use [`en-US/messages.json`](backend/app/i18n/locales/en-US/messages.json) as the reference.
-* Key consistency is enforced by [`backend/app/i18n/test_i18n.py`](backend/app/i18n/test_i18n.py).
-
-Example
-```
-backend/app/i18n/locales/en-US/messages.json # reference
-backend/app/i18n/locales/es-AR/messages.json # new
-```
-For the message structure and keys, please refer to the reference file: [`backend/app/i18n/locales/en-US/messages.json`](backend/app/i18n/locales/en-US/messages.json).
+> **Supported platforms**
+>
+> Currently, only three platforms are powered by this workflow:
+> - **Frontend** (React app)
+> - **Backend** (Python services)
+> - **Feedback** (survey/questions content)
+>
+> Only rows/keys that belong to one of these platforms will be generated. If a *new* platform is ever introduced, the import script must be extended to support it before any keys for that platform will have an effect.
-#### 1.2 Update Supported Constants and Environment Variables
+#### 2.1 Run the Import Script
- - Add the locale to [`backend/app/i18n/types.py`](backend/app/i18n/types.py) (`Locale` enum and `SUPPORTED_LOCALES`).
- - Update [`backend/app/i18n/constants.py`](backend/app/i18n/constants.py) only if the default fallback changes.
- - Enable the new locale in your backend environment via `BACKEND_LANGUAGE_CONFIG`.
-
-#### 1.3 Verify Backend Key Consistency
-
-`I18nManager` can verify that all locales contain the same keys per domain; [`backend/app/i18n/test_i18n.py`](backend/app/i18n/test_i18n.py) enforces key parity.
-
-Optional commands
+Navigate to the `backend` directory and run the automated import script:
```bash
cd backend
-poetry run python scripts/verify_i18n_keys.py --verify
-```
-
-### 2. Frontend
-
-The frontend uses `i18next` with resources defined in locale files and mapped in [`frontend-new/src/i18n/i18n.ts`](frontend-new/src/i18n/i18n.ts).
-
-#### 2.1 Create the locale folder and translation file
-
-Create a folder for your new locale under `frontend-new/src/locales//` and copy the reference `translation.json` from English (`en-GB`). Translate values, keeping all keys identical.
-
-Example
+poetry run python scripts/import_translations_from_sheets.py
```
-frontend-new/src/i18n/locales/en-GB/translation.json # reference
-frontend-new/src/i18n/locales/es-AR/translation.json # new
-```
-
-For the translation structure and keys, please refer to the reference file: [`frontend-new/src/i18n/locales/en-GB/translation.json`](frontend-new/src/i18n/locales/en-GB/translation.json).
-
-* **Add other locale files:**
- - Add `frontend-new/src/feedback/overallFeedback/feedbackForm/questions-.json` mirroring the existing files, e.g., [`questions-en-GB.json`](frontend-new/src/feedback/overallFeedback/feedbackForm/questions-en-GB.json).
- - Add `frontend-new/public/data/config/fields-.yaml`; keep the same structure/keys as [`fields-en-GB.yaml`](frontend-new/public/data/config/fields-en-GB.yaml).
-#### 2.2 Register Locale Resources
+**What this script does:**
+* Connects to the Google Sheet and downloads the translation matrices.
+* Generates or updates the corresponding JSON files in `frontend-new/src/i18n/locales/`, `backend/app/i18n/locales/`, and `frontend-new/src/feedback/overallFeedback/feedbackForm/`.
+* Ensures that **new translation keys** defined in the sheet are created for the supported platforms, and keeps existing keys in sync.
+* **Auto-registers the new locale:** It automatically patches the codebase to recognize the new language by modifying:
+ * `config/default.json` (Adds locale to `supportedLocales` and injects English fallbacks into `sensitiveData`)
+ * `backend/app/i18n/types.py` (Locale enum and `SUPPORTED_LOCALES`)
+ * `frontend-new/src/i18n/constants.ts` (Locale enum, `LocalesLabels`, `SupportedLocales`)
+ * `frontend-new/src/i18n/i18n.ts` (Import statements and internal resource mapping)
-* Update [`frontend-new/src/i18n/constants.ts`](frontend-new/src/i18n/constants.ts) (add to `Locale`, `LocalesLabels`, `SupportedLocales`, and adjust `FALL_BACK_LOCALE` only if the fallback changes).
-* Import and register the translation and feedback JSON files in [`frontend-new/src/i18n/i18n.ts`](frontend-new/src/i18n/i18n.ts).
+> **When to re-run the script**
+>
+> Any time the shared translation sheet is updated (new language, updated strings, or new keys), a developer must:
+> 1. Re-run `scripts/import_translations_from_sheets.py`.
+> 2. Commit the regenerated locale files and configuration changes.
+> 3. Deploy the updated services so the changes are visible in all environments.
-#### 2.3 Environment Config
+#### 2.2 Manual Steps Required
-Enable the new locale via environment variables:
-* `FRONTEND_SUPPORTED_LOCALES`: a JSON array of enabled locale codes (see [`parseEnvSupportedLocales.ts`](frontend-new/src/i18n/languageContextMenu/parseEnvSupportedLocales.ts) for validation rules).
-* `FRONTEND_DEFAULT_LOCALE`: default language if user preference not set.
+While the script handles JSON translations and locale registration, one manual step remains:
-Environment variables are Base64-encoded and read from [`frontend-new/public/data/env.example.js`](frontend-new/public/data/env.example.js).
+1. **Embeddings:** Generate taxonomy embeddings for the new language using the appropriate model ID (see [Generate Embeddings](./deployment-procedure.md#step-43-generate-embeddings)).
-#### 2.4 Verify Frontend Key Consistency
+#### 2.3 Verify Key Consistency
-There is an automated test that ensures all locales share the same keys as English (`en-GB`): [`frontend-new/src/i18n/locales/locales.test.ts`](frontend-new/src/i18n/locales/locales.test.ts).
-
-Optional commands
+You can optionally run the automated tests to ensure exact key consistency across platforms:
```bash
+# Backend verification
+cd backend
+poetry run python scripts/verify_i18n_keys.py --verify
+
+# Frontend verification
cd frontend-new
yarn test -- src/i18n/locales/locales.test.ts
```
@@ -107,30 +82,13 @@ yarn test -- src/i18n/locales/locales.test.ts
Supported languages are enabled via environment variables on both backend and frontend.
-* **Add environment variables for the frontend** ([see deployment procedure](./deployment-procedure.md) and [upload to secret manager](./deployment-procedure.md):
+* **Add environment variables for the frontend** ([see deployment procedure](./deployment-procedure.md) and [upload to secret manager](./deployment-procedure.md)):
- `FRONTEND_SUPPORTED_LOCALES`: JSON string array of supported locales, e.g., `["en-GB","es-ES"]`.
- `FRONTEND_DEFAULT_LOCALE`: Default locale string.
* **Add environment variables for the backend** ([see deployment procedure](./deployment-procedure.md) and [upload to secret manager](./deployment-procedure.md)):
- `BACKEND_LANGUAGE_CONFIG`: Default backend locale (BCP 47).
-### 4. Quick checklist (summary)
-
-* **Backend**
- * [ ] Create `backend/app/i18n/locales//messages.json` with the same keys as English
- * [ ] Add language configurations to `BACKEND_LANGUAGE_CONFIG`
- * [ ] Add the locale to `backend/app/i18n/types.py` (`Locale` enum and `SUPPORTED_LOCALES`)
- * [ ] Generate embeddings for the new language using the new taxonomy model ID \([Generate Embeddings](./deployment-procedure.md#step-43-generate-embeddings)\)
- * [ ] (Optional) Run backend i18n verify script
-
-* **Frontend-new**
- * [ ] Create `frontend-new/src/i18n/locales//translation.json` with the same keys as English (`en-GB`)
- * [ ] Import and register resources in `frontend-new/src/i18n/i18n.ts`
- * [ ] Update `frontend-new/src/i18n/constants.ts` (`Locale`, `LocalesLabels`, `SupportedLocales`)
- * [ ] Add `frontend-new/src/feedback/overallFeedback/feedbackForm/questions-.json`
- * [ ] Add `frontend-new/public/data/config/fields-.yaml`
- * [ ] Update `env` config: `FRONTEND_SUPPORTED_LOCALES` and `FRONTEND_DEFAULT_LOCALE`
- * [ ] (Optional) Run the locales consistency test
## Switching or Using Supported Languages
@@ -144,12 +102,12 @@ This section explains how to enable or switch between languages that are already
### 1. Backend
* **Language Config:** `BACKEND_LANGUAGE_CONFIG` determines the default backend language and other configurations.
-* **Supported languages:** Listed in the `SUPPORTED_LOCALES` enum in [`backend/app/i18n/types.py`](backend/app/i18n/types.py).
+* **Supported languages:** Listed in the `SUPPORTED_LOCALES` list in [`backend/app/i18n/types.py`](backend/app/i18n/types.py).
### 2. Frontend
* **Registered languages:** Defined in [`frontend-new/src/i18n/i18n.ts`](frontend-new/src/i18n/i18n.ts) (translation resources) and [`frontend-new/src/i18n/constants.ts`](frontend-new/src/i18n/constants.ts) (`Locale`, `LocalesLabels`, `SupportedLocales`).
-* **Default language:** `FRONTEND_DEFAULT_LOCALE` in `public/data/env.js` sets the default UI language.
+* **Default language:** `FRONTEND_DEFAULT_LOCALE` in `public/data/env.js` sets the default UI language if no user preference is set.
* **Switching languages at runtime:** Users can select a language from the UI menu. Only locales listed in `FRONTEND_SUPPORTED_LOCALES` are available. To change the default language in the frontend, update `FRONTEND_DEFAULT_LOCALE` in the environment configuration.
-> **Note:** Backend and frontend are not automatically synchronized. To ensure a consistent language across the application, configure both layers to support and use the same language.
+> **Note:** Backend and frontend active languages are not automatically synchronized. To ensure a completely consistent language across the application, configure both layers to support and default to the same language.
diff --git a/backend/.env.example b/backend/.env.example
index b0d4d77a4..04fbb1ee4 100644
--- a/backend/.env.example
+++ b/backend/.env.example
@@ -63,3 +63,7 @@ BACKEND_LANGUAGE_CONFIG='{"default_locale":"en-US","available_locales":[{"locale
# Branding settings
GLOBAL_PRODUCT_NAME=Compass
+
+# Google Sheets translations
+GOOGLE_SHEETS_CREDENTIALS=keys/credentials-sheets.json
+GOOGLE_SHEET_ID=
diff --git a/backend/app/i18n/locales/en-GB/messages.json b/backend/app/i18n/locales/en-GB/messages.json
index adf654dda..5e3460998 100644
--- a/backend/app/i18n/locales/en-GB/messages.json
+++ b/backend/app/i18n/locales/en-GB/messages.json
@@ -62,4 +62,4 @@
"unseenUnpaid": "What do you think is most important when helping out in the community or caring for others?"
}
}
-}
\ No newline at end of file
+}
diff --git a/backend/app/i18n/locales/en-US/messages.json b/backend/app/i18n/locales/en-US/messages.json
index adf654dda..5e3460998 100644
--- a/backend/app/i18n/locales/en-US/messages.json
+++ b/backend/app/i18n/locales/en-US/messages.json
@@ -62,4 +62,4 @@
"unseenUnpaid": "What do you think is most important when helping out in the community or caring for others?"
}
}
-}
\ No newline at end of file
+}
diff --git a/backend/scripts/import_translations_from_sheets.py b/backend/scripts/import_translations_from_sheets.py
new file mode 100644
index 000000000..bb0b81ca6
--- /dev/null
+++ b/backend/scripts/import_translations_from_sheets.py
@@ -0,0 +1,147 @@
+"""
+Script to download the latest translations from the Google Sheet and automatically
+update the frontend and backend JSON language files.
+"""
+
+import json
+import os
+import sys
+from pathlib import Path
+
+from dotenv import load_dotenv
+from google.oauth2 import service_account
+from googleapiclient.discovery import build
+
+from locale_registration import register_locales
+
+# Configuration
+load_dotenv()
+GOOGLE_SHEETS_CREDENTIALS = os.getenv("GOOGLE_SHEETS_CREDENTIALS")
+GOOGLE_SHEET_ID = os.getenv("GOOGLE_SHEET_ID")
+SCOPES = ["https://www.googleapis.com/auth/spreadsheets.readonly"]
+
+PLATFORM_MAPPINGS = {
+ "Frontend": "frontend-new/src/i18n/locales/{locale}/translation.json",
+ "Backend": "backend/app/i18n/locales/{locale}/messages.json",
+ "Feedback": "frontend-new/src/feedback/overallFeedback/feedbackForm/questions-{locale}.json",
+}
+
+def build_service(credentials_path: str):
+ """Builds a Google Sheets service object."""
+ creds = service_account.Credentials.from_service_account_file(
+ credentials_path, scopes=SCOPES
+ )
+ return build("sheets", "v4", credentials=creds)
+
+def fetch_rows(service, sheet_id: str, range_name: str = "A:ZZZ") -> list[list[str]]:
+ """Fetches all rows from the specified spreadsheet range."""
+ result = service.spreadsheets().values().get(
+ spreadsheetId=sheet_id, range=range_name
+ ).execute()
+ return result.get("values", [])
+
+def set_nested(d: dict, key_path: str, value):
+ """Sets a value in a nested dictionary using a dot-notated key path."""
+ parts = key_path.split(".")
+ for part in parts[:-1]:
+ if part not in d or not isinstance(d[part], dict):
+ d[part] = {}
+ d = d[part]
+ # Format value: restore newlines and handle nulls
+ d[parts[-1]] = value.replace("\\n", "\n") if isinstance(value, str) else value
+
+def _get_translations_data(header: list[str], rows: list[list[str]], locales: list[str]) -> dict:
+ """Groups spreadsheet rows by target (platform, locale)."""
+ all_data = {}
+ for row in rows[1:]:
+ padded = row + [""] * (len(header) - len(row))
+ platform, key_path = padded[0].strip(), padded[1].strip()
+
+ if platform not in PLATFORM_MAPPINGS or not key_path:
+ continue
+
+ for i, locale in enumerate(locales):
+ value = padded[2 + i]
+ # If value is empty, use None so it becomes 'null' in JSON
+ val_to_set = value if value != "" else None
+
+ target = (platform, locale)
+ all_data.setdefault(target, {})[key_path] = val_to_set
+ return all_data
+
+def _sync_dict(existing: dict, incoming: dict) -> dict:
+ """Order-preserving sync of two nested dicts"""
+ result: dict = {}
+
+ for key, old_val in existing.items():
+ if key not in incoming:
+ continue
+ new_val = incoming[key]
+ if isinstance(old_val, dict) and isinstance(new_val, dict):
+ result[key] = _sync_dict(old_val, new_val)
+ else:
+ result[key] = new_val
+
+ for key, new_val in incoming.items():
+ if key not in result:
+ result[key] = new_val
+
+ return result
+
+
+def _write_json_files(root_dir: Path, all_data: dict):
+ """Writes translation data to JSON files using an order-preserving sync"""
+ for (platform, locale), updates in all_data.items():
+ file_path = root_dir / PLATFORM_MAPPINGS[platform].format(locale=locale)
+ file_path.parent.mkdir(parents=True, exist_ok=True)
+
+ # Build the incoming dict from sheet data (dot-notation → nested)
+ incoming: dict = {}
+ for key, val in updates.items():
+ set_nested(incoming, key, val)
+
+ # Load an existing file (empty dict for new locales)
+ existing: dict = {}
+ if file_path.exists():
+ try:
+ existing = json.loads(file_path.read_text(encoding="utf-8") or "{}")
+ except json.JSONDecodeError:
+ pass
+
+ synced = _sync_dict(existing, incoming)
+
+ with open(file_path, "w", encoding="utf-8") as f:
+ json.dump(synced, f, indent=2, ensure_ascii=False)
+ f.write("\n")
+
+def main():
+ if not GOOGLE_SHEETS_CREDENTIALS or not GOOGLE_SHEET_ID:
+ print("ERROR: GOOGLE_SHEETS_CREDENTIALS or GOOGLE_SHEET_ID not set in .env")
+ sys.exit(1)
+
+ root_dir = Path(__file__).resolve().parent.parent.parent
+ service = build_service(GOOGLE_SHEETS_CREDENTIALS)
+ rows = fetch_rows(service, GOOGLE_SHEET_ID)
+
+ if not rows:
+ print("ERROR: Sheet is empty.")
+ sys.exit(1)
+
+ header = rows[0]
+ if len(header) < 3 or header[0] != "Platform" or header[1] != "Key":
+ print("ERROR: Invalid header. Expected ['Platform', 'Key', , ...]")
+ sys.exit(1)
+
+ locales = header[2:]
+ all_data = _get_translations_data(header, rows, locales)
+ _write_json_files(root_dir, all_data)
+
+ print(f"Import complete. Processed {len(all_data)} translation files.")
+
+ # Auto-register any new locales found in the sheet
+ new_locs = register_locales(root_dir, locales)
+ if new_locs:
+ print(f"Successfully registered {len(new_locs)} new languages: {', '.join(new_locs)}")
+
+if __name__ == "__main__":
+ main()
diff --git a/backend/scripts/locale_registration.py b/backend/scripts/locale_registration.py
new file mode 100644
index 000000000..718424795
--- /dev/null
+++ b/backend/scripts/locale_registration.py
@@ -0,0 +1,147 @@
+"""
+Helper module called by import_translations_from_sheets.py to auto-register
+new languages in the backend and frontend configurations.
+"""
+
+import json
+import re
+from pathlib import Path
+from typing import Union, Callable
+
+# Converts locale codes like 'en-GB' into 'enGb' for TypeScript var names.
+def get_camel_case(locale: str) -> str:
+ lang, *region = locale.split("-")
+ return lang + "".join(r.capitalize() for r in region)
+
+# Converts locale codes like 'en-GB' into 'EN_GB' for Enum keys.
+def get_enum_key(locale: str) -> str:
+ return locale.replace("-", "_").upper()
+
+# Finds and replaces/injects code within a file using regex.
+def _patch(path: Path, pattern: str, repl: Union[str, Callable], count: int = 1):
+ if not path.exists():
+ return
+ content = path.read_text("utf-8")
+
+ if not re.search(pattern, content, flags=re.DOTALL):
+ raise RuntimeError(
+ f"Locale registration patch failed: pattern did not match in {path}.\n"
+ f"Pattern: {pattern!r}"
+ )
+
+ new_content = re.sub(pattern, repl, content, flags=re.DOTALL, count=count)
+ if content == new_content:
+ raise RuntimeError(
+ f"Locale registration patch made no changes in {path}.\n"
+ f"Pattern: {pattern!r}"
+ )
+
+ path.write_text(new_content, "utf-8")
+
+# Injects a token into a bracketed list (e.g. [A, B]) if not present.
+def _inject_in_list(path: Path, pattern: str, token: str):
+ if not path.exists():
+ return
+ content = path.read_text("utf-8")
+
+ match = re.search(pattern, content, flags=re.DOTALL)
+ if not match:
+ raise RuntimeError(
+ f"Locale registration list injection failed: pattern did not match in {path}.\n"
+ f"Pattern: {pattern!r}"
+ )
+
+ if token in match.group(2):
+ return
+
+ _patch(
+ path,
+ pattern,
+ lambda m: f"{m.group(1)}{m.group(2).rstrip()}{', ' if m.group(2).strip() else ''}{token}{m.group(3)}",
+ )
+
+def update_backend_types(path: Path, locale: str):
+ key = get_enum_key(locale)
+ # Add to Locale Enum members
+ _patch(path, r'([A-Z_]+\s*=\s*"[^"]+")\n+\s+(@staticmethod)', rf'\1\n {key} = "{locale}"\n\n \2')
+ # Add to labels
+ case = f' case Locale.{key}:\n return "{locale}"'
+ _patch(
+ path,
+ r'( def label\(self\)[^\n]*\n[\s\S]*return "[^"]+"\n)(\n\s*SUPPORTED_LOCALES:)',
+ rf'\1{case}\n\2',
+ )
+ # Add to a supported list
+ _inject_in_list(path, r"(SUPPORTED_LOCALES:\s*list\[Locale\]\s*=\s*\[)([^\]]*)(\])", f"Locale.{key}")
+
+def update_frontend_constants(path: Path, locale: str):
+ key = get_enum_key(locale)
+ # Add to Locale enum
+ _patch(path, r"(export enum Locale \{[^}]*)(\n})", rf'\1\n {key} = "{locale}",\2')
+ # Add to labels
+ _patch(path, r"(export const LocalesLabels = \{[\s\S]*?)(} as const;)", rf'\1 [Locale.{key}]: "{locale}",\n\2')
+ # Add to a supported list
+ _inject_in_list(path, r"(export const SupportedLocales: Locale\[\] = \[)([^\]]*)(\];)", f"Locale.{key}")
+
+def update_frontend_i18n(path: Path, locale: str):
+ camel, key = get_camel_case(locale), get_enum_key(locale)
+ q_var = f"questions{camel[0].upper()}{camel[1:]}"
+ # Imports
+ _patch(path, r'(import [a-zA-Z]+ from "\./locales/[^/]+/translation\.json";)', rf'\1\nimport {camel} from "./locales/{locale}/translation.json";')
+ _patch(path, r'(import questions[a-zA-Z]+ from "src/feedback/[^"]+";)', rf'\1\nimport {q_var} from "src/feedback/overallFeedback/feedbackForm/questions-{locale}.json";')
+ # Resources mapping: append to end of block
+ entry = f" ...constructLocaleResources(Locale.{key}, {{ ...{camel}, questions: {q_var} }}),"
+ _patch(path, r"(\n};)", rf"\n{entry}\1")
+
+def _update_field_translations(field: dict, locale: str, fallback: str) -> bool:
+ """Updates translations for a single field in default.json. Returns True if changed."""
+ changed = False
+ for attr in ["label", "questionText", "values", "validation"]:
+ target = field.get(attr) if attr != "validation" else field.get("validation", {}).get("errorMessage")
+ if isinstance(target, dict) and locale not in target:
+ target[locale] = target.get(fallback) or (next(iter(target.values()), "" if attr != "values" else []))
+ changed = True
+ return changed
+
+def update_default_json(path: Path, locale: str, fallback: str = "en-GB"):
+ if not path.exists(): return
+ data = json.loads(path.read_text("utf-8"))
+ changed = False
+
+ # Supported list
+ ui = data.get("i18n", {}).get("ui", {})
+ if "supportedLocales" in ui and locale not in ui["supportedLocales"]:
+ ui["supportedLocales"].append(locale); changed = True
+
+ # Sensitive data fields
+ for field in data.get("sensitiveData", {}).get("fields", {}).values():
+ if _update_field_translations(field, locale, fallback):
+ changed = True
+
+ if changed:
+ path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", "utf-8")
+
+def register_locales(root: Path, locales: list[str]):
+ locs = [str(l).strip() for l in locales if str(l).strip()]
+ constants_path = root / "frontend-new" / "src" / "i18n" / "constants.ts"
+ if not constants_path.exists(): return []
+
+ content = constants_path.read_text("utf-8")
+ new_locs = [l for l in locs if f'= "{l}"' not in content]
+ if not new_locs: return []
+
+ print(f"Registering: {new_locs}")
+ files = {
+ "backend_types": root / "backend" / "app" / "i18n" / "types.py",
+ "frontend_constants": constants_path,
+ "frontend_i18n": root / "frontend-new" / "src" / "i18n" / "i18n.ts",
+ "default_json": root / "config" / "default.json"
+ }
+
+ for locale in new_locs:
+ update_backend_types(files["backend_types"], locale)
+ update_frontend_constants(files["frontend_constants"], locale)
+ update_frontend_i18n(files["frontend_i18n"], locale)
+ update_default_json(files["default_json"], locale)
+
+ return new_locs
diff --git a/frontend-new/src/auth/pages/Login/Login.tsx b/frontend-new/src/auth/pages/Login/Login.tsx
index 013622f90..ac031bea9 100644
--- a/frontend-new/src/auth/pages/Login/Login.tsx
+++ b/frontend-new/src/auth/pages/Login/Login.tsx
@@ -521,7 +521,7 @@ const Login: React.FC = () => {
)}
{!registrationDisabled && (
- {t("auth.pages.login.dontHaveAnAccount")}
+ {t("auth.pages.login.dontHaveAnAccount")}{" "}
navigate(routerPaths.REGISTER)}>{t("common.buttons.register")}
)}
diff --git a/frontend-new/src/auth/pages/Login/__snapshots__/Login.test.tsx.snap b/frontend-new/src/auth/pages/Login/__snapshots__/Login.test.tsx.snap
index bc339f387..00a154382 100644
--- a/frontend-new/src/auth/pages/Login/__snapshots__/Login.test.tsx.snap
+++ b/frontend-new/src/auth/pages/Login/__snapshots__/Login.test.tsx.snap
@@ -86,7 +86,8 @@ exports[`Testing Login component Render tests should hide login code related ele
class="MuiTypography-root MuiTypography-caption css-1q1mrxw-MuiTypography-root"
data-testid="login-register-link-7ce9ba1f-bde0-48e2-88df-e4f697945cc4"
>
- Don't have an account?
+ Don't have an account?
+
- Don't have an account?
+ Don't have an account?
+
- Don't have an account?
+ Don't have an account?
+
- Don't have an account?
+ Don't have an account?
+
- Don't have an account?
+ Don't have an account?
+
= ({ sentAt }) => {
return (
- {t("chat.chatMessage.sent")}
- {sentText}
+ {t("chat.chatMessage.sent")} {sentText}
);
};
diff --git a/frontend-new/src/chat/chatMessage/components/chatMessageFooter/components/timestamp/__snapshots__/Timestamp.test.tsx.snap b/frontend-new/src/chat/chatMessage/components/chatMessageFooter/components/timestamp/__snapshots__/Timestamp.test.tsx.snap
index 4a2f63a13..042f5e056 100644
--- a/frontend-new/src/chat/chatMessage/components/chatMessageFooter/components/timestamp/__snapshots__/Timestamp.test.tsx.snap
+++ b/frontend-new/src/chat/chatMessage/components/chatMessageFooter/components/timestamp/__snapshots__/Timestamp.test.tsx.snap
@@ -5,7 +5,8 @@ exports[`render tests should render the Chat Message Timestamp without a child i
class="MuiTypography-root MuiTypography-caption css-1nk71a8-MuiTypography-root"
data-testid="chat-message-timestamp-c9253326-05ee-4bff-9d43-7852ca78a033"
>
- sent
+ sent
+
a foo years ago
`;
diff --git a/frontend-new/src/experiences/report/reportPdf/__snapshots__/SkillReportPDF.test.tsx.snap b/frontend-new/src/experiences/report/reportPdf/__snapshots__/SkillReportPDF.test.tsx.snap
index 05f6939a0..99f82aa2c 100644
--- a/frontend-new/src/experiences/report/reportPdf/__snapshots__/SkillReportPDF.test.tsx.snap
+++ b/frontend-new/src/experiences/report/reportPdf/__snapshots__/SkillReportPDF.test.tsx.snap
@@ -211,7 +211,7 @@ exports[`Report should render Report correctly 1`] = `
x="0"
y="0"
>
- Top Skills:
+ Top Skills:
- Top Skills:
+ Top Skills:
- Disclaimer:
+ Disclaimer:
- Listed skills are based on a conversation with the candidate, are not verified or validated by Tabiya, and may be inaccurate.
+ Listed skills are based on a conversation with the candidate, are not verified or validated by Tabiya, and may be inaccurate.
Information should be checked before use for job search, job interviews, or for creating a CV. To revise this information, speak with {{appName}} again or create a complete CV based on this report.
diff --git a/frontend-new/src/experiences/report/reportPdf/components/experiencesReportContent/__snapshots__/ExperiencesReportContent.test.tsx.snap b/frontend-new/src/experiences/report/reportPdf/components/experiencesReportContent/__snapshots__/ExperiencesReportContent.test.tsx.snap
index 3701769b5..75a1d7cb4 100644
--- a/frontend-new/src/experiences/report/reportPdf/components/experiencesReportContent/__snapshots__/ExperiencesReportContent.test.tsx.snap
+++ b/frontend-new/src/experiences/report/reportPdf/components/experiencesReportContent/__snapshots__/ExperiencesReportContent.test.tsx.snap
@@ -68,7 +68,7 @@ exports[`ExperiencesReportContent should render ExperiencesReportContent correct
x="0"
y="0"
>
- Top Skills:
+ Top Skills:
= ({
>
{t("features.skillsRanking.components.rotateToSolve.instructions1")}{" "}
-
+ {" "}
{t("features.skillsRanking.components.rotateToSolve.instructions2")}{" "}
.
diff --git a/frontend-new/src/features/skillsRanking/components/rotateToSolve/__snapshots__/RotateToSolvePuzzle.test.tsx.snap b/frontend-new/src/features/skillsRanking/components/rotateToSolve/__snapshots__/RotateToSolvePuzzle.test.tsx.snap
index 6914864e9..44c47a688 100644
--- a/frontend-new/src/features/skillsRanking/components/rotateToSolve/__snapshots__/RotateToSolvePuzzle.test.tsx.snap
+++ b/frontend-new/src/features/skillsRanking/components/rotateToSolve/__snapshots__/RotateToSolvePuzzle.test.tsx.snap
@@ -23,7 +23,8 @@ exports[`RotateToSolvePuzzle snapshot: initial render (non-replay) 1`] = `
d="M15.55 5.55 11 1v3.07C7.06 4.56 4 7.92 4 12s3.05 7.44 7 7.93v-2.02c-2.84-.48-5-2.94-5-5.91s2.16-5.43 5-5.91V10zM19.93 11c-.17-1.39-.72-2.73-1.62-3.89l-1.42 1.42c.54.75.88 1.6 1.02 2.47zM13 17.9v2.02c1.39-.17 2.74-.71 3.9-1.61l-1.44-1.44c-.75.54-1.59.89-2.46 1.03m3.89-2.42 1.42 1.41c.9-1.16 1.45-2.5 1.62-3.89h-2.02c-.14.87-.48 1.72-1.02 2.48"
/>
- or counterclockwise with
+
+ or counterclockwise with
- sent
+ sent
+
just now
diff --git a/frontend-new/src/features/skillsRanking/components/skillsRankingDisclosure/skillsRankingMarketDisclosure/SkillsRankingJobMarketDisclosure.tsx b/frontend-new/src/features/skillsRanking/components/skillsRankingDisclosure/skillsRankingMarketDisclosure/SkillsRankingJobMarketDisclosure.tsx
index 62a031986..005b4ad73 100644
--- a/frontend-new/src/features/skillsRanking/components/skillsRankingDisclosure/skillsRankingMarketDisclosure/SkillsRankingJobMarketDisclosure.tsx
+++ b/frontend-new/src/features/skillsRanking/components/skillsRankingDisclosure/skillsRankingMarketDisclosure/SkillsRankingJobMarketDisclosure.tsx
@@ -118,9 +118,9 @@ const SkillsRankingJobMarketDisclosure: React.FC
- {t("features.skillsRanking.components.skillsRankingDisclosure.skillsRankingMarketDisclosure.message_1")}
- {skillsRankingState.score.jobs_matching_rank}%
- {t("features.skillsRanking.components.skillsRankingDisclosure.skillsRankingMarketDisclosure.message_2")}
+ {t("features.skillsRanking.components.skillsRankingDisclosure.skillsRankingMarketDisclosure.message_1")}{" "}
+ {skillsRankingState.score.jobs_matching_rank}%{" "}
+ {t("features.skillsRanking.components.skillsRankingDisclosure.skillsRankingMarketDisclosure.message_2")}{" "}
{getJobPlatformUrl()}
{t("features.skillsRanking.components.skillsRankingDisclosure.skillsRankingMarketDisclosure.message_3")}
>
diff --git a/frontend-new/src/features/skillsRanking/components/skillsRankingDisclosure/skillsRankingMarketDisclosure/__snapshots__/SkillsRankingJobMarketDisclosure.test.tsx.snap b/frontend-new/src/features/skillsRanking/components/skillsRankingDisclosure/skillsRankingMarketDisclosure/__snapshots__/SkillsRankingJobMarketDisclosure.test.tsx.snap
index 691182054..eef4da5bf 100644
--- a/frontend-new/src/features/skillsRanking/components/skillsRankingDisclosure/skillsRankingMarketDisclosure/__snapshots__/SkillsRankingJobMarketDisclosure.test.tsx.snap
+++ b/frontend-new/src/features/skillsRanking/components/skillsRankingDisclosure/skillsRankingMarketDisclosure/__snapshots__/SkillsRankingJobMarketDisclosure.test.tsx.snap
@@ -19,12 +19,15 @@ exports[`SkillsRankingJobMarketDisclosure snapshot: non-replay initial render (n
class="MuiTypography-root MuiTypography-body1 css-1ow0s3z-MuiTypography-root"
data-testid="chat-message-bubble-message-text-6e685eeb-2b54-432a-8b66-8a81633b3981"
>
- You meet the key skills for
+ You meet the key skills for
+
42
%
- of opportunities advertised on
+
+ of opportunities advertised on
+
x
. That’s a solid range of options!
@@ -41,7 +44,8 @@ exports[`SkillsRankingJobMarketDisclosure snapshot: non-replay initial render (n
class="MuiTypography-root MuiTypography-caption css-1nk71a8-MuiTypography-root"
data-testid="chat-message-timestamp-c9253326-05ee-4bff-9d43-7852ca78a033"
>
- sent
+ sent
+
just now
diff --git a/frontend-new/src/features/skillsRanking/components/skillsRankingProofOfValue/__snapshots__/SkillsRankingProofOfValue.test.tsx.snap b/frontend-new/src/features/skillsRanking/components/skillsRankingProofOfValue/__snapshots__/SkillsRankingProofOfValue.test.tsx.snap
index cfcc0a7d5..dc835bb62 100644
--- a/frontend-new/src/features/skillsRanking/components/skillsRankingProofOfValue/__snapshots__/SkillsRankingProofOfValue.test.tsx.snap
+++ b/frontend-new/src/features/skillsRanking/components/skillsRankingProofOfValue/__snapshots__/SkillsRankingProofOfValue.test.tsx.snap
@@ -55,7 +55,8 @@ exports[`SkillsRankingProofOfValue snapshot: work-based initial render (non-repl
class="MuiTypography-root MuiTypography-caption css-1nk71a8-MuiTypography-root"
data-testid="chat-message-timestamp-c9253326-05ee-4bff-9d43-7852ca78a033"
>
- sent
+ sent
+
just now
diff --git a/frontend-new/src/features/skillsRanking/components/skillsRankingRetypedRank/SkillsRankingRetypedRank.tsx b/frontend-new/src/features/skillsRanking/components/skillsRankingRetypedRank/SkillsRankingRetypedRank.tsx
index 6f7373f36..c2bd3247c 100644
--- a/frontend-new/src/features/skillsRanking/components/skillsRankingRetypedRank/SkillsRankingRetypedRank.tsx
+++ b/frontend-new/src/features/skillsRanking/components/skillsRankingRetypedRank/SkillsRankingRetypedRank.tsx
@@ -144,8 +144,8 @@ const SkillsRankingRetypedRank: React.FC
message={
<>
{t("features.skillsRanking.components.skillsRankingRetypedRank.question_1")}{" "}
- {t("features.skillsRanking.components.skillsRankingRetypedRank.question_2")}
- {t("features.skillsRanking.components.skillsRankingRetypedRank.question_3")} {getJobPlatformUrl()}
+ {t("features.skillsRanking.components.skillsRankingRetypedRank.question_2")}{" "}
+ {t("features.skillsRanking.components.skillsRankingRetypedRank.question_3")} {getJobPlatformUrl()}{" "}
{t("features.skillsRanking.components.skillsRankingRetypedRank.question_4")}
>
}
diff --git a/frontend-new/src/features/skillsRanking/components/skillsRankingRetypedRank/__snapshots__/SkillsRankingRetypedRank.test.tsx.snap b/frontend-new/src/features/skillsRanking/components/skillsRankingRetypedRank/__snapshots__/SkillsRankingRetypedRank.test.tsx.snap
index 7f34cac7a..575ddb363 100644
--- a/frontend-new/src/features/skillsRanking/components/skillsRankingRetypedRank/__snapshots__/SkillsRankingRetypedRank.test.tsx.snap
+++ b/frontend-new/src/features/skillsRanking/components/skillsRankingRetypedRank/__snapshots__/SkillsRankingRetypedRank.test.tsx.snap
@@ -24,10 +24,12 @@ exports[`SkillsRankingRetypedRank snapshot: initial render in RETYPED_RANK (non-
check again what I said three messages ago, how many percent of opportunities
- on
+
+ on
x
- do you meet the key skills for?
+
+ do you meet the key skills for?
- sent
+ sent
+
just now
diff --git a/frontend-new/src/i18n/locales/en-GB/translation.json b/frontend-new/src/i18n/locales/en-GB/translation.json
index c1136b22a..bb893a4cd 100644
--- a/frontend-new/src/i18n/locales/en-GB/translation.json
+++ b/frontend-new/src/i18n/locales/en-GB/translation.json
@@ -137,7 +137,7 @@
"feedbackInProgressLinkText": "Complete your Feedback",
"feedbackInProgressSuffix": "to help us improve your experience!"
},
- "sent": "sent "
+ "sent": "sent"
},
"chat": {
"notifications": {
@@ -262,7 +262,7 @@
"loginUsing": "Login using",
"loggingYouIn": "Logging you in...",
"loggingInAria": "Logging in",
- "dontHaveAnAccount": "Don't have an account? ",
+ "dontHaveAnAccount": "Don't have an account?",
"fillInEmailAndPassword": "Please fill in the email and password fields",
"orLoginToYourAccountToContinue": "Or login to your account to continue",
"or": "or",
@@ -532,11 +532,11 @@
"unpaidWorkTitle": "Unpaid Work",
"traineeWorkTitle": "Trainee Work",
"uncategorizedTitle": "Uncategorized",
- "topSkillsTitle": "Top Skills: ",
+ "topSkillsTitle": "Top Skills:",
"skillsDescriptionText": "Below, you will find a list of the skills discovered during your conversation with {{appName}}, along with their descriptions.",
"disclaimer": {
- "part1": "Disclaimer: ",
- "part2": "Listed skills are based on a conversation with the candidate, are not verified or validated by Tabiya, and may be inaccurate. ",
+ "part1": "Disclaimer:",
+ "part2": "Listed skills are based on a conversation with the candidate, are not verified or validated by Tabiya, and may be inaccurate.",
"part3": "Information should be checked before use for job search, job interviews, or for creating a CV. To revise this information, speak with {{appName}} again or create a complete CV based on this report."
},
"bodyText": "This report summarizes the key information gathered during a conversation with {{appName}} on {{date}}. {{appName}} is an AI chatbot that assists job-seekers in exploring their skills and experiences. This report presents the candidate’s work experience and the skills identified from each experience. This information can be used to guide job search and highlight their skills when applying for jobs, especially during interviews with potential employers. It can be a good starting point for creating a complete CV."
@@ -564,8 +564,8 @@
"skillsRankingRetypedRank": {
"question_1": "As a last question, let's recall what I told you further above just regarding your own fit with opportunities:",
"question_2": "check again what I said three messages ago, how many percent of opportunities",
- "question_3": " on",
- "question_4": " do you meet the key skills for?",
+ "question_3": "on",
+ "question_4": "do you meet the key skills for?",
"sliderAria": "Retyped rank percentile slider"
},
"skillsRankingPerceivedRank": {
@@ -575,25 +575,25 @@
},
"skillsRankingDisclosure": {
"skillsRankingMarketDisclosure": {
- "message_1": "You meet the key skills for ",
- "message_2": " of opportunities advertised on ",
+ "message_1": "You meet the key skills for",
+ "message_2": "of opportunities advertised on",
"message_3": ". That’s a solid range of options!"
},
"skillsRankingJobSeekerDisclosure": {
"pendingMessage": "Thanks! We're double-checking the latest {{jobPlatformUrl}} opportunities so the numbers are accurate. We'll share your results soon or you can ask for them when we call you for the phone survey.",
"comparisonPart1_prefix": "Moreover, ",
"comparisonPart1_main": "Compared to other \n {{jobPlatformUrl}}\n users, you are in group [\n {{groupIndex}}\n ] of \n {{groupTotal}}\n .",
- "comparisonPart2_prefix": "Imagine lining up 100 \n {{jobPlatformUrl}}\n users from the fewest to the most jobs they fit. We cut the line into five blocks of 20 people. Block 1 (highest 20) fit the most jobs; block 5 (lowest 20) fit the fewest. You’re in block ",
+ "comparisonPart2_prefix": "Imagine lining up 100 \n {{jobPlatformUrl}}\n users from the fewest to the most jobs they fit. We cut the line into five blocks of 20 people. Block 1 (highest 20) fit the most jobs; block 5 (lowest 20) fit the fewest. You’re in block",
"comparisonPart2_group": "[\n {{groupIndex}}\n ]",
"comparisonPart2_middle": ", which is the ",
"comparisonPart2_label": "[\n {{comparisonLabel}}\n ]",
- "comparisonPart2_suffix": " block.",
+ "comparisonPart2_suffix": "block.",
"updateError": "Failed to update skills ranking state. Please try again later."
}
},
"rotateToSolve": {
"instructions1": "Rotate each letter until it’s upright. Select a letter by clicking on it, then rotate it clockwise with",
- "instructions2": " or counterclockwise with",
+ "instructions2": "or counterclockwise with",
"allCompleteMessage": "All puzzles complete! Well done!",
"puzzleCompleteMessage": "Puzzle complete! Please solve another one or cancel if you are not that interested in the information.",
"rotateLeftAria": "Rotate character counterclockwise",
@@ -646,7 +646,7 @@
"collectionSkipped": "Personal data collection skipped.",
"rejectParagraph1": "We're sorry that you chose not to provide your data. Providing this information helps {{appName}} deliver a more personalized experience for you. You will not be able to proceed and will be logged out.",
"skipParagraph1": "We're sorry that you prefer not to provide your data. Providing this information helps {{appName}} deliver a more personalized experience for you. Please note that if you skip this step,",
- "skipParagraph1Highlighted": " you won't be able to provide this information later.",
+ "skipParagraph1Highlighted": "you won't be able to provide this information later.",
"startConversation": "Start conversation",
"areYouSureYouWantToSkip": "Are you sure you want to skip?",
"yesSkip": "Yes, skip",
diff --git a/frontend-new/src/i18n/locales/en-US/translation.json b/frontend-new/src/i18n/locales/en-US/translation.json
index f48745dc8..ce60d51f8 100644
--- a/frontend-new/src/i18n/locales/en-US/translation.json
+++ b/frontend-new/src/i18n/locales/en-US/translation.json
@@ -178,7 +178,7 @@
"feedbackInProgressLinkText": "Complete your Feedback",
"feedbackInProgressSuffix": "to help us improve your experience!"
},
- "sent": "sent "
+ "sent": "sent"
},
"chat": {
"notifications": {
@@ -262,7 +262,7 @@
"loginUsing": "Login using",
"loggingYouIn": "Logging you in...",
"loggingInAria": "Logging in",
- "dontHaveAnAccount": "Don't have an account? ",
+ "dontHaveAnAccount": "Don't have an account?",
"fillInEmailAndPassword": "Please fill in the email and password fields",
"orLoginToYourAccountToContinue": "Or login to your account to continue",
"or": "or",
@@ -532,11 +532,11 @@
"unpaidWorkTitle": "Unpaid Work",
"traineeWorkTitle": "Trainee Work",
"uncategorizedTitle": "Uncategorized",
- "topSkillsTitle": "Top Skills: ",
+ "topSkillsTitle": "Top Skills:",
"skillsDescriptionText": "Below, you will find a list of the skills discovered during your conversation with {{appName}}, along with their descriptions.",
"disclaimer": {
- "part1": "Disclaimer: ",
- "part2": "Listed skills are based on a conversation with the candidate, are not verified or validated by Tabiya, and may be inaccurate. ",
+ "part1": "Disclaimer:",
+ "part2": "Listed skills are based on a conversation with the candidate, are not verified or validated by Tabiya, and may be inaccurate.",
"part3": "Information should be checked before use for job search, job interviews, or for creating a CV. To revise this information, speak with {{appName}} again or create a complete CV based on this report."
},
"bodyText": "This report summarizes the key information gathered during a conversation with {{appName}} on {{date}}. {{appName}} is an AI chatbot that assists job-seekers in exploring their skills and experiences. This report presents the candidate’s work experience and the skills identified from each experience. This information can be used to guide job search and highlight their skills when applying for jobs, especially during interviews with potential employers. It can be a good starting point for creating a complete CV."
@@ -564,8 +564,8 @@
"skillsRankingRetypedRank": {
"question_1": "As a last question, let's recall what I told you further above just regarding your own fit with opportunities:",
"question_2": "check again what I said three messages ago, how many percent of opportunities",
- "question_3": " on",
- "question_4": " do you meet the key skills for?",
+ "question_3": "on",
+ "question_4": "do you meet the key skills for?",
"sliderAria": "Retyped rank percentile slider"
},
"skillsRankingPerceivedRank": {
@@ -575,25 +575,25 @@
},
"skillsRankingDisclosure": {
"skillsRankingMarketDisclosure": {
- "message_1": "You meet the key skills for ",
- "message_2": " of opportunities advertised on ",
+ "message_1": "You meet the key skills for",
+ "message_2": "of opportunities advertised on",
"message_3": ". That’s a solid range of options!"
},
"skillsRankingJobSeekerDisclosure": {
"pendingMessage": "Thanks! We're double-checking the latest {{jobPlatformUrl}} opportunities so the numbers are accurate. We'll share your results soon or you can ask for them when we call you for the phone survey.",
"comparisonPart1_prefix": "Moreover, ",
"comparisonPart1_main": "Compared to other \n{{jobPlatformUrl}}\n users, you are in group [\n{{groupIndex}}\n] of \n{{groupTotal}}\n.",
- "comparisonPart2_prefix": "Imagine lining up 100 \n{{jobPlatformUrl}}\n users from the fewest to the most jobs they fit. We cut the line into five blocks of 20 people. Block 1 (highest 20) fit the most jobs; block 5 (lowest 20) fit the fewest. You're in block ",
+ "comparisonPart2_prefix": "Imagine lining up 100 \n{{jobPlatformUrl}}\n users from the fewest to the most jobs they fit. We cut the line into five blocks of 20 people. Block 1 (highest 20) fit the most jobs; block 5 (lowest 20) fit the fewest. You're in block",
"comparisonPart2_group": "[\n{{groupIndex}}\n]",
"comparisonPart2_middle": ", which is the ",
"comparisonPart2_label": "[\n{{comparisonLabel}}\n]",
- "comparisonPart2_suffix": " block.",
+ "comparisonPart2_suffix": "block.",
"updateError": "Failed to update skills ranking state. Please try again later."
}
},
"rotateToSolve": {
"instructions1": "Rotate each letter until it’s upright. Select a letter by clicking on it, then rotate it clockwise with",
- "instructions2": " or counterclockwise with",
+ "instructions2": "or counterclockwise with",
"allCompleteMessage": "All puzzles complete! Well done!",
"puzzleCompleteMessage": "Puzzle complete! Please solve another one or cancel if you are not that interested in the information.",
"rotateLeftAria": "Rotate character counterclockwise",
@@ -646,7 +646,7 @@
"collectionSkipped": "Personal data collection skipped.",
"rejectParagraph1": "We're sorry that you chose not to provide your data. Providing this information helps {{appName}} deliver a more personalized experience for you. You will not be able to proceed and will be logged out.",
"skipParagraph1": "We're sorry that you prefer not to provide your data. Providing this information helps {{appName}} deliver a more personalized experience for you. Please note that if you skip this step,",
- "skipParagraph1Highlighted": " you won't be able to provide this information later.",
+ "skipParagraph1Highlighted": "you won't be able to provide this information later.",
"startConversation": "Start conversation",
"areYouSureYouWantToSkip": "Are you sure you want to skip?",
"yesSkip": "Yes, skip",
diff --git a/frontend-new/src/i18n/locales/es-AR/translation.json b/frontend-new/src/i18n/locales/es-AR/translation.json
index 064b3106f..3772a57f5 100644
--- a/frontend-new/src/i18n/locales/es-AR/translation.json
+++ b/frontend-new/src/i18n/locales/es-AR/translation.json
@@ -137,7 +137,7 @@
"feedbackInProgressLinkText": "Completa tus comentarios",
"feedbackInProgressSuffix": "para ayudarnos a mejorar tu experiencia!"
},
- "sent": "enviado "
+ "sent": "enviado"
},
"chat": {
"notifications": {
@@ -532,11 +532,11 @@
"unpaidWorkTitle": "Trabajo no remunerado",
"traineeWorkTitle": "Trabajo como aprendiz",
"uncategorizedTitle": "Sin categoría",
- "topSkillsTitle": "Habilidades principales: ",
+ "topSkillsTitle": "Habilidades principales:",
"skillsDescriptionText": "A continuación vas a encontrar una lista de las habilidades descubiertas durante tu conversación con {{appName}}, junto con sus descripciones.",
"disclaimer": {
- "part1": "Aviso: ",
- "part2": "Las habilidades listadas se basan en una conversación con la persona candidata, no han sido verificadas ni validadas por Tabiya y pueden ser inexactas. ",
+ "part1": "Aviso:",
+ "part2": "Las habilidades listadas se basan en una conversación con la persona candidata, no han sido verificadas ni validadas por Tabiya y pueden ser inexactas.",
"part3": "La información debe ser verificada antes de usarla para la búsqueda de empleo, entrevistas o para crear un CV. Para revisar esta información, volvé a hablar con {{appName}} o creá un CV completo basado en este informe."
},
"bodyText": "Este informe resume la información clave recopilada durante una conversación con {{appName}} el {{date}}. {{appName}} es un chatbot de IA que ayuda a las personas que buscan empleo a explorar sus habilidades y experiencias. Este informe presenta la experiencia laboral de la persona candidata y las habilidades identificadas en cada experiencia. Esta información puede usarse para guiar la búsqueda de empleo y resaltar sus habilidades al postular, especialmente durante entrevistas con posibles empleadores. Puede ser un buen punto de partida para crear un CV completo."
@@ -564,8 +564,8 @@
"skillsRankingRetypedRank": {
"question_1": "Última pregunta: recordemos lo que te dije más arriba sobre tu encaje con las oportunidades:",
"question_2": "volvé a mirar lo que dije tres mensajes atrás, qué porcentaje de oportunidades",
- "question_3": " en",
- "question_4": " cumplís con las habilidades clave?",
+ "question_3": "en",
+ "question_4": "cumplís con las habilidades clave?",
"sliderAria": "Slider percentil de rango reescrito"
},
"skillsRankingPerceivedRank": {
@@ -575,15 +575,15 @@
},
"skillsRankingDisclosure": {
"skillsRankingMarketDisclosure": {
- "message_1": "Cumplís las habilidades clave para el ",
- "message_2": " de las oportunidades publicadas en ",
+ "message_1": "Cumplís las habilidades clave para el",
+ "message_2": "de las oportunidades publicadas en",
"message_3": ". ¡Es un buen rango de opciones!"
},
"skillsRankingJobSeekerDisclosure": {
"pendingMessage": "¡Gracias! Estamos verificando las últimas oportunidades de {{jobPlatformUrl}} para que los números sean precisos. Compartiremos tus resultados pronto o podés preguntar por ellos cuando te llamemos para la encuesta telefónica.",
"comparisonPart1_prefix": "Además, ",
"comparisonPart1_main": "Comparado con otros usuarios de \n{{jobPlatformUrl}}\n, estás en el grupo [\n{{groupIndex}}\n] de \n{{groupTotal}}\n.",
- "comparisonPart2_prefix": "Imaginá alinear a 100 usuarios de \n{{jobPlatformUrl}}\n desde quienes encajan con menos hasta quienes encajan con más puestos. Dividimos la fila en cinco bloques de 20. El bloque 1 (los 20 más altos) encaja con más puestos; el bloque 5 (los 20 más bajos) con menos. Estás en el bloque ",
+ "comparisonPart2_prefix": "Imaginá alinear a 100 usuarios de \n{{jobPlatformUrl}}\n desde quienes encajan con menos hasta quienes encajan con más puestos. Dividimos la fila en cinco bloques de 20. El bloque 1 (los 20 más altos) encaja con más puestos; el bloque 5 (los 20 más bajos) con menos. Estás en el bloque",
"comparisonPart2_group": "[\n{{groupIndex}}\n]",
"comparisonPart2_middle": ", que es el bloque ",
"comparisonPart2_label": "[\n{{comparisonLabel}}\n]",
@@ -593,7 +593,7 @@
},
"rotateToSolve": {
"instructions1": "Girá cada letra hasta que quede derecha. Seleccioná una letra haciendo clic y después girala en sentido horario con",
- "instructions2": " o antihorario con.",
+ "instructions2": "o antihorario con.",
"allCompleteMessage": "¡Todos los rompecabezas completos! ¡Bien hecho!",
"puzzleCompleteMessage": "¡Rompecabezas completo! Resolvé otro o cancelá si no te interesa tanto la información.",
"rotateLeftAria": "Rotar carácter en sentido antihorario",
@@ -646,7 +646,7 @@
"collectionSkipped": "Se omitió la recopilación de datos personales.",
"rejectParagraph1": "Lamentamos que hayas decidido no proporcionar tus datos. Proporcionar esta información ayuda a {{appName}} a ofrecerte una experiencia más personalizada. No podrás continuar y se cerrará tu sesión.",
"skipParagraph1": "Lamentamos que prefieras no proporcionar tus datos. Proporcionar esta información ayuda a {{appName}} a ofrecerte una experiencia más personalizada. Ten en cuenta que si omites este paso,",
- "skipParagraph1Highlighted": " no podrás proporcionar esta información más adelante.",
+ "skipParagraph1Highlighted": "no podrás proporcionar esta información más adelante.",
"startConversation": "Iniciar conversación",
"areYouSureYouWantToSkip": "¿Estás seguro de que quieres omitirlo?",
"yesSkip": "Sí, omitir",
diff --git a/frontend-new/src/i18n/locales/es-ES/translation.json b/frontend-new/src/i18n/locales/es-ES/translation.json
index becf02fcc..c74b25bfe 100644
--- a/frontend-new/src/i18n/locales/es-ES/translation.json
+++ b/frontend-new/src/i18n/locales/es-ES/translation.json
@@ -137,7 +137,7 @@
"feedbackInProgressLinkText": "Completa tus comentarios",
"feedbackInProgressSuffix": "para ayudarnos a mejorar tu experiencia!"
},
- "sent": "enviado "
+ "sent": "enviado"
},
"chat": {
"notifications": {
@@ -532,11 +532,11 @@
"unpaidWorkTitle": "Trabajo no remunerado",
"traineeWorkTitle": "Trabajo como aprendiz",
"uncategorizedTitle": "Sin categoría",
- "topSkillsTitle": "Habilidades principales: ",
+ "topSkillsTitle": "Habilidades principales:",
"skillsDescriptionText": "A continuación encontrarás una lista de las habilidades descubiertas durante tu conversación con {{appName}}, junto con sus descripciones.",
"disclaimer": {
- "part1": "Aviso: ",
- "part2": "Las habilidades listadas se basan en una conversación con el candidato, no han sido verificadas o validadas por Tabiya y pueden ser inexactas. ",
+ "part1": "Aviso:",
+ "part2": "Las habilidades listadas se basan en una conversación con el candidato, no han sido verificadas o validadas por Tabiya y pueden ser inexactas.",
"part3": "La información debe ser verificada antes de usarla para la búsqueda de empleo, entrevistas o para crear un CV. Para revisar esta información, vuelve a hablar con {{appName}} o crea un CV completo basado en este informe."
},
"bodyText": "Este informe resume la información clave recopilada durante una conversación con {{appName}} el {{date}}. {{appName}} es un chatbot de IA que ayuda a los buscadores de empleo a explorar sus habilidades y experiencias. Este informe presenta la experiencia laboral del candidato y las habilidades identificadas en cada experiencia. Esta información puede usarse para guiar la búsqueda de empleo y resaltar sus habilidades al postular, especialmente durante entrevistas con posibles empleadores. Puede ser un buen punto de partida para crear un CV completo."
@@ -564,8 +564,8 @@
"skillsRankingRetypedRank": {
"question_1": "Última pregunta: recordemos lo que te dije más arriba sobre tu propio encaje con las oportunidades:",
"question_2": "vuelve a revisar lo que dije tres mensajes atrás, qué porcentaje de oportunidades",
- "question_3": " en",
- "question_4": " cumples con las habilidades clave?",
+ "question_3": "en",
+ "question_4": "cumples con las habilidades clave?",
"sliderAria": "Control deslizante percentil de rango reescrito"
},
"skillsRankingPerceivedRank": {
@@ -575,15 +575,15 @@
},
"skillsRankingDisclosure": {
"skillsRankingMarketDisclosure": {
- "message_1": "Cumples las habilidades clave para el ",
- "message_2": " de las oportunidades publicadas en ",
+ "message_1": "Cumples las habilidades clave para el",
+ "message_2": "de las oportunidades publicadas en",
"message_3": ". ¡Es un rango sólido de opciones!"
},
"skillsRankingJobSeekerDisclosure": {
"pendingMessage": "¡Gracias! Estamos verificando las últimas oportunidades de {{jobPlatformUrl}} para que los números sean precisos. Compartiremos tus resultados pronto o puedes preguntar por ellos cuando te llamemos para la encuesta telefónica.",
"comparisonPart1_prefix": "Además, ",
"comparisonPart1_main": "Comparado con otros usuarios de \n{{jobPlatformUrl}}\n, estás en el grupo [\n{{groupIndex}}\n] de \n{{groupTotal}}\n.",
- "comparisonPart2_prefix": "Imagina alinear a 100 usuarios de \n{{jobPlatformUrl}}\n desde los que encajan con menos hasta los que encajan con más puestos. Dividimos la fila en cinco bloques de 20 personas. El bloque 1 (los 20 más altos) encaja con más puestos; el bloque 5 (los 20 más bajos) con menos. Estás en el bloque ",
+ "comparisonPart2_prefix": "Imagina alinear a 100 usuarios de \n{{jobPlatformUrl}}\n desde los que encajan con menos hasta los que encajan con más puestos. Dividimos la fila en cinco bloques de 20 personas. El bloque 1 (los 20 más altos) encaja con más puestos; el bloque 5 (los 20 más bajos) con menos. Estás en el bloque",
"comparisonPart2_group": "[\n{{groupIndex}}\n]",
"comparisonPart2_middle": ", que es el bloque ",
"comparisonPart2_label": "[\n{{comparisonLabel}}\n]",
@@ -593,7 +593,7 @@
},
"rotateToSolve": {
"instructions1": "Gira cada letra hasta que quede derecha. Selecciona una letra haciendo clic y luego gírala en sentido horario con",
- "instructions2": " o antihorario con.",
+ "instructions2": "o antihorario con.",
"allCompleteMessage": "¡Todos los rompecabezas completados! ¡Bien hecho!",
"puzzleCompleteMessage": "¡Rompecabezas completado! Resuelve otro o cancela si no te interesa tanto la información.",
"rotateLeftAria": "Rotar carácter en sentido antihorario",
@@ -646,7 +646,7 @@
"collectionSkipped": "Se omitió la recopilación de datos personales.",
"rejectParagraph1": "Lamentamos que hayas decidido no proporcionar tus datos. Proporcionar esta información ayuda a {{appName}} a ofrecerte una experiencia más personalizada. No podrás continuar y se cerrará tu sesión.",
"skipParagraph1": "Lamentamos que prefieras no proporcionar tus datos. Proporcionar esta información ayuda a {{appName}} a ofrecerte una experiencia más personalizada. Ten en cuenta que si omites este paso,",
- "skipParagraph1Highlighted": " no podrás proporcionar esta información más adelante.",
+ "skipParagraph1Highlighted": "no podrás proporcionar esta información más adelante.",
"startConversation": "Iniciar conversación",
"areYouSureYouWantToSkip": "¿Estás seguro de que quieres omitirlo?",
"yesSkip": "Sí, omitir",