diff --git a/Apis/api/Services/EmailApiEmailSender.cs b/Apis/api/Services/EmailApiEmailSender.cs
index a886ac6..f44ec06 100644
--- a/Apis/api/Services/EmailApiEmailSender.cs
+++ b/Apis/api/Services/EmailApiEmailSender.cs
@@ -138,7 +138,7 @@ public sealed class EmailApiEmailSender : IEmailSender
| IP Address |
- {userIp ?? "Unknown"} |
+ {userIp ?? _emailTemplates.Get("email.notification.unknown-ip", "en")} |
""";
@@ -215,8 +215,8 @@ public sealed class EmailApiEmailSender : IEmailSender
// email.match.body is now stored as HTML in the database
var body = _emailTemplates.Render("email.match.body", language,
("cvDocumentId", cvDocumentId),
- ("jobLabel", jobLabel ?? "N/A"),
- ("jobUrl", result.JobUrl ?? "N/A"),
+ ("jobLabel", jobLabel ?? _emailTemplates.Get("email.match.fallback-na", language)),
+ ("jobUrl", result.JobUrl ?? _emailTemplates.Get("email.match.fallback-na", language)),
("score", result.Score.ToString()),
("summary", WebUtility.HtmlEncode(result.Summary ?? string.Empty)),
("strengths", strengths),
@@ -238,7 +238,7 @@ public sealed class EmailApiEmailSender : IEmailSender
public string BuildMatchEmailSubject(int score, string? jobLabel, string language) =>
_emailTemplates.Render("email.match.subject", language,
("score", score.ToString()),
- ("jobLabel", jobLabel ?? "Job"));
+ ("jobLabel", jobLabel ?? _emailTemplates.Get("email.match.subject-fallback-label", language)));
public string GetManualJobLabel(string language) =>
_emailTemplates.Get("email.match.manual-job-label", language);
diff --git a/Apis/cv-matcher-api/Services/CvMatcherService.cs b/Apis/cv-matcher-api/Services/CvMatcherService.cs
index 65b7327..d55a219 100644
--- a/Apis/cv-matcher-api/Services/CvMatcherService.cs
+++ b/Apis/cv-matcher-api/Services/CvMatcherService.cs
@@ -141,7 +141,9 @@ public sealed class CvMatcherService : ICvMatcherService
""";
var json = await _ai.CreateChatCompletionAsync(systemPrompt, userPrompt, 0.2m, ct);
- var result = ParseResult(json);
+ var errorSummary = await _aiPrompts.GetAsync("parse-error.summary", language, ct);
+ var errorRec = await _aiPrompts.GetAsync("parse-error.recommendation", language, ct);
+ var result = ParseResult(json, errorSummary, errorRec);
result.JobDocumentId = job.Id;
result.JobUrl = job.SourceUrl;
result.Cached = false;
@@ -153,7 +155,10 @@ public sealed class CvMatcherService : ICvMatcherService
/// Deserialises the LLM's JSON output into a .
/// Returns a safe fallback response instead of throwing when the JSON cannot be parsed.
///
- private static JobMatchResponse ParseResult(string json)
+ private static JobMatchResponse ParseResult(
+ string json,
+ string? errorSummary = null,
+ string? errorRec = null)
{
try
{
@@ -168,8 +173,8 @@ public sealed class CvMatcherService : ICvMatcherService
return new JobMatchResponse
{
Score = 0,
- Summary = "The AI response could not be parsed as structured JSON.",
- Recommendations = ["Inspect the raw model output and tune the scoring prompt."]
+ Summary = errorSummary ?? "The AI response could not be parsed as structured JSON.",
+ Recommendations = [errorRec ?? "Inspect the raw model output and tune the scoring prompt."]
};
}
diff --git a/Apis/cv-matcher-data/Migrations/20260608193046_AddParseErrorPrompts.Designer.cs b/Apis/cv-matcher-data/Migrations/20260608193046_AddParseErrorPrompts.Designer.cs
new file mode 100644
index 0000000..2abb33f
--- /dev/null
+++ b/Apis/cv-matcher-data/Migrations/20260608193046_AddParseErrorPrompts.Designer.cs
@@ -0,0 +1,138 @@
+//
+using System;
+using CvMatcher.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace CvMatcher.Data.Migrations
+{
+ [DbContext(typeof(CvMatcherDbContext))]
+ [Migration("20260608193046_AddParseErrorPrompts")]
+ partial class AddParseErrorPrompts
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasDefaultSchema("cvMatcher")
+ .HasAnnotation("ProductVersion", "10.0.7")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("CvMatcher.Data.Entities.AiPromptEntity", b =>
+ {
+ b.Property("Key")
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("Language")
+ .HasMaxLength(8)
+ .HasColumnType("nvarchar(8)");
+
+ b.Property("Description")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)")
+ .HasDefaultValue("");
+
+ b.Property("UpdatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Key", "Language");
+
+ b.ToTable("AiPrompts", "cvMatcher");
+ });
+
+ modelBuilder.Entity("CvMatcher.Data.Entities.CvMatchResultEntity", b =>
+ {
+ b.Property("Id")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("ClientIpAddress")
+ .HasMaxLength(45)
+ .HasColumnType("nvarchar(45)");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.Property("CvDocumentId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("Email")
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("JobDocumentId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("Language")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("ResultJson")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Score")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CvDocumentId", "JobDocumentId", "Language")
+ .IsUnique();
+
+ b.ToTable("Results", "cvMatcher");
+ });
+
+ modelBuilder.Entity("CvMatcher.Data.Entities.CvMatcherChatCacheEntity", b =>
+ {
+ b.Property("CacheKey")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.Property("Model")
+ .IsRequired()
+ .HasMaxLength(120)
+ .HasColumnType("nvarchar(120)");
+
+ b.Property("ResponseText")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Temperature")
+ .HasColumnType("decimal(4,2)");
+
+ b.HasKey("CacheKey");
+
+ b.ToTable("ChatCache", "cvMatcher");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Apis/cv-matcher-data/Migrations/20260608193046_AddParseErrorPrompts.cs b/Apis/cv-matcher-data/Migrations/20260608193046_AddParseErrorPrompts.cs
new file mode 100644
index 0000000..d1b15bb
--- /dev/null
+++ b/Apis/cv-matcher-data/Migrations/20260608193046_AddParseErrorPrompts.cs
@@ -0,0 +1,67 @@
+using CvMatcher.Data;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace CvMatcher.Data.Migrations
+{
+ ///
+ public partial class AddParseErrorPrompts : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.InsertData(
+ schema: MigrationConstants.SchemaName,
+ table: "AiPrompts",
+ columns: ["Key", "Language", "Value", "Description"],
+ values: ["parse-error.summary", "en", "The AI response could not be parsed. Please try again.", "Summary shown in match email when the AI returns an unparseable response"]);
+
+ migrationBuilder.InsertData(
+ schema: MigrationConstants.SchemaName,
+ table: "AiPrompts",
+ columns: ["Key", "Language", "Value", "Description"],
+ values: ["parse-error.summary", "ro", "Răspunsul AI nu a putut fi interpretat. Vă rugăm să încercați din nou.", "Sumar afișat în emailul de potrivire când AI returnează un răspuns care nu poate fi interpretat"]);
+
+ migrationBuilder.InsertData(
+ schema: MigrationConstants.SchemaName,
+ table: "AiPrompts",
+ columns: ["Key", "Language", "Value", "Description"],
+ values: ["parse-error.recommendation", "en", "If the problem persists, try a different job link or description.", "Recommendation shown in match email when the AI returns an unparseable response"]);
+
+ migrationBuilder.InsertData(
+ schema: MigrationConstants.SchemaName,
+ table: "AiPrompts",
+ columns: ["Key", "Language", "Value", "Description"],
+ values: ["parse-error.recommendation", "ro", "Dacă problema persistă, încercați un alt link sau descriere de job.", "Recomandare afișată în emailul de potrivire când AI returnează un răspuns care nu poate fi interpretat"]);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DeleteData(
+ schema: MigrationConstants.SchemaName,
+ table: "AiPrompts",
+ keyColumns: ["Key", "Language"],
+ keyValues: ["parse-error.summary", "en"]);
+
+ migrationBuilder.DeleteData(
+ schema: MigrationConstants.SchemaName,
+ table: "AiPrompts",
+ keyColumns: ["Key", "Language"],
+ keyValues: ["parse-error.summary", "ro"]);
+
+ migrationBuilder.DeleteData(
+ schema: MigrationConstants.SchemaName,
+ table: "AiPrompts",
+ keyColumns: ["Key", "Language"],
+ keyValues: ["parse-error.recommendation", "en"]);
+
+ migrationBuilder.DeleteData(
+ schema: MigrationConstants.SchemaName,
+ table: "AiPrompts",
+ keyColumns: ["Key", "Language"],
+ keyValues: ["parse-error.recommendation", "ro"]);
+ }
+ }
+}
diff --git a/Apis/email-data/Migrations/20260608192938_AddFallbackStringTemplates.Designer.cs b/Apis/email-data/Migrations/20260608192938_AddFallbackStringTemplates.Designer.cs
new file mode 100644
index 0000000..be94fc7
--- /dev/null
+++ b/Apis/email-data/Migrations/20260608192938_AddFallbackStringTemplates.Designer.cs
@@ -0,0 +1,69 @@
+//
+using System;
+using Email.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Email.Data.Migrations
+{
+ [DbContext(typeof(EmailDbContext))]
+ [Migration("20260608192938_AddFallbackStringTemplates")]
+ partial class AddFallbackStringTemplates
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasDefaultSchema("email")
+ .HasAnnotation("ProductVersion", "10.0.7")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("Email.Data.Entities.EmailTemplateEntity", b =>
+ {
+ b.Property("Key")
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("Language")
+ .HasMaxLength(8)
+ .HasColumnType("nvarchar(8)");
+
+ b.Property("Description")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)")
+ .HasDefaultValue("");
+
+ b.Property("OperatorCopy")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)")
+ .HasDefaultValue("");
+
+ b.Property("UpdatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Key", "Language");
+
+ b.ToTable("Templates", "email");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Apis/email-data/Migrations/20260608192938_AddFallbackStringTemplates.cs b/Apis/email-data/Migrations/20260608192938_AddFallbackStringTemplates.cs
new file mode 100644
index 0000000..a30b904
--- /dev/null
+++ b/Apis/email-data/Migrations/20260608192938_AddFallbackStringTemplates.cs
@@ -0,0 +1,163 @@
+using Email.Data;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Email.Data.Migrations
+{
+ ///
+ public partial class AddFallbackStringTemplates : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.InsertData(
+ schema: MigrationConstants.SchemaName,
+ table: "Templates",
+ columns: ["Key", "Language", "Value", "Description"],
+ values: ["email.match.fallback-na", "en", "N/A", "Fallback when a match email field (job label or URL) has no value"]);
+
+ migrationBuilder.InsertData(
+ schema: MigrationConstants.SchemaName,
+ table: "Templates",
+ columns: ["Key", "Language", "Value", "Description"],
+ values: ["email.match.fallback-na", "ro", "N/A", "Fallback când un câmp al emailului de potrivire (etichetă job sau URL) nu are valoare"]);
+
+ migrationBuilder.InsertData(
+ schema: MigrationConstants.SchemaName,
+ table: "Templates",
+ columns: ["Key", "Language", "Value", "Description"],
+ values: ["email.match.subject-fallback-label", "en", "Job", "Fallback job label used in match email subject when no specific label is available"]);
+
+ migrationBuilder.InsertData(
+ schema: MigrationConstants.SchemaName,
+ table: "Templates",
+ columns: ["Key", "Language", "Value", "Description"],
+ values: ["email.match.subject-fallback-label", "ro", "Job", "Etichetă fallback pentru subiectul emailului de potrivire când nu există o etichetă specifică"]);
+
+ migrationBuilder.InsertData(
+ schema: MigrationConstants.SchemaName,
+ table: "Templates",
+ columns: ["Key", "Language", "Value", "Description"],
+ values: ["email.notification.unknown-ip", "en", "Unknown", "Fallback IP address label in operator notification emails"]);
+
+ migrationBuilder.InsertData(
+ schema: MigrationConstants.SchemaName,
+ table: "Templates",
+ columns: ["Key", "Language", "Value", "Description"],
+ values: ["email.notification.unknown-ip", "ro", "Necunoscut", "Etichetă fallback pentru adresa IP în emailurile de notificare operator"]);
+
+ migrationBuilder.InsertData(
+ schema: MigrationConstants.SchemaName,
+ table: "Templates",
+ columns: ["Key", "Language", "Value", "Description"],
+ values: ["email.search-results.keywords-empty", "en", "none detected", "Text shown in job search results email when no CV keywords were extracted"]);
+
+ migrationBuilder.InsertData(
+ schema: MigrationConstants.SchemaName,
+ table: "Templates",
+ columns: ["Key", "Language", "Value", "Description"],
+ values: ["email.search-results.keywords-empty", "ro", "niciunul detectat", "Text afișat în emailul cu rezultatele căutării când nu au fost extrase cuvinte cheie din CV"]);
+
+ migrationBuilder.InsertData(
+ schema: MigrationConstants.SchemaName,
+ table: "Templates",
+ columns: ["Key", "Language", "Value", "Description"],
+ values: ["email.search-results.providers-empty", "en", "none", "Text shown in job search results email when no providers were searched"]);
+
+ migrationBuilder.InsertData(
+ schema: MigrationConstants.SchemaName,
+ table: "Templates",
+ columns: ["Key", "Language", "Value", "Description"],
+ values: ["email.search-results.providers-empty", "ro", "niciunul", "Text afișat în emailul cu rezultatele căutării când nu au fost căutați furnizori"]);
+
+ migrationBuilder.InsertData(
+ schema: MigrationConstants.SchemaName,
+ table: "Templates",
+ columns: ["Key", "Language", "Value", "Description"],
+ values: ["email.search-results.location-empty", "en", "-", "Fallback location display in job search results email scan summary"]);
+
+ migrationBuilder.InsertData(
+ schema: MigrationConstants.SchemaName,
+ table: "Templates",
+ columns: ["Key", "Language", "Value", "Description"],
+ values: ["email.search-results.location-empty", "ro", "-", "Afișaj fallback pentru locație în sumarului de scanare al emailului cu rezultatele căutării"]);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DeleteData(
+ schema: MigrationConstants.SchemaName,
+ table: "Templates",
+ keyColumns: ["Key", "Language"],
+ keyValues: ["email.match.fallback-na", "en"]);
+
+ migrationBuilder.DeleteData(
+ schema: MigrationConstants.SchemaName,
+ table: "Templates",
+ keyColumns: ["Key", "Language"],
+ keyValues: ["email.match.fallback-na", "ro"]);
+
+ migrationBuilder.DeleteData(
+ schema: MigrationConstants.SchemaName,
+ table: "Templates",
+ keyColumns: ["Key", "Language"],
+ keyValues: ["email.match.subject-fallback-label", "en"]);
+
+ migrationBuilder.DeleteData(
+ schema: MigrationConstants.SchemaName,
+ table: "Templates",
+ keyColumns: ["Key", "Language"],
+ keyValues: ["email.match.subject-fallback-label", "ro"]);
+
+ migrationBuilder.DeleteData(
+ schema: MigrationConstants.SchemaName,
+ table: "Templates",
+ keyColumns: ["Key", "Language"],
+ keyValues: ["email.notification.unknown-ip", "en"]);
+
+ migrationBuilder.DeleteData(
+ schema: MigrationConstants.SchemaName,
+ table: "Templates",
+ keyColumns: ["Key", "Language"],
+ keyValues: ["email.notification.unknown-ip", "ro"]);
+
+ migrationBuilder.DeleteData(
+ schema: MigrationConstants.SchemaName,
+ table: "Templates",
+ keyColumns: ["Key", "Language"],
+ keyValues: ["email.search-results.keywords-empty", "en"]);
+
+ migrationBuilder.DeleteData(
+ schema: MigrationConstants.SchemaName,
+ table: "Templates",
+ keyColumns: ["Key", "Language"],
+ keyValues: ["email.search-results.keywords-empty", "ro"]);
+
+ migrationBuilder.DeleteData(
+ schema: MigrationConstants.SchemaName,
+ table: "Templates",
+ keyColumns: ["Key", "Language"],
+ keyValues: ["email.search-results.providers-empty", "en"]);
+
+ migrationBuilder.DeleteData(
+ schema: MigrationConstants.SchemaName,
+ table: "Templates",
+ keyColumns: ["Key", "Language"],
+ keyValues: ["email.search-results.providers-empty", "ro"]);
+
+ migrationBuilder.DeleteData(
+ schema: MigrationConstants.SchemaName,
+ table: "Templates",
+ keyColumns: ["Key", "Language"],
+ keyValues: ["email.search-results.location-empty", "en"]);
+
+ migrationBuilder.DeleteData(
+ schema: MigrationConstants.SchemaName,
+ table: "Templates",
+ keyColumns: ["Key", "Language"],
+ keyValues: ["email.search-results.location-empty", "ro"]);
+ }
+ }
+}
diff --git a/Jobs/cv-search-job/Services/CvSearchEmailSender.cs b/Jobs/cv-search-job/Services/CvSearchEmailSender.cs
index 9ecefc6..be97c5a 100644
--- a/Jobs/cv-search-job/Services/CvSearchEmailSender.cs
+++ b/Jobs/cv-search-job/Services/CvSearchEmailSender.cs
@@ -127,13 +127,15 @@ public sealed class CvSearchEmailSender
var keywordsHtml = keywords.Count > 0
? string.Join(" ", keywords.Select(k =>
$"{k}"))
- : "none detected";
+ : $"{_emailTemplates.Get("email.search-results.keywords-empty", language)}";
var providers = providerNames.Count > 0
? string.Join(", ", providerNames)
- : "none";
+ : _emailTemplates.Get("email.search-results.providers-empty", language);
- var locationDisplay = string.IsNullOrWhiteSpace(location) ? "-" : location;
+ var locationDisplay = string.IsNullOrWhiteSpace(location)
+ ? _emailTemplates.Get("email.search-results.location-empty", language)
+ : location;
return _emailTemplates.Render("email.search-results.scan-summary", language,
("keywordsHtml", keywordsHtml),
diff --git a/web/wwwroot/js/i18n.js b/web/wwwroot/js/i18n.js
index 1e6697a..0235f81 100644
--- a/web/wwwroot/js/i18n.js
+++ b/web/wwwroot/js/i18n.js
@@ -115,7 +115,10 @@
"cv.noItems": "No items yet.",
"cv.strengths": "Strengths",
"cv.gaps": "Gaps",
- "cv.evidence": "Supporting CV excerpts"
+ "cv.evidence": "Supporting CV excerpts",
+ "error.cv_file_missing": "Missing CV PDF.",
+ "error.captcha_verification_failed": "Captcha verification failed.",
+ "error.request_cancelled": "Request was cancelled."
},
ro: {
"brand.subtitle": "prezentare inginerie AI",
@@ -224,7 +227,10 @@
"cv.noItems": "Niciun element.",
"cv.strengths": "Puncte forte",
"cv.gaps": "Lipsuri",
- "cv.evidence": "Fragmente relevante din CV"
+ "cv.evidence": "Fragmente relevante din CV",
+ "error.cv_file_missing": "Fișierul CV PDF lipsește.",
+ "error.captcha_verification_failed": "Verificarea captcha a eșuat.",
+ "error.request_cancelled": "Cererea a fost anulată."
}
};
})();
diff --git a/web/wwwroot/js/utils/form-helpers.js b/web/wwwroot/js/utils/form-helpers.js
index 85786ae..0e20503 100644
--- a/web/wwwroot/js/utils/form-helpers.js
+++ b/web/wwwroot/js/utils/form-helpers.js
@@ -52,6 +52,7 @@ function isValidEmail(value) {
*
* Rules:
* - 429 (rate limit) → return rateLimitKey translation
+ * - 4xx with known error code → look up 'error.' in i18n dictionary first
* - 4xx with error body → return server's error message (intentional feedback)
* - 5xx or no body → return fallbackKey translation
*
@@ -65,8 +66,17 @@ function extractApiError(body, status, fallbackKey, rateLimitKey) {
if (status === 429) {
return window.MyAi.t(rateLimitKey || 'form.rateLimited');
}
- var msg = body && (body.error || body.Error || body.title);
- return (status >= 400 && status < 500 && msg) ? msg : window.MyAi.t(fallbackKey);
+ if (status >= 400 && status < 500) {
+ // Prefer i18n translation keyed on the machine-readable error code
+ if (body && body.code) {
+ var codeKey = 'error.' + body.code;
+ var translated = window.MyAi.t(codeKey);
+ if (translated !== codeKey) return translated;
+ }
+ var msg = body && (body.error || body.Error || body.title);
+ if (msg) return msg;
+ }
+ return window.MyAi.t(fallbackKey);
}
// Expose helpers on window.MyAi for use by other scripts