diff --git a/Apis/api/Controllers/CvMatcherController.cs b/Apis/api/Controllers/CvMatcherController.cs index 5a5f60d..57aaeef 100644 --- a/Apis/api/Controllers/CvMatcherController.cs +++ b/Apis/api/Controllers/CvMatcherController.cs @@ -172,7 +172,7 @@ public sealed class CvMatcherController : ControllerBase var attachmentPath = TryGetCachedCvPath(request.CvDocumentId); var jobLabel = !string.IsNullOrWhiteSpace(request.JobUrl) ? request.JobUrl - : "Manual job description"; + : _emailSender.GetManualJobLabel(language); var language = NormalizeLanguage(request.Language); diff --git a/Apis/api/Services/Contracts/IEmailSender.cs b/Apis/api/Services/Contracts/IEmailSender.cs index 8fd7502..a7ccfd2 100644 --- a/Apis/api/Services/Contracts/IEmailSender.cs +++ b/Apis/api/Services/Contracts/IEmailSender.cs @@ -61,5 +61,11 @@ namespace Api.Services.Contracts /// Number of days until the job-search link expires (shown in the footer copy). /// Rendered HTML body string. string BuildMatchEmailBody(string cvDocumentId, JobMatchResponse result, string? jobLabel, string language, string? jobSearchLink = null, int expiryDays = 7); + + /// + /// Returns the localised label for a manually-entered job description (no URL provided). + /// + /// Two-letter language code. + string GetManualJobLabel(string language); } } diff --git a/Apis/api/Services/EmailApiEmailSender.cs b/Apis/api/Services/EmailApiEmailSender.cs index 86a416f..a886ac6 100644 --- a/Apis/api/Services/EmailApiEmailSender.cs +++ b/Apis/api/Services/EmailApiEmailSender.cs @@ -239,4 +239,7 @@ public sealed class EmailApiEmailSender : IEmailSender _emailTemplates.Render("email.match.subject", language, ("score", score.ToString()), ("jobLabel", jobLabel ?? "Job")); + + public string GetManualJobLabel(string language) => + _emailTemplates.Get("email.match.manual-job-label", language); } diff --git a/Apis/email-data/Migrations/20260608190611_AddManualJobLabelTemplate.Designer.cs b/Apis/email-data/Migrations/20260608190611_AddManualJobLabelTemplate.Designer.cs new file mode 100644 index 0000000..9bbf1b6 --- /dev/null +++ b/Apis/email-data/Migrations/20260608190611_AddManualJobLabelTemplate.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("20260608190611_AddManualJobLabelTemplate")] + partial class AddManualJobLabelTemplate + { + /// + 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/20260608190611_AddManualJobLabelTemplate.cs b/Apis/email-data/Migrations/20260608190611_AddManualJobLabelTemplate.cs new file mode 100644 index 0000000..6fe873f --- /dev/null +++ b/Apis/email-data/Migrations/20260608190611_AddManualJobLabelTemplate.cs @@ -0,0 +1,43 @@ +using Email.Data; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Email.Data.Migrations +{ + /// + public partial class AddManualJobLabelTemplate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.InsertData( + schema: MigrationConstants.SchemaName, + table: "Templates", + columns: ["Key", "Language", "Value", "Description"], + values: ["email.match.manual-job-label", "en", "Manual job description", "Label used in the match email subject and body when no job URL was provided"]); + + migrationBuilder.InsertData( + schema: MigrationConstants.SchemaName, + table: "Templates", + columns: ["Key", "Language", "Value", "Description"], + values: ["email.match.manual-job-label", "ro", "Descriere manuală a jobului", "Etichetă folosită în subiectul și corpul emailului când nu a fost furnizat un URL de job"]); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DeleteData( + schema: MigrationConstants.SchemaName, + table: "Templates", + keyColumns: ["Key", "Language"], + keyValues: ["email.match.manual-job-label", "en"]); + + migrationBuilder.DeleteData( + schema: MigrationConstants.SchemaName, + table: "Templates", + keyColumns: ["Key", "Language"], + keyValues: ["email.match.manual-job-label", "ro"]); + } + } +} diff --git a/Jobs/cv-search-job/Tasks/CvSearchJobTask.cs b/Jobs/cv-search-job/Tasks/CvSearchJobTask.cs index 43337d5..f2ffb2b 100644 --- a/Jobs/cv-search-job/Tasks/CvSearchJobTask.cs +++ b/Jobs/cv-search-job/Tasks/CvSearchJobTask.cs @@ -198,7 +198,9 @@ public sealed class CvSearchJobTask : IJobTask // Pre-fetched text passed directly so cv-matcher-api skips re-fetching the page JobDescription = jobText, // User already gave GDPR consent when they clicked the one-time job search link - GdprConsent = true + GdprConsent = true, + // Propagate language so the LLM uses the correct language-specific prompt + Language = session.Language }; var matchResult = await _matcherApi.MatchJobAsync(matchRequest, ct);