🌐 Localizable notification & email

This commit is contained in:
2025-05-17 18:50:14 +08:00
parent 6728bd5607
commit b5226a72f2
17 changed files with 404 additions and 17 deletions

View File

@ -0,0 +1,19 @@
using Microsoft.AspNetCore.Mvc;
namespace DysonNetwork.Sphere.Account;
[ApiController]
[Route("/spells")]
public class MagicSpellController(AppDatabase db, MagicSpellService sp) : ControllerBase
{
[HttpPost("{spellId:guid}/resend")]
public async Task<ActionResult> ResendMagicSpell(Guid spellId)
{
var spell = db.MagicSpells.FirstOrDefault(x => x.Id == spellId);
if (spell == null)
return NotFound();
await sp.NotifyMagicSpell(spell, true);
return Ok();
}
}

View File

@ -1,13 +1,23 @@
using System.Globalization;
using System.Security.Cryptography;
using DysonNetwork.Sphere.Account.Email;
using DysonNetwork.Sphere.Pages.Emails;
using DysonNetwork.Sphere.Permission;
using DysonNetwork.Sphere.Resources.Localization;
using DysonNetwork.Sphere.Resources.Pages.Emails;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Localization;
using NodaTime;
namespace DysonNetwork.Sphere.Account;
public class MagicSpellService(AppDatabase db, EmailService email, IConfiguration configuration, ILogger<MagicSpellService> logger)
public class MagicSpellService(
AppDatabase db,
EmailService email,
IConfiguration configuration,
ILogger<MagicSpellService> logger,
IStringLocalizer<Localization.EmailResource> localizer
)
{
public async Task<MagicSpell> CreateMagicSpell(
Account account,
@ -48,6 +58,15 @@ public class MagicSpellService(AppDatabase db, EmailService email, IConfiguratio
logger.LogInformation("Sending magic spell... {Link}", link);
var accountLanguage = await db.Accounts
.Where(a => a.Id == spell.AccountId)
.Select(a => a.Language)
.FirstOrDefaultAsync();
var cultureInfo = new CultureInfo(accountLanguage ?? "en-us", false);
CultureInfo.CurrentCulture = cultureInfo;
CultureInfo.CurrentUICulture = cultureInfo;
try
{
switch (spell.Type)
@ -56,7 +75,7 @@ public class MagicSpellService(AppDatabase db, EmailService email, IConfiguratio
await email.SendTemplatedEmailAsync<LandingEmail, LandingEmailModel>(
contact.Account.Name,
contact.Content,
"Confirm your registration",
localizer["EmailLandingTitle"],
new LandingEmailModel
{
Name = contact.Account.Name,

View File

@ -92,6 +92,24 @@
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>SharedResource.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Pages\Emails\LandingEmailResource.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>LandingEmail.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Localization\NotificationResource.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>NotificationResource.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Localization\EmailResource.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Email.LandingResource.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Localization\SharedResource.zh-hans.resx">
<DependentUpon>SharedResource.resx</DependentUpon>
</EmbeddedResource>
<EmbeddedResource Update="Resources\Localization\AccountEventResource.zh-hans.resx">
<DependentUpon>AccountEventResource.resx</DependentUpon>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
@ -105,6 +123,16 @@
<AutoGen>True</AutoGen>
<DependentUpon>SharedResource.resx</DependentUpon>
</Compile>
<Compile Update="Resources\Pages\Emails\LandingEmailResource.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>LandingEmailResource.resx</DependentUpon>
</Compile>
<Compile Update="Resources\Localization\NotificationResource.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>NotificationResource.resx</DependentUpon>
</Compile>
</ItemGroup>
</Project>

View File

@ -0,0 +1,2 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeEditing/Localization/MoveResource/LastResourceFile/@EntryValue">CFF62EFA-F4C2-4FC7-8D97-25570B4DB452/d:Resources/d:Localization/f:EmailResource.resx</s:String></wpf:ResourceDictionary>

View File

@ -0,0 +1,6 @@
namespace DysonNetwork.Sphere.Localization;
public class EmailResource
{
}

View File

@ -0,0 +1,6 @@
namespace DysonNetwork.Sphere.Localization;
public class NotificationResource
{
}

View File

@ -1,9 +1,13 @@
@using DysonNetwork.Sphere.Localization
@using Microsoft.Extensions.Localization
@using EmailResource = DysonNetwork.Sphere.Localization.EmailResource
<EmailLayout>
<table class="container">
<tr>
<td class="columns">
<h1 style="font-size: 1.875rem; font-weight: 700; color: #111827; margin: 0; text-align: center;">
Welcome to the Solar Network!
@(Localizer["LandingHeader1"])
</h1>
</td>
</tr>
@ -11,14 +15,13 @@
<tr>
<td class="columns">
<p style="color: #374151; margin: 0;">
Dear @Name,
@(Localizer["LandingPara1"]) @@@Name,
</p>
<p style="color: #374151; margin: 0;">
Thank you for creating an account on the Solar Network. We're excited to have you join our community!
@(Localizer["LandingPara2"])
</p>
<p style="color: #374151; margin: 0;">
To access all features and ensure the security of your account, please confirm your registration by
clicking the button below:
@(Localizer["LandingPara3"])
</p>
</td>
</tr>
@ -26,10 +29,10 @@
<tr>
<td class="columns">
<div style="text-align: center;">
<button href="@VerificationLink"
style="background-color: #2563eb; color: #ffffff; padding: 0.75rem 1.5rem; border-radius: 0.5rem; font-weight: 600; text-decoration: none;">
Confirm Registration
</button>
<a href="@VerificationLink" target="_blank"
style="background-color: #2563eb; color: #ffffff; padding: 0.75rem 1.5rem; border-radius: 0.5rem; font-weight: 600; text-decoration: none;">
@(Localizer["LandingButton1"])
</a>
</div>
</td>
</tr>
@ -37,16 +40,16 @@
<tr>
<td class="columns">
<p style="color: #374151; margin: 0;">
If the button doesn't work, you can also copy and paste this link into your browser:
@(LocalizerShared["EmailLinkHint"])
<br>
<a href="@VerificationLink" style="color: #2563eb; word-break: break-all;">@VerificationLink</a>
</p>
<p style="color: #374151; margin: 0;">
If you didn't create this account, please ignore this email.
@(Localizer["LandingPara4"])
</p>
<p style="color: #374151; margin: 2rem 0 0 0;">
Best regards,<br>
The Solar Network Team
@(LocalizerShared["EmailFooter1"]) <br />
@(LocalizerShared["EmailFooter2"])
</p>
</td>
</tr>
@ -56,4 +59,7 @@
@code {
[Parameter] public required string Name { get; set; }
[Parameter] public required string VerificationLink { get; set; }
[Inject] IStringLocalizer<EmailResource> Localizer { get; set; } = null!;
[Inject] IStringLocalizer<SharedResource> LocalizerShared { get; set; } = null!;
}

View File

@ -0,0 +1,90 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace DysonNetwork.Sphere.Resources.Pages.Emails {
using System;
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class EmailResource {
private static System.Resources.ResourceManager resourceMan;
private static System.Globalization.CultureInfo resourceCulture;
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal EmailResource() {
}
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
internal static System.Resources.ResourceManager ResourceManager {
get {
if (object.Equals(null, resourceMan)) {
System.Resources.ResourceManager temp = new System.Resources.ResourceManager("DysonNetwork.Sphere.Resources.Localization.EmailResource", typeof(EmailResource).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
internal static System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
internal static string LandingHeader1 {
get {
return ResourceManager.GetString("LandingHeader1", resourceCulture);
}
}
internal static string LandingPara1 {
get {
return ResourceManager.GetString("LandingPara1", resourceCulture);
}
}
internal static string LandingPara2 {
get {
return ResourceManager.GetString("LandingPara2", resourceCulture);
}
}
internal static string LandingPara3 {
get {
return ResourceManager.GetString("LandingPara3", resourceCulture);
}
}
internal static string LandingButton1 {
get {
return ResourceManager.GetString("LandingButton1", resourceCulture);
}
}
internal static string LandingPara4 {
get {
return ResourceManager.GetString("LandingPara4", resourceCulture);
}
}
internal static string EmailLandingTitle {
get {
return ResourceManager.GetString("EmailLandingTitle", resourceCulture);
}
}
}
}

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="LandingHeader1" xml:space="preserve">
<value>Welcome to the Solar Network!</value>
</data>
<data name="LandingPara1" xml:space="preserve">
<value>Dear, </value>
</data>
<data name="LandingPara2" xml:space="preserve">
<value>Thank you for creating an account on the Solar Network. We're excited to have you join our community!</value>
</data>
<data name="LandingPara3" xml:space="preserve">
<value>To access all features and ensure the security of your account, please confirm your registration by clicking the button below:</value>
</data>
<data name="LandingButton1" xml:space="preserve">
<value>Confirm Registration</value>
</data>
<data name="LandingPara4" xml:space="preserve">
<value>If you didn't create this account, please ignore this email.</value>
</data>
<data name="EmailLandingTitle" xml:space="preserve">
<value>Confirm your registration</value>
</data>
</root>

View File

@ -0,0 +1,35 @@
<root>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="LandingHeader1" xml:space="preserve">
<value>欢迎来到 Solar Network</value>
</data>
<data name="LandingPara1" xml:space="preserve">
<value>尊敬的 </value>
</data>
<data name="LandingButton1" xml:space="preserve">
<value>确认注册</value>
</data>
<data name="LandingPara2" xml:space="preserve">
<value>感谢你在 Solar Network 上注册帐号,我们很激动你即将加入我们的社区!</value>
</data>
<data name="LandingPara3" xml:space="preserve">
<value>点击下方按钮来确认你的注册以获得所有功能的权限。</value>
</data>
<data name="LandingPara4" xml:space="preserve">
<value>如果你并没有注册帐号,你可以忽略此邮件。</value>
</data>
<data name="EmailLandingTitle" xml:space="preserve">
<value>确认你的注册</value>
</data>
</root>

View File

@ -0,0 +1,48 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace DysonNetwork.Sphere.Resources.Localization {
using System;
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class NotificationResource {
private static System.Resources.ResourceManager resourceMan;
private static System.Globalization.CultureInfo resourceCulture;
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal NotificationResource() {
}
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
internal static System.Resources.ResourceManager ResourceManager {
get {
if (object.Equals(null, resourceMan)) {
System.Resources.ResourceManager temp = new System.Resources.ResourceManager("DysonNetwork.Sphere.Resources.Localization.NotificationResource", typeof(NotificationResource).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)]
internal static System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
}
}

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:element name="root" msdata:IsDataSet="true">
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@ -0,0 +1,14 @@
<root>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>1.3</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@ -28,7 +28,7 @@ namespace DysonNetwork.Sphere.Resources {
internal static System.Resources.ResourceManager ResourceManager {
get {
if (object.Equals(null, resourceMan)) {
System.Resources.ResourceManager temp = new System.Resources.ResourceManager("DysonNetwork.Sphere.Resources.SharedResource", typeof(SharedResource).Assembly);
System.Resources.ResourceManager temp = new System.Resources.ResourceManager("DysonNetwork.Sphere.Resources.Localization.SharedResource", typeof(SharedResource).Assembly);
resourceMan = temp;
}
return resourceMan;
@ -44,5 +44,23 @@ namespace DysonNetwork.Sphere.Resources {
resourceCulture = value;
}
}
internal static string EmailLinkHint {
get {
return ResourceManager.GetString("EmailLinkHint", resourceCulture);
}
}
internal static string EmailFooter1 {
get {
return ResourceManager.GetString("EmailFooter1", resourceCulture);
}
}
internal static string EmailFooter2 {
get {
return ResourceManager.GetString("EmailFooter2", resourceCulture);
}
}
}
}

View File

@ -18,4 +18,13 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="EmailLinkHint" xml:space="preserve">
<value>If the button doesn't work, you can also copy and paste this link into your browser:</value>
</data>
<data name="EmailFooter1" xml:space="preserve">
<value>Best regards,</value>
</data>
<data name="EmailFooter2" xml:space="preserve">
<value>The Solar Network Team</value>
</data>
</root>

View File

@ -11,4 +11,13 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="EmailFooter1" xml:space="preserve">
<value>此致</value>
</data>
<data name="EmailFooter2" xml:space="preserve">
<value>Solar Network 团队</value>
</data>
<data name="EmailLinkHint" xml:space="preserve">
<value>如果上方的按钮不起作用,你也可以复制下面的链接到浏览器。</value>
</data>
</root>